blob: 48d65189476fd034368461e1458f006fad17a387 [file] [log] [blame]
// 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.projects;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.mail.Authenticator;
import javax.mail.Message.RecipientType;
import javax.mail.MessagingException;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.URLName;
import javax.mail.internet.InternetAddress;
import javax.naming.ConfigurationException;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Component;
import com.sun.mail.smtp.SMTPMessage;
import com.sun.mail.smtp.SMTPSSLTransport;
import com.sun.mail.smtp.SMTPTransport;
import org.apache.cloudstack.acl.SecurityChecker.AccessType;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
import com.cloud.api.ApiDBUtils;
import com.cloud.api.query.dao.ProjectAccountJoinDao;
import com.cloud.api.query.dao.ProjectInvitationJoinDao;
import com.cloud.api.query.dao.ProjectJoinDao;
import com.cloud.configuration.Config;
import com.cloud.configuration.ConfigurationManager;
import com.cloud.configuration.Resource.ResourceType;
import com.cloud.domain.DomainVO;
import com.cloud.domain.dao.DomainDao;
import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.projects.Project.State;
import com.cloud.projects.ProjectAccount.Role;
import com.cloud.projects.dao.ProjectAccountDao;
import com.cloud.projects.dao.ProjectDao;
import com.cloud.projects.dao.ProjectInvitationDao;
import com.cloud.tags.dao.ResourceTagDao;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.user.AccountVO;
import com.cloud.user.DomainManager;
import com.cloud.user.ResourceLimitService;
import com.cloud.user.User;
import com.cloud.user.dao.AccountDao;
import com.cloud.utils.DateUtil;
import com.cloud.utils.NumbersUtil;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.concurrency.NamedThreadFactory;
import com.cloud.utils.db.DB;
import com.cloud.utils.db.Transaction;
import com.cloud.utils.db.TransactionCallback;
import com.cloud.utils.db.TransactionCallbackNoReturn;
import com.cloud.utils.db.TransactionCallbackWithExceptionNoReturn;
import com.cloud.utils.db.TransactionStatus;
import com.cloud.utils.exception.CloudRuntimeException;
@Component
public class ProjectManagerImpl extends ManagerBase implements ProjectManager {
public static final Logger s_logger = Logger.getLogger(ProjectManagerImpl.class);
private EmailInvite _emailInvite;
@Inject
private DomainDao _domainDao;
@Inject
private ProjectDao _projectDao;
@Inject
private ProjectJoinDao _projectJoinDao;
@Inject
AccountManager _accountMgr;
@Inject
DomainManager _domainMgr;
@Inject
ConfigurationManager _configMgr;
@Inject
ResourceLimitService _resourceLimitMgr;
@Inject
private ProjectAccountDao _projectAccountDao;
@Inject
private ProjectAccountJoinDao _projectAccountJoinDao;
@Inject
private AccountDao _accountDao;
@Inject
private ConfigurationDao _configDao;
@Inject
private ProjectInvitationDao _projectInvitationDao;
@Inject
private ProjectInvitationJoinDao _projectInvitationJoinDao;
@Inject
protected ResourceTagDao _resourceTagDao;
protected boolean _invitationRequired = false;
protected long _invitationTimeOut = 86400000;
protected boolean _allowUserToCreateProject = true;
protected ScheduledExecutorService _executor;
protected int _projectCleanupExpInvInterval = 60; //Interval defining how often project invitation cleanup thread is running
@Override
public boolean configure(final String name, final Map<String, Object> params) throws ConfigurationException {
Map<String, String> configs = _configDao.getConfiguration(params);
_invitationRequired = Boolean.valueOf(configs.get(Config.ProjectInviteRequired.key()));
String value = configs.get(Config.ProjectInvitationExpirationTime.key());
_invitationTimeOut = Long.parseLong(value != null ? value : "86400") * 1000;
_allowUserToCreateProject = Boolean.valueOf(configs.get(Config.AllowUserToCreateProject.key()));
// set up the email system for project invitations
String smtpHost = configs.get("project.smtp.host");
int smtpPort = NumbersUtil.parseInt(configs.get("project.smtp.port"), 25);
String useAuthStr = configs.get("project.smtp.useAuth");
boolean useAuth = ((useAuthStr == null) ? false : Boolean.parseBoolean(useAuthStr));
String smtpUsername = configs.get("project.smtp.username");
String smtpPassword = configs.get("project.smtp.password");
String emailSender = configs.get("project.email.sender");
String smtpDebugStr = configs.get("project.smtp.debug");
boolean smtpDebug = false;
if (smtpDebugStr != null) {
smtpDebug = Boolean.parseBoolean(smtpDebugStr);
}
_emailInvite = new EmailInvite(smtpHost, smtpPort, useAuth, smtpUsername, smtpPassword, emailSender, smtpDebug);
_executor = Executors.newScheduledThreadPool(1, new NamedThreadFactory("Project-ExpireInvitations"));
return true;
}
@Override
public boolean start() {
_executor.scheduleWithFixedDelay(new ExpiredInvitationsCleanup(), _projectCleanupExpInvInterval, _projectCleanupExpInvInterval, TimeUnit.SECONDS);
return true;
}
@Override
public boolean stop() {
return true;
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_PROJECT_CREATE, eventDescription = "creating project", create = true)
@DB
public Project createProject(final String name, final String displayText, String accountName, final Long domainId) throws ResourceAllocationException {
Account caller = CallContext.current().getCallingAccount();
Account owner = caller;
//check if the user authorized to create the project
if (_accountMgr.isNormalUser(caller.getId()) && !_allowUserToCreateProject) {
throw new PermissionDeniedException("Regular user is not permitted to create a project");
}
//Verify request parameters
if ((accountName != null && domainId == null) || (domainId != null && accountName == null)) {
throw new InvalidParameterValueException("Account name and domain id must be specified together");
}
if (accountName != null) {
owner = _accountMgr.finalizeOwner(caller, accountName, domainId, null);
}
//don't allow 2 projects with the same name inside the same domain
if (_projectDao.findByNameAndDomain(name, owner.getDomainId()) != null) {
throw new InvalidParameterValueException("Project with name " + name + " already exists in domain id=" + owner.getDomainId());
}
//do resource limit check
_resourceLimitMgr.checkResourceLimit(owner, ResourceType.project);
final Account ownerFinal = owner;
return Transaction.execute(new TransactionCallback<Project>() {
@Override
public Project doInTransaction(TransactionStatus status) {
//Create an account associated with the project
StringBuilder acctNm = new StringBuilder("PrjAcct-");
acctNm.append(name).append("-").append(ownerFinal.getDomainId());
Account projectAccount = _accountMgr.createAccount(acctNm.toString(), Account.ACCOUNT_TYPE_PROJECT, null, domainId, null, null, UUID.randomUUID().toString());
Project project = _projectDao.persist(new ProjectVO(name, displayText, ownerFinal.getDomainId(), projectAccount.getId()));
//assign owner to the project
assignAccountToProject(project, ownerFinal.getId(), ProjectAccount.Role.Admin);
if (project != null) {
CallContext.current().setEventDetails("Project id=" + project.getId());
CallContext.current().putContextParameter(Project.class, project.getUuid());
}
//Increment resource count
_resourceLimitMgr.incrementResourceCount(ownerFinal.getId(), ResourceType.project);
return project;
}
});
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_PROJECT_CREATE, eventDescription = "creating project", async = true)
@DB
public Project enableProject(long projectId) {
Account caller = CallContext.current().getCallingAccount();
ProjectVO project = getProject(projectId);
//verify input parameters
if (project == null) {
throw new InvalidParameterValueException("Unable to find project by id " + projectId);
}
_accountMgr.checkAccess(caller, AccessType.ModifyProject, true, _accountMgr.getAccount(project.getProjectAccountId()));
//at this point enabling project doesn't require anything, so just update the state
project.setState(State.Active);
_projectDao.update(projectId, project);
return project;
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_PROJECT_DELETE, eventDescription = "deleting project", async = true)
public boolean deleteProject(long projectId) {
CallContext ctx = CallContext.current();
ProjectVO project = getProject(projectId);
//verify input parameters
if (project == null) {
throw new InvalidParameterValueException("Unable to find project by id " + projectId);
}
_accountMgr.checkAccess(ctx.getCallingAccount(), AccessType.ModifyProject, true, _accountMgr.getAccount(project.getProjectAccountId()));
return deleteProject(ctx.getCallingAccount(), ctx.getCallingUserId(), project);
}
@DB
@Override
public boolean deleteProject(Account caller, long callerUserId, final ProjectVO project) {
//mark project as inactive first, so you can't add resources to it
boolean updateResult = Transaction.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus status) {
s_logger.debug("Marking project id=" + project.getId() + " with state " + State.Disabled + " as a part of project delete...");
project.setState(State.Disabled);
boolean updateResult = _projectDao.update(project.getId(), project);
//owner can be already removed at this point, so adding the conditional check
Account projectOwner = getProjectOwner(project.getId());
if (projectOwner != null) {
_resourceLimitMgr.decrementResourceCount(projectOwner.getId(), ResourceType.project);
}
return updateResult;
}
});
if (updateResult) {
//pass system caller when clenaup projects account
if (!cleanupProject(project, _accountDao.findById(Account.ACCOUNT_ID_SYSTEM), User.UID_SYSTEM)) {
s_logger.warn("Failed to cleanup project's id=" + project.getId() + " resources, not removing the project yet");
return false;
} else {
return _projectDao.remove(project.getId());
}
} else {
s_logger.warn("Failed to mark the project id=" + project.getId() + " with state " + State.Disabled);
return false;
}
}
@DB
private boolean cleanupProject(final Project project, AccountVO caller, Long callerUserId) {
boolean result = true;
//Delete project's account
AccountVO account = _accountDao.findById(project.getProjectAccountId());
s_logger.debug("Deleting projects " + project + " internal account id=" + account.getId() + " as a part of project cleanup...");
result = result && _accountMgr.deleteAccount(account, callerUserId, caller);
if (result) {
//Unassign all users from the project
result = Transaction.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus status) {
boolean result = true;
s_logger.debug("Unassigning all accounts from project " + project + " as a part of project cleanup...");
List<? extends ProjectAccount> projectAccounts = _projectAccountDao.listByProjectId(project.getId());
for (ProjectAccount projectAccount : projectAccounts) {
result = result && unassignAccountFromProject(projectAccount.getProjectId(), projectAccount.getAccountId());
}
s_logger.debug("Removing all invitations for the project " + project + " as a part of project cleanup...");
_projectInvitationDao.cleanupInvitations(project.getId());
return result;
}
});
if (result) {
s_logger.debug("Accounts are unassign successfully from project " + project + " as a part of project cleanup...");
}
} else {
s_logger.warn("Failed to cleanup project's internal account");
}
return result;
}
@Override
public boolean unassignAccountFromProject(long projectId, long accountId) {
ProjectAccountVO projectAccount = _projectAccountDao.findByProjectIdAccountId(projectId, accountId);
if (projectAccount == null) {
s_logger.debug("Account id=" + accountId + " is not assigned to project id=" + projectId + " so no need to unassign");
return true;
}
if (_projectAccountDao.remove(projectAccount.getId())) {
return true;
} else {
s_logger.warn("Failed to unassign account id=" + accountId + " from the project id=" + projectId);
return false;
}
}
@Override
public ProjectVO getProject(long projectId) {
return _projectDao.findById(projectId);
}
@Override
public long getInvitationTimeout() {
return _invitationTimeOut;
}
@Override
public ProjectAccount assignAccountToProject(Project project, long accountId, ProjectAccount.Role accountRole) {
return _projectAccountDao.persist(new ProjectAccountVO(project, accountId, accountRole));
}
@Override
@DB
public boolean deleteAccountFromProject(final long projectId, final long accountId) {
return Transaction.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus status) {
boolean success = true;
//remove account
ProjectAccountVO projectAccount = _projectAccountDao.findByProjectIdAccountId(projectId, accountId);
success = _projectAccountDao.remove(projectAccount.getId());
//remove all invitations for account
if (success) {
s_logger.debug("Removed account " + accountId + " from project " + projectId + " , cleaning up old invitations for account/project...");
ProjectInvitation invite = _projectInvitationDao.findByAccountIdProjectId(accountId, projectId);
if (invite != null) {
success = success && _projectInvitationDao.remove(invite.getId());
}
}
return success;
}
});
}
@Override
public Account getProjectOwner(long projectId) {
ProjectAccount prAcct = _projectAccountDao.getProjectOwner(projectId);
if (prAcct != null) {
return _accountMgr.getAccount(prAcct.getAccountId());
}
return null;
}
@Override
public ProjectVO findByProjectAccountId(long projectAccountId) {
return _projectDao.findByProjectAccountId(projectAccountId);
}
@Override
public ProjectVO findByProjectAccountIdIncludingRemoved(long projectAccountId) {
return _projectDao.findByProjectAccountIdIncludingRemoved(projectAccountId);
}
@Override
public Project findByNameAndDomainId(String name, long domainId) {
return _projectDao.findByNameAndDomain(name, domainId);
}
@Override
public boolean canAccessProjectAccount(Account caller, long accountId) {
//ROOT admin always can access the project
if (_accountMgr.isRootAdmin(caller.getId())) {
return true;
} else if (_accountMgr.isDomainAdmin(caller.getId())) {
Account owner = _accountMgr.getAccount(accountId);
_accountMgr.checkAccess(caller, _domainDao.findById(owner.getDomainId()));
return true;
}
return _projectAccountDao.canAccessProjectAccount(caller.getId(), accountId);
}
@Override
public boolean canModifyProjectAccount(Account caller, long accountId) {
//ROOT admin always can access the project
if (_accountMgr.isRootAdmin(caller.getId())) {
return true;
} else if (_accountMgr.isDomainAdmin(caller.getId())) {
Account owner = _accountMgr.getAccount(accountId);
_accountMgr.checkAccess(caller, _domainDao.findById(owner.getDomainId()));
return true;
}
return _projectAccountDao.canModifyProjectAccount(caller.getId(), accountId);
}
@Override
@DB
@ActionEvent(eventType = EventTypes.EVENT_PROJECT_UPDATE, eventDescription = "updating project", async = true)
public Project updateProject(final long projectId, final String displayText, final String newOwnerName) throws ResourceAllocationException {
Account caller = CallContext.current().getCallingAccount();
//check that the project exists
final ProjectVO project = getProject(projectId);
if (project == null) {
throw new InvalidParameterValueException("Unable to find the project id=" + projectId);
}
//verify permissions
_accountMgr.checkAccess(caller, AccessType.ModifyProject, true, _accountMgr.getAccount(project.getProjectAccountId()));
Transaction.execute(new TransactionCallbackWithExceptionNoReturn<ResourceAllocationException>() {
@Override
public void doInTransactionWithoutResult(TransactionStatus status) throws ResourceAllocationException {
if (displayText != null) {
project.setDisplayText(displayText);
_projectDao.update(projectId, project);
}
if (newOwnerName != null) {
//check that the new owner exists
Account futureOwnerAccount = _accountMgr.getActiveAccountByName(newOwnerName, project.getDomainId());
if (futureOwnerAccount == null) {
throw new InvalidParameterValueException("Unable to find account name=" + newOwnerName + " in domain id=" + project.getDomainId());
}
Account currentOwnerAccount = getProjectOwner(projectId);
if (currentOwnerAccount == null) {
s_logger.error("Unable to find the current owner for the project id=" + projectId);
throw new InvalidParameterValueException("Unable to find the current owner for the project id=" + projectId);
}
if (currentOwnerAccount.getId() != futureOwnerAccount.getId()) {
ProjectAccountVO futureOwner = _projectAccountDao.findByProjectIdAccountId(projectId, futureOwnerAccount.getAccountId());
if (futureOwner == null) {
throw new InvalidParameterValueException("Account " + newOwnerName +
" doesn't belong to the project. Add it to the project first and then change the project's ownership");
}
//do resource limit check
_resourceLimitMgr.checkResourceLimit(_accountMgr.getAccount(futureOwnerAccount.getId()), ResourceType.project);
//unset the role for the old owner
ProjectAccountVO currentOwner = _projectAccountDao.findByProjectIdAccountId(projectId, currentOwnerAccount.getId());
currentOwner.setAccountRole(Role.Regular);
_projectAccountDao.update(currentOwner.getId(), currentOwner);
_resourceLimitMgr.decrementResourceCount(currentOwnerAccount.getId(), ResourceType.project);
//set new owner
futureOwner.setAccountRole(Role.Admin);
_projectAccountDao.update(futureOwner.getId(), futureOwner);
_resourceLimitMgr.incrementResourceCount(futureOwnerAccount.getId(), ResourceType.project);
} else {
s_logger.trace("Future owner " + newOwnerName + "is already the owner of the project id=" + projectId);
}
}
}
});
return _projectDao.findById(projectId);
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_PROJECT_ACCOUNT_ADD, eventDescription = "adding account to project", async = true)
public boolean addAccountToProject(long projectId, String accountName, String email) {
Account caller = CallContext.current().getCallingAccount();
//check that the project exists
Project project = getProject(projectId);
if (project == null) {
InvalidParameterValueException ex = new InvalidParameterValueException("Unable to find project with specified id");
ex.addProxyObject(String.valueOf(projectId), "projectId");
throw ex;
}
//User can be added to Active project only
if (project.getState() != Project.State.Active) {
InvalidParameterValueException ex =
new InvalidParameterValueException("Can't add account to the specified project id in state=" + project.getState() + " as it's no longer active");
ex.addProxyObject(project.getUuid(), "projectId");
throw ex;
}
//check that account-to-add exists
Account account = null;
if (accountName != null) {
account = _accountMgr.getActiveAccountByName(accountName, project.getDomainId());
if (account == null) {
InvalidParameterValueException ex = new InvalidParameterValueException("Unable to find account name=" + accountName + " in specified domain id");
DomainVO domain = ApiDBUtils.findDomainById(project.getDomainId());
String domainUuid = String.valueOf(project.getDomainId());
if (domain != null) {
domainUuid = domain.getUuid();
}
ex.addProxyObject(domainUuid, "domainId");
throw ex;
}
//verify permissions - only project owner can assign
_accountMgr.checkAccess(caller, AccessType.ModifyProject, true, _accountMgr.getAccount(project.getProjectAccountId()));
//Check if the account already added to the project
ProjectAccount projectAccount = _projectAccountDao.findByProjectIdAccountId(projectId, account.getId());
if (projectAccount != null) {
s_logger.debug("Account " + accountName + " already added to the project id=" + projectId);
return true;
}
}
if (_invitationRequired) {
return inviteAccountToProject(project, account, email);
} else {
if (account == null) {
throw new InvalidParameterValueException("Account information is required for assigning account to the project");
}
if (assignAccountToProject(project, account.getId(), ProjectAccount.Role.Regular) != null) {
return true;
} else {
s_logger.warn("Failed to add account " + accountName + " to project id=" + projectId);
return false;
}
}
}
private boolean inviteAccountToProject(Project project, Account account, String email) {
if (account != null) {
if (createAccountInvitation(project, account.getId()) != null) {
return true;
} else {
s_logger.warn("Failed to generate invitation for account " + account.getAccountName() + " to project id=" + project);
return false;
}
}
if (email != null) {
//generate the token
String token = generateToken(10);
if (generateTokenBasedInvitation(project, email, token) != null) {
return true;
} else {
s_logger.warn("Failed to generate invitation for email " + email + " to project id=" + project);
return false;
}
}
return false;
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_PROJECT_ACCOUNT_REMOVE, eventDescription = "removing account from project", async = true)
public boolean deleteAccountFromProject(long projectId, String accountName) {
Account caller = CallContext.current().getCallingAccount();
//check that the project exists
Project project = getProject(projectId);
if (project == null) {
InvalidParameterValueException ex = new InvalidParameterValueException("Unable to find project with specified id");
ex.addProxyObject(String.valueOf(projectId), "projectId");
throw ex;
}
//check that account-to-remove exists
Account account = _accountMgr.getActiveAccountByName(accountName, project.getDomainId());
if (account == null) {
InvalidParameterValueException ex =
new InvalidParameterValueException("Unable to find account name=" + accountName + " in domain id=" + project.getDomainId());
DomainVO domain = ApiDBUtils.findDomainById(project.getDomainId());
String domainUuid = String.valueOf(project.getDomainId());
if (domain != null) {
domainUuid = domain.getUuid();
}
ex.addProxyObject(domainUuid, "domainId");
throw ex;
}
//verify permissions
_accountMgr.checkAccess(caller, AccessType.ModifyProject, true, _accountMgr.getAccount(project.getProjectAccountId()));
//Check if the account exists in the project
ProjectAccount projectAccount = _projectAccountDao.findByProjectIdAccountId(projectId, account.getId());
if (projectAccount == null) {
InvalidParameterValueException ex = new InvalidParameterValueException("Account " + accountName + " is not assigned to the project with specified id");
// Use the projectVO object and not the projectAccount object to inject the projectId.
ex.addProxyObject(project.getUuid(), "projectId");
throw ex;
}
//can't remove the owner of the project
if (projectAccount.getAccountRole() == Role.Admin) {
InvalidParameterValueException ex =
new InvalidParameterValueException("Unable to delete account " + accountName +
" from the project with specified id as the account is the owner of the project");
ex.addProxyObject(project.getUuid(), "projectId");
throw ex;
}
return deleteAccountFromProject(projectId, account.getId());
}
public ProjectInvitation createAccountInvitation(Project project, Long accountId) {
if (activeInviteExists(project, accountId, null)) {
throw new InvalidParameterValueException("There is already a pending invitation for account id=" + accountId + " to the project id=" + project);
}
ProjectInvitation invitation = _projectInvitationDao.persist(new ProjectInvitationVO(project.getId(), accountId, project.getDomainId(), null, null));
return invitation;
}
@DB
public boolean activeInviteExists(final Project project, final Long accountId, final String email) {
return Transaction.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus status) {
//verify if the invitation was already generated
ProjectInvitationVO invite = null;
if (accountId != null) {
invite = _projectInvitationDao.findByAccountIdProjectId(accountId, project.getId());
} else if (email != null) {
invite = _projectInvitationDao.findByEmailAndProjectId(email, project.getId());
}
if (invite != null) {
if (invite.getState() == ProjectInvitation.State.Completed ||
(invite.getState() == ProjectInvitation.State.Pending && _projectInvitationDao.isActive(invite.getId(), _invitationTimeOut))) {
return true;
} else {
if (invite.getState() == ProjectInvitation.State.Pending) {
expireInvitation(invite);
}
//remove the expired/declined invitation
if (accountId != null) {
s_logger.debug("Removing invitation in state " + invite.getState() + " for account id=" + accountId + " to project " + project);
} else if (email != null) {
s_logger.debug("Removing invitation in state " + invite.getState() + " for email " + email + " to project " + project);
}
_projectInvitationDao.expunge(invite.getId());
}
}
return false;
}
});
}
public ProjectInvitation generateTokenBasedInvitation(Project project, String email, String token) {
//verify if the invitation was already generated
if (activeInviteExists(project, null, email)) {
throw new InvalidParameterValueException("There is already a pending invitation for email " + email + " to the project id=" + project);
}
ProjectInvitation projectInvitation = _projectInvitationDao.persist(new ProjectInvitationVO(project.getId(), null, project.getDomainId(), email, token));
try {
_emailInvite.sendInvite(token, email, project.getId());
} catch (Exception ex) {
s_logger.warn("Failed to send project id=" + project + " invitation to the email " + email + "; removing the invitation record from the db", ex);
_projectInvitationDao.remove(projectInvitation.getId());
return null;
}
return projectInvitation;
}
private boolean expireInvitation(ProjectInvitationVO invite) {
s_logger.debug("Expiring invitation id=" + invite.getId());
invite.setState(ProjectInvitation.State.Expired);
return _projectInvitationDao.update(invite.getId(), invite);
}
@Override
@DB
@ActionEvent(eventType = EventTypes.EVENT_PROJECT_INVITATION_UPDATE, eventDescription = "updating project invitation", async = true)
public boolean updateInvitation(final long projectId, String accountName, String token, final boolean accept) {
Account caller = CallContext.current().getCallingAccount();
Long accountId = null;
boolean result = true;
//if accountname and token are null, default accountname to caller's account name
if (accountName == null && token == null) {
accountName = caller.getAccountName();
}
//check that the project exists
final Project project = getProject(projectId);
if (project == null) {
throw new InvalidParameterValueException("Unable to find the project id=" + projectId);
}
if (accountName != null) {
//check that account-to-remove exists
Account account = _accountMgr.getActiveAccountByName(accountName, project.getDomainId());
if (account == null) {
throw new InvalidParameterValueException("Unable to find account name=" + accountName + " in domain id=" + project.getDomainId());
}
//verify permissions
_accountMgr.checkAccess(caller, null, true, account);
accountId = account.getId();
} else {
accountId = caller.getId();
}
//check that invitation exists
ProjectInvitationVO invite = null;
if (token == null) {
invite = _projectInvitationDao.findByAccountIdProjectId(accountId, projectId, ProjectInvitation.State.Pending);
} else {
invite = _projectInvitationDao.findPendingByTokenAndProjectId(token, projectId, ProjectInvitation.State.Pending);
}
if (invite != null) {
if (!_projectInvitationDao.isActive(invite.getId(), _invitationTimeOut) && accept) {
expireInvitation(invite);
throw new InvalidParameterValueException("Invitation is expired for account id=" + accountName + " to the project id=" + projectId);
} else {
final ProjectInvitationVO inviteFinal = invite;
final Long accountIdFinal = accountId;
final String accountNameFinal = accountName;
result = Transaction.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus status) {
boolean result = true;
ProjectInvitation.State newState = accept ? ProjectInvitation.State.Completed : ProjectInvitation.State.Declined;
//update invitation
s_logger.debug("Marking invitation " + inviteFinal + " with state " + newState);
inviteFinal.setState(newState);
result = _projectInvitationDao.update(inviteFinal.getId(), inviteFinal);
if (result && accept) {
//check if account already exists for the project (was added before invitation got accepted)
ProjectAccount projectAccount = _projectAccountDao.findByProjectIdAccountId(projectId, accountIdFinal);
if (projectAccount != null) {
s_logger.debug("Account " + accountNameFinal + " already added to the project id=" + projectId);
} else {
assignAccountToProject(project, accountIdFinal, ProjectAccount.Role.Regular);
}
} else {
s_logger.warn("Failed to update project invitation " + inviteFinal + " with state " + newState);
}
return result;
}
});
}
} else {
throw new InvalidParameterValueException("Unable to find invitation for account name=" + accountName + " to the project id=" + projectId);
}
return result;
}
@Override
public List<Long> listPermittedProjectAccounts(long accountId) {
return _projectAccountDao.listPermittedAccountIds(accountId);
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_PROJECT_ACTIVATE, eventDescription = "activating project")
@DB
public Project activateProject(final long projectId) {
Account caller = CallContext.current().getCallingAccount();
//check that the project exists
final ProjectVO project = getProject(projectId);
if (project == null) {
InvalidParameterValueException ex = new InvalidParameterValueException("Unable to find project with specified id");
ex.addProxyObject(String.valueOf(projectId), "projectId");
throw ex;
}
//verify permissions
_accountMgr.checkAccess(caller, AccessType.ModifyProject, true, _accountMgr.getAccount(project.getProjectAccountId()));
//allow project activation only when it's in Suspended state
Project.State currentState = project.getState();
if (currentState == State.Active) {
s_logger.debug("The project id=" + projectId + " is already active, no need to activate it again");
return project;
}
if (currentState != State.Suspended) {
throw new InvalidParameterValueException("Can't activate the project in " + currentState + " state");
}
Transaction.execute(new TransactionCallbackNoReturn() {
@Override
public void doInTransactionWithoutResult(TransactionStatus status) {
project.setState(Project.State.Active);
_projectDao.update(projectId, project);
_accountMgr.enableAccount(project.getProjectAccountId());
}
});
return _projectDao.findById(projectId);
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_PROJECT_SUSPEND, eventDescription = "suspending project", async = true)
public Project suspendProject(long projectId) throws ConcurrentOperationException, ResourceUnavailableException {
Account caller = CallContext.current().getCallingAccount();
ProjectVO project = getProject(projectId);
//verify input parameters
if (project == null) {
InvalidParameterValueException ex = new InvalidParameterValueException("Unable to find project with specified id");
ex.addProxyObject(String.valueOf(projectId), "projectId");
throw ex;
}
_accountMgr.checkAccess(caller, AccessType.ModifyProject, true, _accountMgr.getAccount(project.getProjectAccountId()));
if (suspendProject(project)) {
s_logger.debug("Successfully suspended project id=" + projectId);
return _projectDao.findById(projectId);
} else {
CloudRuntimeException ex = new CloudRuntimeException("Failed to suspend project with specified id");
ex.addProxyObject(project.getUuid(), "projectId");
throw ex;
}
}
private boolean suspendProject(ProjectVO project) throws ConcurrentOperationException, ResourceUnavailableException {
s_logger.debug("Marking project " + project + " with state " + State.Suspended + " as a part of project suspend...");
project.setState(State.Suspended);
boolean updateResult = _projectDao.update(project.getId(), project);
if (updateResult) {
long projectAccountId = project.getProjectAccountId();
if (!_accountMgr.disableAccount(projectAccountId)) {
s_logger.warn("Failed to suspend all project's " + project + " resources; the resources will be suspended later by background thread");
}
} else {
throw new CloudRuntimeException("Failed to mark the project " + project + " with state " + State.Suspended);
}
return true;
}
public static String generateToken(int length) {
String charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
Random rand = new Random(System.currentTimeMillis());
StringBuffer sb = new StringBuffer();
for (int i = 0; i < length; i++) {
int pos = rand.nextInt(charset.length());
sb.append(charset.charAt(pos));
}
return sb.toString();
}
class EmailInvite {
private Session _smtpSession;
private final String _smtpHost;
private int _smtpPort = -1;
private boolean _smtpUseAuth = false;
private final String _smtpUsername;
private final String _smtpPassword;
private final String _emailSender;
public EmailInvite(String smtpHost, int smtpPort, boolean smtpUseAuth, final String smtpUsername, final String smtpPassword, String emailSender, boolean smtpDebug) {
_smtpHost = smtpHost;
_smtpPort = smtpPort;
_smtpUseAuth = smtpUseAuth;
_smtpUsername = smtpUsername;
_smtpPassword = smtpPassword;
_emailSender = emailSender;
if (_smtpHost != null) {
Properties smtpProps = new Properties();
smtpProps.put("mail.smtp.host", smtpHost);
smtpProps.put("mail.smtp.port", smtpPort);
smtpProps.put("mail.smtp.auth", "" + smtpUseAuth);
if (smtpUsername != null) {
smtpProps.put("mail.smtp.user", smtpUsername);
}
smtpProps.put("mail.smtps.host", smtpHost);
smtpProps.put("mail.smtps.port", smtpPort);
smtpProps.put("mail.smtps.auth", "" + smtpUseAuth);
if (smtpUsername != null) {
smtpProps.put("mail.smtps.user", smtpUsername);
}
if ((smtpUsername != null) && (smtpPassword != null)) {
_smtpSession = Session.getInstance(smtpProps, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(smtpUsername, smtpPassword);
}
});
} else {
_smtpSession = Session.getInstance(smtpProps);
}
_smtpSession.setDebug(smtpDebug);
} else {
_smtpSession = null;
}
}
public void sendInvite(String token, String email, long projectId) throws MessagingException, UnsupportedEncodingException {
if (_smtpSession != null) {
InternetAddress address = null;
if (email != null) {
try {
address = new InternetAddress(email, email);
} catch (Exception ex) {
s_logger.error("Exception creating address for: " + email, ex);
}
}
String content = "You've been invited to join the CloudStack project id=" + projectId + ". Please use token " + token + " to complete registration";
SMTPMessage msg = new SMTPMessage(_smtpSession);
msg.setSender(new InternetAddress(_emailSender, _emailSender));
msg.setFrom(new InternetAddress(_emailSender, _emailSender));
msg.addRecipient(RecipientType.TO, address);
msg.setSubject("You are invited to join the cloud stack project id=" + projectId);
msg.setSentDate(new Date(DateUtil.currentGMTTime().getTime() >> 10));
msg.setContent(content, "text/plain");
msg.saveChanges();
SMTPTransport smtpTrans = null;
if (_smtpUseAuth) {
smtpTrans = new SMTPSSLTransport(_smtpSession, new URLName("smtp", _smtpHost, _smtpPort, null, _smtpUsername, _smtpPassword));
} else {
smtpTrans = new SMTPTransport(_smtpSession, new URLName("smtp", _smtpHost, _smtpPort, null, _smtpUsername, _smtpPassword));
}
smtpTrans.connect();
smtpTrans.sendMessage(msg, msg.getAllRecipients());
smtpTrans.close();
} else {
throw new CloudRuntimeException("Unable to send email invitation; smtp ses");
}
}
}
@Override
@DB
@ActionEvent(eventType = EventTypes.EVENT_PROJECT_INVITATION_REMOVE, eventDescription = "removing project invitation", async = true)
public boolean deleteProjectInvitation(long id) {
Account caller = CallContext.current().getCallingAccount();
ProjectInvitation invitation = _projectInvitationDao.findById(id);
if (invitation == null) {
throw new InvalidParameterValueException("Unable to find project invitation by id " + id);
}
//check that the project exists
Project project = getProject(invitation.getProjectId());
//check permissions - only project owner can remove the invitations
_accountMgr.checkAccess(caller, AccessType.ModifyProject, true, _accountMgr.getAccount(project.getProjectAccountId()));
if (_projectInvitationDao.remove(id)) {
s_logger.debug("Project Invitation id=" + id + " is removed");
return true;
} else {
s_logger.debug("Failed to remove project invitation id=" + id);
return false;
}
}
public class ExpiredInvitationsCleanup extends ManagedContextRunnable {
@Override
protected void runInContext() {
try {
TimeZone.getDefault();
List<ProjectInvitationVO> invitationsToExpire = _projectInvitationDao.listInvitationsToExpire(_invitationTimeOut);
if (!invitationsToExpire.isEmpty()) {
s_logger.debug("Found " + invitationsToExpire.size() + " projects to expire");
for (ProjectInvitationVO invitationToExpire : invitationsToExpire) {
invitationToExpire.setState(ProjectInvitation.State.Expired);
_projectInvitationDao.update(invitationToExpire.getId(), invitationToExpire);
s_logger.trace("Expired project invitation id=" + invitationToExpire.getId());
}
}
} catch (Exception ex) {
s_logger.warn("Exception while running expired invitations cleanup", ex);
}
}
}
@Override
public boolean projectInviteRequired() {
return _invitationRequired;
}
@Override
public boolean allowUserToCreateProject() {
return _allowUserToCreateProject;
}
}