content: // 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.network.vpn;
  
  import java.lang.reflect.InvocationTargetException;
  import java.util.ArrayList;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  import java.util.stream.Collectors;
  
  import javax.inject.Inject;
  import javax.naming.ConfigurationException;
  
  import org.apache.cloudstack.acl.SecurityChecker.AccessType;
  import org.apache.cloudstack.api.command.user.vpn.ListRemoteAccessVpnsCmd;
  import org.apache.cloudstack.api.command.user.vpn.ListVpnUsersCmd;
  import org.apache.cloudstack.context.CallContext;
  import org.apache.cloudstack.framework.config.ConfigKey;
  import org.apache.cloudstack.framework.config.Configurable;
  import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
  import org.apache.commons.collections.CollectionUtils;
  
  import com.cloud.configuration.Config;
  import com.cloud.domain.DomainVO;
  import com.cloud.domain.dao.DomainDao;
  import com.cloud.event.ActionEvent;
  import com.cloud.event.EventTypes;
  import com.cloud.event.UsageEventUtils;
  import com.cloud.event.dao.UsageEventDao;
  import com.cloud.exception.AccountLimitException;
  import com.cloud.exception.InvalidParameterValueException;
  import com.cloud.exception.NetworkRuleConflictException;
  import com.cloud.exception.ResourceUnavailableException;
  import com.cloud.network.Network;
  import com.cloud.network.Network.Service;
  import com.cloud.network.NetworkModel;
  import com.cloud.network.PublicIpAddress;
  import com.cloud.network.RemoteAccessVpn;
  import com.cloud.network.VpnUser;
  import com.cloud.network.VpnUser.State;
  import com.cloud.network.VpnUserVO;
  import com.cloud.network.dao.FirewallRulesDao;
  import com.cloud.network.dao.IPAddressDao;
  import com.cloud.network.dao.IPAddressVO;
  import com.cloud.network.dao.RemoteAccessVpnDao;
  import com.cloud.network.dao.RemoteAccessVpnVO;
  import com.cloud.network.dao.VpnUserDao;
  import com.cloud.network.element.RemoteAccessVPNServiceProvider;
  import com.cloud.network.rules.FirewallManager;
  import com.cloud.network.rules.FirewallRule;
  import com.cloud.network.rules.FirewallRule.Purpose;
  import com.cloud.network.rules.FirewallRuleVO;
  import com.cloud.network.rules.RulesManager;
  import com.cloud.network.vpc.Vpc;
  import com.cloud.network.vpc.VpcManager;
  import com.cloud.network.vpc.dao.VpcDao;
  import com.cloud.projects.Project.ListProjectResourcesCriteria;
  import com.cloud.server.ConfigurationServer;
  import com.cloud.user.Account;
  import com.cloud.user.AccountManager;
  import com.cloud.user.DomainManager;
  import com.cloud.user.dao.AccountDao;
  import com.cloud.utils.NumbersUtil;
  import com.cloud.utils.Pair;
  import com.cloud.utils.PasswordGenerator;
  import com.cloud.utils.Ternary;
  import com.cloud.utils.component.ManagerBase;
  import com.cloud.utils.db.DB;
  import com.cloud.utils.db.Filter;
  import com.cloud.utils.db.JoinBuilder;
  import com.cloud.utils.db.SearchBuilder;
  import com.cloud.utils.db.SearchCriteria;
  import com.cloud.utils.db.SearchCriteria.Op;
  import com.cloud.utils.db.Transaction;
  import com.cloud.utils.db.TransactionCallback;
  import com.cloud.utils.db.TransactionCallbackNoReturn;
  import com.cloud.utils.db.TransactionCallbackWithException;
  import com.cloud.utils.db.TransactionStatus;
  import com.cloud.utils.exception.CloudRuntimeException;
  import com.cloud.utils.net.NetUtils;
  
  public class RemoteAccessVpnManagerImpl extends ManagerBase implements RemoteAccessVpnService, Configurable {
  
      static final ConfigKey<String> RemoteAccessVpnClientIpRange = new ConfigKey<String>("Network", String.class, RemoteAccessVpnClientIpRangeCK, "10.1.2.1-10.1.2.8",
          "The range of ips to be allocated to remote access vpn clients. The first ip in the range is used by the VPN server", false, ConfigKey.Scope.Account);
  
      @Inject
      AccountDao _accountDao;
      @Inject
      VpnUserDao _vpnUsersDao;
      @Inject
      RemoteAccessVpnDao _remoteAccessVpnDao;
      @Inject
      IPAddressDao _ipAddressDao;
      @Inject
      AccountManager _accountMgr;
      @Inject
      DomainManager _domainMgr;
      @Inject
      NetworkModel _networkMgr;
      @Inject
      RulesManager _rulesMgr;
      @Inject
      DomainDao _domainDao;
      @Inject
      FirewallRulesDao _rulesDao;
      @Inject
      FirewallManager _firewallMgr;
      @Inject
      UsageEventDao _usageEventDao;
      @Inject
      ConfigurationDao _configDao;
      @Inject
      VpcManager vpcManager;
  
      List<RemoteAccessVPNServiceProvider> _vpnServiceProviders;
  
      @Inject
      ConfigurationServer _configServer;
      @Inject
      VpcDao _vpcDao;
  
      int _userLimit;
      int _pskLength;
      SearchBuilder<RemoteAccessVpnVO> VpnSearch;
  
      private List<RemoteAccessVpnVO> getValidRemoteAccessVpnForAccount(long accountId) {
          List<RemoteAccessVpnVO> vpns = _remoteAccessVpnDao.findByAccount(accountId);
          if (CollectionUtils.isNotEmpty(vpns)) {
              List<RemoteAccessVpnVO> validVpns = new ArrayList<>();
              for (RemoteAccessVpnVO vpn : vpns) {
                  if (vpn.getNetworkId() != null) {
                      Network network = _networkMgr.getNetwork(vpn.getNetworkId());
                      if (!Network.State.Implemented.equals(network.getState())) {
                          continue;
                      }
                  }
                  validVpns.add(vpn);
              }
              vpns = validVpns;
          }
          return vpns;
      }
  
  
      @Override
      @DB
      public RemoteAccessVpn createRemoteAccessVpn(final long publicIpId, String ipRange, boolean openFirewall, final Boolean forDisplay) throws NetworkRuleConflictException {
          CallContext ctx = CallContext.current();
          final Account caller = ctx.getCallingAccount();
  
          final PublicIpAddress ipAddr = _networkMgr.getPublicIpAddress(publicIpId);
          if (ipAddr == null) {
              throw new InvalidParameterValueException(String.format("Unable to create remote access VPN, invalid public IP address {\"id\": %s}.", publicIpId));
          }
  
          _accountMgr.checkAccess(caller, null, true, ipAddr);
  
          if (!ipAddr.readyToUse()) {
              throw new InvalidParameterValueException("The Ip address is not ready to be used yet: " + ipAddr.getAddress());
          }
  
          try {
              IPAddressVO ipAddress = _ipAddressDao.acquireInLockTable(publicIpId);
  
              if (ipAddress == null) {
                  logger.error(String.format("Unable to acquire lock on public IP %s.", publicIpId));
                  throw new CloudRuntimeException("Unable to acquire lock on public IP.");
              }
  
              Long networkId = ipAddress.getAssociatedWithNetworkId();
              if (networkId != null) {
                  if (ipAddress.isForRouter()) {
                      logger.debug("The IP address is reserved for Domain Router, skipping the check now");
                  } else {
                      _networkMgr.checkIpForService(ipAddress, Service.Vpn, null);
                  }
              }
  
              final Long vpcId = ipAddress.getVpcId();
              if (vpcId != null && ipAddress.isSourceNat()) {
                  assert networkId == null;
                  openFirewall = false;
              }
  
              final boolean openFirewallFinal = openFirewall;
  
              if (networkId == null && vpcId == null) {
                  throw new InvalidParameterValueException("Unable to create remote access vpn for the ipAddress: " + ipAddr.getAddress().addr() +
                          " as ip is not associated with any network or VPC");
              }
  
              RemoteAccessVpnVO vpnVO = _remoteAccessVpnDao.findByPublicIpAddress(publicIpId);
  
              if (vpnVO != null) {
                  if (vpnVO.getState() == RemoteAccessVpn.State.Added) {
                      return vpnVO;
                  }
  
                  throw new InvalidParameterValueException(String.format("A remote Access VPN already exists for the public IP address [%s].", ipAddr.getAddress().toString()));
              }
  
              if (ipRange == null) {
                  ipRange = RemoteAccessVpnClientIpRange.valueIn(ipAddr.getAccountId());
              }
  
              validateIpRange(ipRange, InvalidParameterValueException.class);
  
              String[] range = ipRange.split("-");
  
              Pair<String, Integer> cidr = null;
  
              if (networkId != null) {
                  Network network = _networkMgr.getNetwork(networkId);
                  long ipAddressOwner = ipAddr.getAccountId();
                  vpnVO = _remoteAccessVpnDao.findByAccountAndNetwork(ipAddressOwner, networkId);
                  if (vpnVO != null) {
                      if (vpnVO.getState() == RemoteAccessVpn.State.Added) {
                          return vpnVO;
                      }
  
                      throw new InvalidParameterValueException(String.format("A remote access VPN already exists for the network %s.", network));
                  }
                  if (!_networkMgr.areServicesSupportedInNetwork(network.getId(), Service.Vpn)) {
                      throw new InvalidParameterValueException("Vpn service is not supported in network id=" + ipAddr.getAssociatedWithNetworkId());
                  }
                  cidr = NetUtils.getCidr(network.getCidr());
                  validateIpAddressForVpnServiceOnNetwork(network, ipAddress);
              } else {
                  Vpc vpc = _vpcDao.findById(vpcId);
                  cidr = NetUtils.getCidr(vpc.getCidr());
                  validateIpAddressForVpnServiceOnVpc(vpc, ipAddress);
              }
  
              String[] guestIpRange = NetUtils.getIpRangeFromCidr(cidr.first(), cidr.second());
              if (NetUtils.ipRangesOverlap(range[0], range[1], guestIpRange[0], guestIpRange[1])) {
                  throw new InvalidParameterValueException("Invalid ip range: " + ipRange + " overlaps with guest ip range " + guestIpRange[0] + "-" + guestIpRange[1]);
              }
  
              long startIp = NetUtils.ip2Long(range[0]);
              final String newIpRange = NetUtils.long2Ip(++startIp) + "-" + range[1];
              final String sharedSecret = PasswordGenerator.generatePresharedKey(_pskLength);
  
              return Transaction.execute((TransactionCallbackWithException<RemoteAccessVpn, NetworkRuleConflictException>) status -> {
                  if (vpcId == null) {
                      _rulesMgr.reservePorts(ipAddr, NetUtils.UDP_PROTO, Purpose.Vpn, openFirewallFinal, caller, NetUtils.VPN_PORT, NetUtils.VPN_L2TP_PORT,
                              NetUtils.VPN_NATT_PORT);
                  }
                  RemoteAccessVpnVO remoteAccessVpnVO = new RemoteAccessVpnVO(ipAddr.getAccountId(), ipAddr.getDomainId(), ipAddr.getAssociatedWithNetworkId(),
                          publicIpId, vpcId, range[0], newIpRange, sharedSecret);
  
                  if (forDisplay != null) {
                      remoteAccessVpnVO.setDisplay(forDisplay);
                  }
                  remoteAccessVpnVO = _remoteAccessVpnDao.persist(remoteAccessVpnVO);
                  if (vpcId != null) {
                      try {
                          vpcManager.applyStaticRouteForVpcVpnIfNeeded(vpcId, false);
                      } catch (ResourceUnavailableException | CloudRuntimeException e) {
                          logger.error("Unable to apply static routes for vpc " + vpcId + " due to " + e.getMessage());
                      }
                  }
                  return remoteAccessVpnVO;
              });
          } finally {
              _ipAddressDao.releaseFromLockTable(publicIpId);
          }
      }
  
      private void validateIpAddressForVpnServiceOnNetwork(Network network, IPAddressVO ipAddress) {
          Long networkId = network.getId();
          if (_networkMgr.isProviderSupportServiceInNetwork(networkId, Service.Vpn, Network.Provider.VirtualRouter)) {
              // if VR is the VPN provider,
              // (1) if VR is Source NAT, the IP address must be used as Source NAT
              // (2) if VR is not Source NAT, the IP address must not be used as Source NAT
              boolean isVRSourceNat = _networkMgr.isProviderSupportServiceInNetwork(networkId, Service.SourceNat, Network.Provider.VirtualRouter);
              if (isVRSourceNat && !ipAddress.isSourceNat()) {
                  throw new InvalidParameterValueException("Vpn service can only be configured on the Source NAT IP of network id=" + ipAddress.getAssociatedWithNetworkId());
              }
              if (!isVRSourceNat && ipAddress.isSourceNat()) {
                  throw new InvalidParameterValueException("Vpn service can not be configured on the Source NAT IP of network id=" + ipAddress.getAssociatedWithNetworkId());
              }
              if (!isVRSourceNat) {
                  throw new InvalidParameterValueException("Currently it is not supported to create Vpn service on a non-Source NAT IP of network id=" + ipAddress.getAssociatedWithNetworkId());
              }
          }
      }
  
      private void validateIpAddressForVpnServiceOnVpc(Vpc vpc, IPAddressVO ipAddress) {
          Long vpcId = vpc.getId();
          if (vpcManager.isProviderSupportServiceInVpc(vpcId, Service.Vpn, Network.Provider.VPCVirtualRouter)) {
              // if VPC VR is the VPN provider,
              // (1) if VPC VR is Source NAT, the IP address must be used as Source NAT
              // (2) if VPC VR is not Source NAT, the IP address must not be used as Source NAT
              boolean isVpcVRSourceNat = vpcManager.isProviderSupportServiceInVpc(vpcId, Service.SourceNat, Network.Provider.VPCVirtualRouter);
              if (isVpcVRSourceNat && !ipAddress.isSourceNat()) {
                  throw new InvalidParameterValueException("Vpn service can only be configured on the Source NAT IP of VPC id=" + ipAddress.getVpcId());
              }
              if (!isVpcVRSourceNat) {
                  IPAddressVO ipAddressForVpcVR = vpcManager.getIpAddressForVpcVr(vpc, ipAddress, true);
                  if (!vpcManager.configStaticNatForVpcVr(vpc, ipAddressForVpcVR)) {
                      throw new CloudRuntimeException("Failed to enable static nat for VPC VR as part of remote access vpn creation");
                  }
              }
          }
      }
  
      private void validateRemoteAccessVpnConfiguration() throws ConfigurationException {
          String ipRange = RemoteAccessVpnClientIpRange.value();
          if (ipRange == null) {
              logger.warn(String.format("Remote access VPN configuration: Global configuration [%s] missing client IP range.", RemoteAccessVpnClientIpRange.key()));
              return;
          }
  
          if (_pskLength < 8 || _pskLength > 256) {
              throw new ConfigurationException(String.format("Remote access VPN configuration: IPSec preshared key length [%s] should be between 8 and 256.", _pskLength));
          }
  
          validateIpRange(ipRange, ConfigurationException.class);
      }
  
      protected <T extends Throwable> void validateIpRange(String ipRange, Class<T> exceptionClass) throws T {
          String[] range = ipRange.split("-");
  
          if (range.length != 2) {
              handleExceptionOnValidateIpRangeError(exceptionClass, String.format("IP range [%s] is an invalid IP range.", ipRange));
          }
  
          if (!NetUtils.isValidIp4(range[0]) || !NetUtils.isValidIp4(range[1])) {
              handleExceptionOnValidateIpRangeError(exceptionClass, String.format("One or both IPs sets in the range [%s] are invalid IPs.", ipRange));
          }
  
          if (!NetUtils.validIpRange(range[0], range[1])) {
              handleExceptionOnValidateIpRangeError(exceptionClass, String.format("Range of IPs [%s] is invalid.", ipRange));
          }
      }
  
      protected <T extends Throwable> void handleExceptionOnValidateIpRangeError(Class<T> exceptionClass, String errorMessage) throws T {
          try {
              throw exceptionClass.getConstructor(String.class).newInstance(errorMessage);
          } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
              throw new CloudRuntimeException(String.format("Unexpected exception [%s] while throwing error [%s] on validateIpRange.", ex.getMessage(), errorMessage), ex);
          }
      }
  
      @Override
      @DB
      @ActionEvent(eventType = EventTypes.EVENT_REMOTE_ACCESS_VPN_DESTROY, eventDescription = "removing remote access vpn", async = true)
      public boolean destroyRemoteAccessVpnForIp(long ipId, Account caller, final boolean forceCleanup) throws ResourceUnavailableException {
          final RemoteAccessVpnVO vpn = _remoteAccessVpnDao.findByPublicIpAddress(ipId);
          if (vpn == null) {
              logger.debug("there are no Remote access vpns for public ip address id=" + ipId);
              return true;
          }
  
          _accountMgr.checkAccess(caller, AccessType.OperateEntry, true, vpn);
  
          RemoteAccessVpn.State prevState = vpn.getState();
          vpn.setState(RemoteAccessVpn.State.Removed);
          _remoteAccessVpnDao.update(vpn.getId(), vpn);
  
          boolean success = false;
          try {
              for (RemoteAccessVPNServiceProvider element : _vpnServiceProviders) {
                  if (element.stopVpn(vpn)) {
                      success = true;
                      break;
                  }
              }
          }catch (ResourceUnavailableException ex) {
              vpn.setState(prevState);
              _remoteAccessVpnDao.update(vpn.getId(), vpn);
              logger.debug("Failed to stop the vpn {}, so reverted state to {}", vpn, RemoteAccessVpn.State.Running);
              success = false;
          } finally {
              if (success|| forceCleanup) {
                  final List<? extends FirewallRule> vpnFwRules = _rulesDao.listByIpAndPurpose(ipId, Purpose.Vpn);
  
                  boolean applyFirewall = false;
                  final List<FirewallRuleVO> fwRules = new ArrayList<>();
  
                  if (CollectionUtils.isNotEmpty(vpnFwRules) && _rulesDao.findByRelatedId(vpnFwRules.get(0).getId()) != null) {
                      applyFirewall = true;
                  }
  
                  if (applyFirewall) {
                      Transaction.execute(new TransactionCallbackNoReturn() {
                          @Override
                          public void doInTransactionWithoutResult(TransactionStatus status) {
                              for (FirewallRule vpnFwRule : vpnFwRules) {
                                  _firewallMgr.revokeRelatedFirewallRule(vpnFwRule.getId(), false);
                                  fwRules.add(_rulesDao.findByRelatedId(vpnFwRule.getId()));
                              }
  
                              logger.debug("Marked " + fwRules.size() + " firewall rules as Revoked as a part of disable remote access vpn");
                          }
                      });
  
                      logger.debug("Reapplying firewall rules for ip id=" + ipId + " as a part of disable remote access vpn");
                      success = _firewallMgr.applyIngressFirewallRules(ipId, caller);
                  }
  
                  if (success|| forceCleanup) {
                      try {
                          Transaction.execute(new TransactionCallbackNoReturn() {
                              @Override
                              public void doInTransactionWithoutResult(TransactionStatus status) {
                                  if (vpn.getVpcId() != null) {
                                      try {
                                          vpcManager.applyStaticRouteForVpcVpnIfNeeded(vpn.getVpcId(), false);
                                      } catch (ResourceUnavailableException | CloudRuntimeException e) {
                                          logger.error("Unable to apply static routes for vpc " + vpn.getVpcId() + " due to " + e.getMessage());
                                      }
                                  }
  
                                  _remoteAccessVpnDao.remove(vpn.getId());
  
                                  List<VpnUserVO> vpnUsers = _vpnUsersDao.listByAccount(vpn.getAccountId());
                                  for (VpnUserVO user : vpnUsers) {
                                      if (user.getState() != VpnUser.State.Revoke) {
                                          UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VPN_USER_REMOVE, user.getAccountId(), 0, user.getId(), user.getUsername(),
                                              user.getClass().getName(), user.getUuid());
                                      }
                                  }
                                  if (vpnFwRules != null) {
                                      for (FirewallRule vpnFwRule : vpnFwRules) {
                                          _rulesDao.remove(vpnFwRule.getId());
                                          logger.debug("Successfully removed firewall rule with ip id=" + vpnFwRule.getSourceIpAddressId() + " and port " +
                                              vpnFwRule.getSourcePortStart() + " as a part of vpn cleanup");
                                      }
                                  }
                              }
                          });
                      } catch (Exception ex) {
                          logger.warn(String.format("Unable to release the VPN ports from the firewall rules [%s] due to [%s]", fwRules.stream().map(rule ->
                            String.format("{\"ipId\": %s, \"port\": %s}", rule.getSourceIpAddressId(), rule.getSourcePortStart())).collect(Collectors.joining(", ")), ex.getMessage()), ex);
                      }
                  }
              }
          }
          return success;
      }
  
      @Override
      @DB
      public VpnUser addVpnUser(final long vpnOwnerId, final String username, final String password) {
          final Account caller = CallContext.current().getCallingAccount();
  
          if (!username.matches("^[a-zA-Z0-9][a-zA-Z0-9@._-]{2,63}$")) {
              throw new InvalidParameterValueException(String.format("Username [%s] is invalid. Username has to begin with an alphabet have 3-64 characters including alphabets, numbers and the set '@.-_'.", username));
          }
          if (!password.matches("^[a-zA-Z0-9][a-zA-Z0-9@+=._-]{2,31}$")) {
              throw new InvalidParameterValueException("Password has to be 3-32 characters including alphabets, numbers and the set '@+=.-_'");
          }
  
          return Transaction.execute(new TransactionCallback<VpnUser>() {
              @Override
              public VpnUser doInTransaction(TransactionStatus status) {
                  Account owner = _accountDao.lockRow(vpnOwnerId, true);
                  if (owner == null) {
                      throw new InvalidParameterValueException(String.format("Unable to add VPN user {\"id\": %s, \"username\": \"%s\"}: Another operation is active.", vpnOwnerId, username));
                  }
                  _accountMgr.checkAccess(caller, null, true, owner);
  
                  VpnUserVO vpnUser = _vpnUsersDao.findByAccountAndUsername(owner.getId(), username);
                  if (vpnUser != null) {
                      throw new InvalidParameterValueException("VPN User with name " + username + " is already added for account " + owner);
                  }
  
                  long userCount = _vpnUsersDao.getVpnUserCount(owner.getId());
                  if (userCount >= _userLimit) {
                      throw new AccountLimitException(String.format("Cannot add more than [%s] remote access VPN users to %s.", _userLimit, owner.toString()));
                  }
  
                  VpnUser user = _vpnUsersDao.persist(new VpnUserVO(vpnOwnerId, owner.getDomainId(), username, password));
                  UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VPN_USER_ADD, user.getAccountId(), 0, user.getId(), user.getUsername(), user.getClass().getName(),
                      user.getUuid());
  
                  return user;
              }
          });
      }
  
      @DB
      @Override
      public boolean removeVpnUser(Account vpnOwner, String username, Account caller) {
          final VpnUserVO user = _vpnUsersDao.findByAccountAndUsername(vpnOwner.getId(), username);
          if (user == null) {
              String errorMessage = String.format("Could not find VPN user=[%s]. VPN owner=[%s]", username, vpnOwner);
              logger.debug(errorMessage);
              throw new InvalidParameterValueException(errorMessage);
          }
          _accountMgr.checkAccess(caller, null, true, user);
  
          Transaction.execute(new TransactionCallbackNoReturn() {
              @Override
              public void doInTransactionWithoutResult(TransactionStatus status) {
                  user.setState(State.Revoke);
                  _vpnUsersDao.update(user.getId(), user);
                  UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VPN_USER_REMOVE, user.getAccountId(), 0, user.getId(), user.getUsername(), user.getClass().getName(),
                      user.getUuid());
              }
          });
  
          return true;
      }
  
      @Override
      public List<? extends VpnUser> listVpnUsers(long vpnOwnerId, String userName) {
          Account caller = CallContext.current().getCallingAccount();
          Account owner = _accountDao.findById(vpnOwnerId);
          _accountMgr.checkAccess(caller, null, true, owner);
          return _vpnUsersDao.listByAccount(vpnOwnerId);
      }
  
      @Override
      @DB
      @ActionEvent(eventType = EventTypes.EVENT_REMOTE_ACCESS_VPN_CREATE, eventDescription = "creating remote access vpn", async = true)
      public RemoteAccessVpnVO startRemoteAccessVpn(long ipAddressId, boolean openFirewall) throws ResourceUnavailableException {
          Account caller = CallContext.current().getCallingAccount();
  
          final RemoteAccessVpnVO vpn = _remoteAccessVpnDao.findByPublicIpAddress(ipAddressId);
          if (vpn == null) {
              throw new InvalidParameterValueException("Unable to find your vpn: " + ipAddressId);
          }
  
          if (vpn.getVpcId() != null) {
              openFirewall = false;
          }
  
          _accountMgr.checkAccess(caller, null, true, vpn);
  
          boolean started = false;
          try {
              boolean firewallOpened = true;
              if (openFirewall) {
                  firewallOpened = _firewallMgr.applyIngressFirewallRules(vpn.getServerAddressId(), caller);
              }
  
              if (firewallOpened) {
                  for (RemoteAccessVPNServiceProvider element : _vpnServiceProviders) {
                      if (element.startVpn(vpn)) {
                          started = true;
                          break;
                      }
                  }
              }
  
              return vpn;
          } finally {
              if (started) {
                  Transaction.execute(new TransactionCallbackNoReturn() {
                      @Override
                      public void doInTransactionWithoutResult(TransactionStatus status) {
                          vpn.setState(RemoteAccessVpn.State.Running);
                          _remoteAccessVpnDao.update(vpn.getId(), vpn);
  
                          List<VpnUserVO> vpnUsers = _vpnUsersDao.listByAccount(vpn.getAccountId());
                          for (VpnUserVO user : vpnUsers) {
                              if (user.getState() != VpnUser.State.Revoke) {
                                  UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VPN_USER_ADD, user.getAccountId(), 0, user.getId(), user.getUsername(),
                                      user.getClass().getName(), user.getUuid());
                              }
                          }
                      }
                  });
              }
          }
      }
  
      @DB
      private boolean removeVpnUserWithoutRemoteAccessVpn(Account vpnOwner, String userName) {
          VpnUserVO vpnUser = _vpnUsersDao.findByAccountAndUsername(vpnOwner.getId(), userName);
          if (vpnUser == null) {
              logger.error("VPN user not found with owner: {} and username: {}", vpnOwner, userName);
              return false;
          }
          if (!State.Revoke.equals(vpnUser.getState())) {
              logger.error("VPN user with owner: {} and username: {} is not in revoked state, current state: {}", vpnOwner, userName, vpnUser.getState());
              return false;
          }
          return _vpnUsersDao.remove(vpnUser.getId());
      }
  
      @DB
      @Override
      public boolean applyVpnUsers(long vpnOwnerId, String userName, boolean forRemove) throws  ResourceUnavailableException {
          Account caller = CallContext.current().getCallingAccount();
          Account owner = _accountDao.findById(vpnOwnerId);
          _accountMgr.checkAccess(caller, null, true, owner);
  
          logger.debug(String.format("Applying VPN users for %s.", owner.toString()));
          List<RemoteAccessVpnVO> vpns = getValidRemoteAccessVpnForAccount(vpnOwnerId);
  
          if (CollectionUtils.isEmpty(vpns)) {
              if (forRemove) {
                  return removeVpnUserWithoutRemoteAccessVpn(owner, userName);
              }
              logger.warn(String.format("Unable to apply VPN user due to there are no remote access VPNs configured on %s to apply VPN user.", owner.toString()));
              return true;
          }
  
          RemoteAccessVpnVO vpnTemp  = null;
  
          List<VpnUserVO> users = _vpnUsersDao.listByAccount(vpnOwnerId);
  
          for (VpnUserVO user : users) {
              if (user.getState() == State.Active) {
                  user.setState(State.Add);
                  _vpnUsersDao.update(user.getId(), user);
              }
          }
  
          boolean success = true;
  
          Boolean[] finals = new Boolean[users.size()];
          for (RemoteAccessVPNServiceProvider element : _vpnServiceProviders) {
              logger.debug("Applying vpn access to " + element.getName());
              for (RemoteAccessVpnVO vpn : vpns) {
                  try {
                      String[] results = element.applyVpnUsers(vpn, users);
                      if (results != null) {
                          int indexUser = -1;
                          for (String result : results) {
                              indexUser ++;
                              if (indexUser == users.size()) {
                                  indexUser = 0;
                              }
                              logger.debug("VPN User {}{}{}", users.get(indexUser), result == null ? " is set on " : (" couldn't be set due to " + result) + " on ", vpn);
                              if (result == null) {
                                  if (finals[indexUser] == null) {
                                      finals[indexUser] = true;
                                  }
                              } else {
                                  finals[indexUser] = false;
                                  success = false;
                                  vpnTemp = vpn;
                              }
                          }
                      }
                  } catch (ResourceUnavailableException e) {
                      logger.warn(String.format("Unable to apply VPN users [%s] due to [%s].", users.stream().map(user -> user.toString()).collect(Collectors.joining(", ")), e.getMessage()), e);
                      success = false;
                      vpnTemp = vpn;
  
                      for (int i = 0; i < finals.length; i++) {
                          finals[i] = false;
                      }
                  }
              }
          }
  
          for (int i = 0; i < finals.length; i++) {
              final VpnUserVO user = users.get(i);
              if (finals[i]) {
                  if (user.getState() == State.Add) {
                      user.setState(State.Active);
                      _vpnUsersDao.update(user.getId(), user);
                  } else if (user.getState() == State.Revoke) {
                      _vpnUsersDao.remove(user.getId());
                  }
              } else {
                  if (user.getState() == State.Add && (user.getUsername()).equals(userName)) {
                      Transaction.execute(new TransactionCallbackNoReturn() {
                          @Override
                          public void doInTransactionWithoutResult(TransactionStatus status) {
                              _vpnUsersDao.remove(user.getId());
                              UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VPN_USER_REMOVE, user.getAccountId(), 0, user.getId(), user.getUsername(), user.getClass()
                                  .getName(), user.getUuid());
                          }
                      });
                  }
  
                  logger.warn(String.format("Failed to apply VPN for %s.", user.toString()));
              }
          }
  
          if (!success) {
              throw new  ResourceUnavailableException("Failed add vpn user due to Resource unavailable ",
                      RemoteAccessVPNServiceProvider.class, vpnTemp.getId());
          }
  
          return success;
      }
  
      @DB
      @Override
      public boolean applyVpnUsers(long vpnOwnerId, String userName) throws  ResourceUnavailableException {
          return applyVpnUsers(vpnOwnerId, userName, false);
      }
  
      @Override
      public Pair<List<? extends VpnUser>, Integer> searchForVpnUsers(ListVpnUsersCmd cmd) {
          String username = cmd.getUsername();
          Long id = cmd.getId();
          String keyword = cmd.getKeyword();
          Account caller = CallContext.current().getCallingAccount();
          List<Long> permittedAccounts = new ArrayList<>();
  
          Ternary<Long, Boolean, ListProjectResourcesCriteria> domainIdRecursiveListProject = new Ternary<>(cmd.getDomainId(), cmd.isRecursive(), null);
          _accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, cmd.listAll(), false);
          Long domainId = domainIdRecursiveListProject.first();
          Boolean isRecursive = domainIdRecursiveListProject.second();
          ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third();
          Filter searchFilter = new Filter(VpnUserVO.class, "username", true, cmd.getStartIndex(), cmd.getPageSizeVal());
          SearchBuilder<VpnUserVO> sb = _vpnUsersDao.createSearchBuilder();
          _accountMgr.buildACLSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria);
  
  
          sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ);
          sb.and("username", sb.entity().getUsername(), SearchCriteria.Op.EQ);
          sb.and("keyword", sb.entity().getUsername(), SearchCriteria.Op.LIKE);
          sb.and("state", sb.entity().getState(), Op.IN);
  
          SearchCriteria<VpnUserVO> sc = sb.create();
          _accountMgr.buildACLSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria);
  
          sc.setParameters("state", State.Active, State.Add);
  
          if(keyword != null){
              sc.setParameters("keyword", "%" + keyword + "%");
          }
  
          if (id != null) {
              sc.setParameters("id", id);
          }
  
          if (username != null) {
              sc.setParameters("username", username);
          }
  
          Pair<List<VpnUserVO>, Integer> result = _vpnUsersDao.searchAndCount(sc, searchFilter);
          return new Pair<>(result.first(), result.second());
      }
  
      @Override
      public Pair<List<? extends RemoteAccessVpn>, Integer> searchForRemoteAccessVpns(ListRemoteAccessVpnsCmd cmd) {
          Account caller = CallContext.current().getCallingAccount();
          Long ipAddressId = cmd.getPublicIpId();
          List<Long> permittedAccounts = new ArrayList<>();
  
          Long vpnId = cmd.getId();
          Long networkId = cmd.getNetworkId();
  
          if (ipAddressId != null) {
              PublicIpAddress publicIp = _networkMgr.getPublicIpAddress(ipAddressId);
              if (publicIp == null) {
                  throw new InvalidParameterValueException("Unable to list remote access vpns, IP address " + ipAddressId + " not found.");
              } else {
                  Long ipAddrAcctId = publicIp.getAccountId();
                  if (ipAddrAcctId == null) {
                      throw new InvalidParameterValueException("Unable to list remote access vpns, IP address " + ipAddressId + " is not associated with an account.");
                  }
              }
              _accountMgr.checkAccess(caller, null, true, publicIp);
          }
  
          Ternary<Long, Boolean, ListProjectResourcesCriteria> domainIdRecursiveListProject = new Ternary<>(cmd.getDomainId(), cmd.isRecursive(), null);
          _accountMgr.buildACLSearchParameters(caller, null, cmd.getAccountName(), cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, cmd.listAll(), false);
          Long domainId = domainIdRecursiveListProject.first();
          Boolean isRecursive = domainIdRecursiveListProject.second();
          ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third();
  
          Filter filter = new Filter(RemoteAccessVpnVO.class, "serverAddressId", false, cmd.getStartIndex(), cmd.getPageSizeVal());
          SearchBuilder<RemoteAccessVpnVO> sb = _remoteAccessVpnDao.createSearchBuilder();
          _accountMgr.buildACLSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria);
  
          sb.and("serverAddressId", sb.entity().getServerAddressId(), Op.EQ);
          sb.and("id", sb.entity().getId(), Op.EQ);
          sb.and("networkId", sb.entity().getNetworkId(), Op.EQ);
          sb.and("state", sb.entity().getState(), Op.EQ);
          sb.and("display", sb.entity().isDisplay(), Op.EQ);
  
          SearchCriteria<RemoteAccessVpnVO> sc = sb.create();
          _accountMgr.buildACLSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria);
  
  
          sc.setParameters("state", RemoteAccessVpn.State.Running);
  
          if (ipAddressId != null) {
              sc.setParameters("serverAddressId", ipAddressId);
          }
  
          if (vpnId != null) {
              sc.setParameters("id", vpnId);
          }
  
          if (networkId != null) {
              sc.setParameters("networkId", networkId);
          }
  
          Pair<List<RemoteAccessVpnVO>, Integer> result = _remoteAccessVpnDao.searchAndCount(sc, filter);
          return new Pair<>(result.first(), result.second());
      }
  
      @Override
      public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
          Map<String, String> configs = _configDao.getConfiguration(params);
  
          _userLimit = NumbersUtil.parseInt(configs.get(Config.RemoteAccessVpnUserLimit.key()), 8);
  
          _pskLength = NumbersUtil.parseInt(configs.get(Config.RemoteAccessVpnPskLength.key()), 24);
  
          validateRemoteAccessVpnConfiguration();
  
          VpnSearch = _remoteAccessVpnDao.createSearchBuilder();
          VpnSearch.and("accountId", VpnSearch.entity().getAccountId(), SearchCriteria.Op.EQ);
          SearchBuilder<DomainVO> domainSearch = _domainDao.createSearchBuilder();
          domainSearch.and("path", domainSearch.entity().getPath(), SearchCriteria.Op.LIKE);
          VpnSearch.join("domainSearch", domainSearch, VpnSearch.entity().getDomainId(), domainSearch.entity().getId(), JoinBuilder.JoinType.INNER);
          VpnSearch.done();
  
          return true;
      }
  
      @Override
      public List<? extends RemoteAccessVpn> listRemoteAccessVpns(long networkId) {
          return _remoteAccessVpnDao.listByNetworkId(networkId);
      }
  
      @Override
      public RemoteAccessVpn getRemoteAccessVpn(long vpnAddrId) {
          return _remoteAccessVpnDao.findByPublicIpAddress(vpnAddrId);
      }
  
      @Override
      public RemoteAccessVpn getRemoteAccessVpnById(long vpnId) {
          return _remoteAccessVpnDao.findById(vpnId);
      }
  
      public List<RemoteAccessVPNServiceProvider> getRemoteAccessVPNServiceProviders() {
          List<RemoteAccessVPNServiceProvider> result = new ArrayList<>();
          for (Iterator<RemoteAccessVPNServiceProvider> e = _vpnServiceProviders.iterator(); e.hasNext();) {
              result.add(e.next());
          }
  
          return result;
      }
  
      @Override
      public String getConfigComponentName() {
          return RemoteAccessVpnService.class.getSimpleName();
      }
  
      @Override
      public ConfigKey<?>[] getConfigKeys() {
          return new ConfigKey<?>[] {RemoteAccessVpnClientIpRange};
      }
  
      public List<RemoteAccessVPNServiceProvider> getVpnServiceProviders() {
          return _vpnServiceProviders;
      }
  
      public void setVpnServiceProviders(List<RemoteAccessVPNServiceProvider> vpnServiceProviders) {
          _vpnServiceProviders = vpnServiceProviders;
      }
  
      @Override
      @ActionEvent(eventType = EventTypes.EVENT_REMOTE_ACCESS_VPN_UPDATE, eventDescription = "updating remote access vpn", async = true)
      public RemoteAccessVpn updateRemoteAccessVpn(long id, String customId, Boolean forDisplay) {
          final RemoteAccessVpnVO vpn = _remoteAccessVpnDao.findById(id);
          if (vpn == null) {
              throw new InvalidParameterValueException("Can't find remote access vpn by id " + id);
          }
  
          _accountMgr.checkAccess(CallContext.current().getCallingAccount(), null, true, vpn);
          if (customId != null) {
              vpn.setUuid(customId);
          }
          if (forDisplay != null) {
              vpn.setDisplay(forDisplay);
          }
  
          _remoteAccessVpnDao.update(vpn.getId(), vpn);
          return _remoteAccessVpnDao.findById(id);
      }
  
  }
  