This is an automated email from the ASF dual-hosted git repository.
paul_a pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/master by this push:
new 5d81574 Allow users to share templates with Accounts or Projects
through the UI
5d81574 is described below
commit 5d8157422dbbcacc66543185544c00eb2bb49c3a
Author: Rohit Yadav <[email protected]>
AuthorDate: Thu Jul 18 22:12:55 2019 +0530
Allow users to share templates with Accounts or Projects through the UI
* Allow users to share templates with Accounts or Projects through the
updateTemplate permissions API
* Change behaviour to show only supported projects and accounts with update
template permissions
* Allow admins to see accounts dropdown and only hide lists for users
* Don't allow sharing project owned templates as you cannot retrieve them
in list api calls
---
.../api/BaseUpdateTemplateOrIsoPermissionsCmd.java | 3 +-
.../command/user/config/ListCapabilitiesCmd.java | 1 +
.../api/response/CapabilitiesResponse.java | 8 +
.../org/apache/cloudstack/query/QueryService.java | 4 +
.../main/java/com/cloud/api/ApiResponseHelper.java | 5 +
.../java/com/cloud/api/query/QueryManagerImpl.java | 13 +-
.../com/cloud/api/query/dao/UserVmJoinDaoImpl.java | 6 +-
.../com/cloud/server/ManagementServerImpl.java | 7 +-
.../com/cloud/template/TemplateManagerImpl.java | 74 ++---
ui/css/cloudstack3.css | 8 +
ui/l10n/en.js | 5 +
ui/scripts/cloudStack.js | 2 +
ui/scripts/docs.js | 22 +-
ui/scripts/sharedFunctions.js | 1 +
ui/scripts/templates.js | 319 ++++++++++++++++++++-
15 files changed, 424 insertions(+), 54 deletions(-)
diff --git
a/api/src/main/java/org/apache/cloudstack/api/BaseUpdateTemplateOrIsoPermissionsCmd.java
b/api/src/main/java/org/apache/cloudstack/api/BaseUpdateTemplateOrIsoPermissionsCmd.java
index 77e5a15..410ffef 100644
---
a/api/src/main/java/org/apache/cloudstack/api/BaseUpdateTemplateOrIsoPermissionsCmd.java
+++
b/api/src/main/java/org/apache/cloudstack/api/BaseUpdateTemplateOrIsoPermissionsCmd.java
@@ -45,7 +45,7 @@ public abstract class BaseUpdateTemplateOrIsoPermissionsCmd
extends BaseCmd {
@Parameter(name = ApiConstants.ACCOUNTS,
type = CommandType.LIST,
collectionType = CommandType.STRING,
- description = "a comma delimited list of accounts. If
specified, \"op\" parameter has to be passed in.")
+ description = "a comma delimited list of accounts within
caller's domain. If specified, \"op\" parameter has to be passed in.")
private List<String> accountNames;
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType =
TemplateResponse.class, required = true, description = "the template ID")
@@ -80,7 +80,6 @@ public abstract class BaseUpdateTemplateOrIsoPermissionsCmd
extends BaseCmd {
if (accountNames != null && projectIds != null) {
throw new InvalidParameterValueException("Accounts and projectIds
can't be specified together");
}
-
return accountNames;
}
diff --git
a/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java
b/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java
index 9c52656..40d1a71 100644
---
a/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java
+++
b/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java
@@ -59,6 +59,7 @@ public class ListCapabilitiesCmd extends BaseCmd {
response.setKVMSnapshotEnabled((Boolean)capabilities.get("KVMSnapshotEnabled"));
response.setAllowUserViewDestroyedVM((Boolean)capabilities.get("allowUserViewDestroyedVM"));
response.setAllowUserExpungeRecoverVM((Boolean)capabilities.get("allowUserExpungeRecoverVM"));
+
response.setAllowUserViewAllDomainAccounts((Boolean)capabilities.get("allowUserViewAllDomainAccounts"));
if (capabilities.containsKey("apiLimitInterval")) {
response.setApiLimitInterval((Integer)capabilities.get("apiLimitInterval"));
}
diff --git
a/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java
b/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java
index bcdad46..153d7df 100644
---
a/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java
+++
b/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java
@@ -84,6 +84,10 @@ public class CapabilitiesResponse extends BaseResponse {
@Param(description = "true if the user can recover and expunge
virtualmachines, false otherwise", since = "4.6.0")
private boolean allowUserExpungeRecoverVM;
+ @SerializedName("allowuserviewalldomainaccounts")
+ @Param(description = "true if users can see all accounts within the same
domain, false otherwise")
+ private boolean allowUserViewAllDomainAccounts;
+
public void setSecurityGroupsEnabled(boolean securityGroupsEnabled) {
this.securityGroupsEnabled = securityGroupsEnabled;
}
@@ -143,4 +147,8 @@ public class CapabilitiesResponse extends BaseResponse {
public void setAllowUserExpungeRecoverVM(boolean
allowUserExpungeRecoverVM) {
this.allowUserExpungeRecoverVM = allowUserExpungeRecoverVM;
}
+
+ public void setAllowUserViewAllDomainAccounts(boolean
allowUserViewAllDomainAccounts) {
+ this.allowUserViewAllDomainAccounts = allowUserViewAllDomainAccounts;
+ }
}
\ No newline at end of file
diff --git a/api/src/main/java/org/apache/cloudstack/query/QueryService.java
b/api/src/main/java/org/apache/cloudstack/query/QueryService.java
index 618a8f6..b9010cb 100644
--- a/api/src/main/java/org/apache/cloudstack/query/QueryService.java
+++ b/api/src/main/java/org/apache/cloudstack/query/QueryService.java
@@ -103,6 +103,10 @@ public interface QueryService {
"network offering, zones), we use the flag to determine if
the entities should be sorted ascending (when flag is true) " +
"or descending (when flag is false). Within the scope of
the config all users see the same result.", true, ConfigKey.Scope.Global);
+ public static final ConfigKey<Boolean> AllowUserViewAllDomainAccounts =
new ConfigKey<>("Advanced", Boolean.class,
+ "allow.user.view.all.domain.accounts", "false",
+ "Determines whether users can view all user accounts within the
same domain", true, ConfigKey.Scope.Domain);
+
ListResponse<UserResponse> searchForUsers(ListUsersCmd cmd) throws
PermissionDeniedException;
ListResponse<EventResponse> searchForEvents(ListEventsCmd cmd);
diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java
b/server/src/main/java/com/cloud/api/ApiResponseHelper.java
index 524e109..20bfb96 100644
--- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java
+++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java
@@ -1808,6 +1808,11 @@ public class ApiResponseHelper implements
ResponseGenerator {
List<String> regularAccounts = new ArrayList<String>();
for (String accountName : accountNames) {
Account account = ApiDBUtils.findAccountByNameDomain(accountName,
templateOwner.getDomainId());
+ if (account == null) {
+ s_logger.error("Missing Account " + accountName + " in domain
" + templateOwner.getDomainId());
+ continue;
+ }
+
if (account.getType() != Account.ACCOUNT_TYPE_PROJECT) {
regularAccounts.add(accountName);
} else {
diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
index 92a110b..ee56cbb 100644
--- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
+++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
@@ -394,6 +394,7 @@ public class QueryManagerImpl extends
MutualExclusiveIdsManagerBase implements Q
* com.cloud.api.query.QueryService#searchForUsers(org.apache.cloudstack
* .api.command.admin.user.ListUsersCmd)
*/
+
@Override
public ListResponse<UserResponse> searchForUsers(ListUsersCmd cmd) throws
PermissionDeniedException {
Pair<List<UserAccountJoinVO>, Integer> result =
searchForUsersInternal(cmd);
@@ -1980,7 +1981,8 @@ public class QueryManagerImpl extends
MutualExclusiveIdsManagerBase implements Q
// if no "id" specified...
if (accountId == null) {
// listall only has significance if they are an admin
- if (listAll && callerIsAdmin) {
+ boolean isDomainListAllAllowed =
AllowUserViewAllDomainAccounts.valueIn(caller.getDomainId());
+ if ((listAll && callerIsAdmin) || isDomainListAllAllowed) {
// if no domain id specified, use caller's domain
if (domainId == null) {
domainId = caller.getDomainId();
@@ -2026,6 +2028,7 @@ public class QueryManagerImpl extends
MutualExclusiveIdsManagerBase implements Q
sb.and("needsCleanup", sb.entity().isNeedsCleanup(),
SearchCriteria.Op.EQ);
sb.and("typeNEQ", sb.entity().getType(), SearchCriteria.Op.NEQ);
sb.and("idNEQ", sb.entity().getId(), SearchCriteria.Op.NEQ);
+ sb.and("type2NEQ", sb.entity().getType(), SearchCriteria.Op.NEQ);
if (domainId != null && isRecursive) {
sb.and("path", sb.entity().getDomainPath(),
SearchCriteria.Op.LIKE);
@@ -2035,9 +2038,15 @@ public class QueryManagerImpl extends
MutualExclusiveIdsManagerBase implements Q
// don't return account of type project to the end user
sc.setParameters("typeNEQ", Account.ACCOUNT_TYPE_PROJECT);
+
// don't return system account...
sc.setParameters("idNEQ", Account.ACCOUNT_ID_SYSTEM);
+ // do not return account of type domain admin to the end user
+ if (!callerIsAdmin) {
+ sc.setParameters("type2NEQ", Account.ACCOUNT_TYPE_DOMAIN_ADMIN);
+ }
+
if (keyword != null) {
SearchCriteria<AccountJoinVO> ssc =
_accountJoinDao.createSearchCriteria();
ssc.addOr("accountName", SearchCriteria.Op.LIKE, "%" + keyword +
"%");
@@ -3836,6 +3845,6 @@ public class QueryManagerImpl extends
MutualExclusiveIdsManagerBase implements Q
@Override
public ConfigKey<?>[] getConfigKeys() {
- return new ConfigKey<?>[] {AllowUserViewDestroyedVM,
UserVMBlacklistedDetails, UserVMReadOnlyUIDetails, SortKeyAscending};
+ return new ConfigKey<?>[] {AllowUserViewDestroyedVM,
UserVMBlacklistedDetails, UserVMReadOnlyUIDetails, SortKeyAscending,
AllowUserViewAllDomainAccounts};
}
}
diff --git
a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java
b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java
index 58b167f..0e73743 100644
--- a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java
+++ b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java
@@ -37,12 +37,12 @@ import
org.apache.cloudstack.api.response.NicSecondaryIpResponse;
import org.apache.cloudstack.api.response.SecurityGroupResponse;
import org.apache.cloudstack.api.response.UserVmResponse;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
+import org.apache.cloudstack.query.QueryService;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Component;
import com.cloud.api.ApiDBUtils;
import com.cloud.api.ApiResponseHelper;
-import com.cloud.api.query.QueryManagerImpl;
import com.cloud.api.query.vo.UserVmJoinVO;
import com.cloud.gpu.GPU;
import com.cloud.service.ServiceOfferingDetailsVO;
@@ -315,14 +315,14 @@ public class UserVmJoinDaoImpl extends
GenericDaoBaseWithTagInformation<UserVmJo
}
// Remove blacklisted settings if user is not admin
if (caller.getType() != Account.ACCOUNT_TYPE_ADMIN) {
- String[] userVmSettingsToHide =
QueryManagerImpl.UserVMBlacklistedDetails.value().split(",");
+ String[] userVmSettingsToHide =
QueryService.UserVMBlacklistedDetails.value().split(",");
for (String key : userVmSettingsToHide) {
resourceDetails.remove(key.trim());
}
}
userVmResponse.setDetails(resourceDetails);
if (caller.getType() != Account.ACCOUNT_TYPE_ADMIN) {
-
userVmResponse.setReadOnlyUIDetails(QueryManagerImpl.UserVMReadOnlyUIDetails.value());
+
userVmResponse.setReadOnlyUIDetails(QueryService.UserVMReadOnlyUIDetails.value());
}
}
diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java
b/server/src/main/java/com/cloud/server/ManagementServerImpl.java
index 0066a95..305c0f1 100644
--- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java
+++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java
@@ -535,6 +535,7 @@ import
org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.framework.config.impl.ConfigurationVO;
import org.apache.cloudstack.framework.security.keystore.KeystoreManager;
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
+import org.apache.cloudstack.query.QueryService;
import org.apache.cloudstack.resourcedetail.dao.GuestOsDetailsDao;
import org.apache.cloudstack.storage.datastore.db.ImageStoreDao;
import org.apache.cloudstack.storage.datastore.db.ImageStoreVO;
@@ -555,7 +556,6 @@ import com.cloud.alert.AlertManager;
import com.cloud.alert.AlertVO;
import com.cloud.alert.dao.AlertDao;
import com.cloud.api.ApiDBUtils;
-import com.cloud.api.query.QueryManagerImpl;
import com.cloud.capacity.Capacity;
import com.cloud.capacity.CapacityVO;
import com.cloud.capacity.dao.CapacityDao;
@@ -3486,9 +3486,11 @@ public class ManagementServerImpl extends ManagerBase
implements ManagementServe
final Integer apiLimitInterval =
Integer.valueOf(_configDao.getValue(Config.ApiLimitInterval.key()));
final Integer apiLimitMax =
Integer.valueOf(_configDao.getValue(Config.ApiLimitMax.key()));
- final boolean allowUserViewDestroyedVM =
(QueryManagerImpl.AllowUserViewDestroyedVM.valueIn(caller.getId()) |
_accountService.isAdmin(caller.getId()));
+ final boolean allowUserViewDestroyedVM =
(QueryService.AllowUserViewDestroyedVM.valueIn(caller.getId()) |
_accountService.isAdmin(caller.getId()));
final boolean allowUserExpungeRecoverVM =
(UserVmManager.AllowUserExpungeRecoverVm.valueIn(caller.getId()) |
_accountService.isAdmin(caller.getId()));
+ final boolean allowUserViewAllDomainAccounts =
(QueryService.AllowUserViewAllDomainAccounts.valueIn(caller.getDomainId()));
+
// check if region-wide secondary storage is used
boolean regionSecondaryEnabled = false;
final List<ImageStoreVO> imgStores =
_imgStoreDao.findRegionImageStores();
@@ -3508,6 +3510,7 @@ public class ManagementServerImpl extends ManagerBase
implements ManagementServe
capabilities.put("KVMSnapshotEnabled", KVMSnapshotEnabled);
capabilities.put("allowUserViewDestroyedVM", allowUserViewDestroyedVM);
capabilities.put("allowUserExpungeRecoverVM",
allowUserExpungeRecoverVM);
+ capabilities.put("allowUserViewAllDomainAccounts",
allowUserViewAllDomainAccounts);
if (apiLimitEnabled) {
capabilities.put("apiLimitInterval", apiLimitInterval);
capabilities.put("apiLimitMax", apiLimitMax);
diff --git a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java
b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java
index 373735c..8d732cb 100755
--- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java
+++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java
@@ -32,28 +32,6 @@ import java.util.concurrent.Executors;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
-import com.cloud.deploy.DeployDestination;
-import com.cloud.storage.ImageStoreUploadMonitorImpl;
-import com.cloud.utils.StringUtils;
-import com.cloud.utils.EncryptionUtil;
-import com.cloud.utils.DateUtil;
-import com.cloud.utils.Pair;
-import com.cloud.utils.EnumUtils;
-import com.cloud.vm.VmDetailConstants;
-import com.google.common.base.Joiner;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-
-import org.apache.cloudstack.api.command.user.iso.GetUploadParamsForIsoCmd;
-import
org.apache.cloudstack.api.command.user.template.GetUploadParamsForTemplateCmd;
-import org.apache.cloudstack.framework.async.AsyncCallFuture;
-import org.apache.cloudstack.storage.command.TemplateOrVolumePostUploadCommand;
-import org.apache.cloudstack.storage.datastore.db.ImageStoreDao;
-import org.apache.cloudstack.storage.datastore.db.ImageStoreVO;
-import org.apache.cloudstack.utils.imagestore.ImageStoreUtil;
-import org.apache.commons.collections.CollectionUtils;
-import org.apache.commons.collections.MapUtils;
-import org.apache.log4j.Logger;
import org.apache.cloudstack.acl.SecurityChecker.AccessType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseListTemplateOrIsoPermissionsCmd;
@@ -61,6 +39,7 @@ import org.apache.cloudstack.api.BaseUpdateTemplateOrIsoCmd;
import org.apache.cloudstack.api.BaseUpdateTemplateOrIsoPermissionsCmd;
import org.apache.cloudstack.api.command.user.iso.DeleteIsoCmd;
import org.apache.cloudstack.api.command.user.iso.ExtractIsoCmd;
+import org.apache.cloudstack.api.command.user.iso.GetUploadParamsForIsoCmd;
import org.apache.cloudstack.api.command.user.iso.ListIsoPermissionsCmd;
import org.apache.cloudstack.api.command.user.iso.RegisterIsoCmd;
import org.apache.cloudstack.api.command.user.iso.UpdateIsoCmd;
@@ -69,6 +48,7 @@ import
org.apache.cloudstack.api.command.user.template.CopyTemplateCmd;
import org.apache.cloudstack.api.command.user.template.CreateTemplateCmd;
import org.apache.cloudstack.api.command.user.template.DeleteTemplateCmd;
import org.apache.cloudstack.api.command.user.template.ExtractTemplateCmd;
+import
org.apache.cloudstack.api.command.user.template.GetUploadParamsForTemplateCmd;
import
org.apache.cloudstack.api.command.user.template.ListTemplatePermissionsCmd;
import org.apache.cloudstack.api.command.user.template.RegisterTemplateCmd;
import org.apache.cloudstack.api.command.user.template.UpdateTemplateCmd;
@@ -95,6 +75,7 @@ import
org.apache.cloudstack.engine.subsystem.api.storage.TemplateService.Templa
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope;
+import org.apache.cloudstack.framework.async.AsyncCallFuture;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
@@ -104,6 +85,9 @@ import
org.apache.cloudstack.managed.context.ManagedContextRunnable;
import org.apache.cloudstack.storage.command.AttachCommand;
import org.apache.cloudstack.storage.command.CommandResult;
import org.apache.cloudstack.storage.command.DettachCommand;
+import org.apache.cloudstack.storage.command.TemplateOrVolumePostUploadCommand;
+import org.apache.cloudstack.storage.datastore.db.ImageStoreDao;
+import org.apache.cloudstack.storage.datastore.db.ImageStoreVO;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
@@ -111,6 +95,12 @@ import
org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO;
import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity;
import org.apache.cloudstack.storage.to.TemplateObjectTO;
+import org.apache.cloudstack.utils.imagestore.ImageStoreUtil;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.collections.MapUtils;
+import org.apache.log4j.Logger;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
@@ -129,6 +119,7 @@ import com.cloud.configuration.Resource.ResourceType;
import com.cloud.dc.DataCenter;
import com.cloud.dc.DataCenterVO;
import com.cloud.dc.dao.DataCenterDao;
+import com.cloud.deploy.DeployDestination;
import com.cloud.domain.Domain;
import com.cloud.domain.dao.DomainDao;
import com.cloud.event.ActionEvent;
@@ -148,6 +139,7 @@ import com.cloud.projects.Project;
import com.cloud.projects.ProjectManager;
import com.cloud.storage.DataStoreRole;
import com.cloud.storage.GuestOSVO;
+import com.cloud.storage.ImageStoreUploadMonitorImpl;
import com.cloud.storage.LaunchPermissionVO;
import com.cloud.storage.Snapshot;
import com.cloud.storage.SnapshotVO;
@@ -186,6 +178,11 @@ import com.cloud.user.AccountVO;
import com.cloud.user.ResourceLimitService;
import com.cloud.user.dao.AccountDao;
import com.cloud.uservm.UserVm;
+import com.cloud.utils.DateUtil;
+import com.cloud.utils.EncryptionUtil;
+import com.cloud.utils.EnumUtils;
+import com.cloud.utils.Pair;
+import com.cloud.utils.StringUtils;
import com.cloud.utils.component.AdapterBase;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.concurrency.NamedThreadFactory;
@@ -200,11 +197,12 @@ import com.cloud.vm.UserVmVO;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.VirtualMachine.State;
import com.cloud.vm.VirtualMachineProfile;
+import com.cloud.vm.VmDetailConstants;
import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.dao.VMInstanceDao;
-
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
+import com.google.common.base.Joiner;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
public class TemplateManagerImpl extends ManagerBase implements
TemplateManager, TemplateApiService, Configurable {
private final static Logger s_logger =
Logger.getLogger(TemplateManagerImpl.class);
@@ -1483,9 +1481,24 @@ public class TemplateManagerImpl extends ManagerBase
implements TemplateManager,
throw new InvalidParameterValueException("unable to update
permissions for " + mediaType + " with id " + id);
}
- boolean isAdmin = _accountMgr.isAdmin(caller.getId());
+ Long ownerId = template.getAccountId();
+ Account owner = _accountMgr.getAccount(ownerId);
+ if (ownerId == null) {
+ // if there is no owner of the template then it's probably already
a
+ // public template (or domain private template) so
+ // publishing to individual users is irrelevant
+ throw new InvalidParameterValueException("Update template
permissions is an invalid operation on template " + template.getName());
+ }
+
+ if (owner.getType() == Account.ACCOUNT_TYPE_PROJECT) {
+ // Currently project owned templates cannot be shared outside
project but is available to all users within project by default.
+ throw new InvalidParameterValueException("Update template
permissions is an invalid operation on template " + template.getName() +
+ ". Project owned templates cannot be shared outside
template.");
+ }
+
// check configuration parameter(allow.public.user.templates) value for
// the template owner
+ boolean isAdmin = _accountMgr.isAdmin(caller.getId());
boolean allowPublicUserTemplates =
AllowPublicUserTemplates.valueIn(template.getAccountId());
if (!isAdmin && !allowPublicUserTemplates && isPublic != null &&
isPublic) {
throw new InvalidParameterValueException("Only private " +
mediaType + "s can be created.");
@@ -1499,14 +1512,6 @@ public class TemplateManagerImpl extends ManagerBase
implements TemplateManager,
}
}
- Long ownerId = template.getAccountId();
- if (ownerId == null) {
- // if there is no owner of the template then it's probably already
a
- // public template (or domain private template) so
- // publishing to individual users is irrelevant
- throw new InvalidParameterValueException("Update template
permissions is an invalid operation on template " + template.getName());
- }
-
//Only admin or owner of the template should be able to change its
permissions
if (caller.getId() != ownerId && !isAdmin) {
throw new InvalidParameterValueException("Unable to grant
permission to account " + caller.getAccountName() + " as it is neither admin
nor owner or the template");
@@ -1540,7 +1545,6 @@ public class TemplateManagerImpl extends ManagerBase
implements TemplateManager,
}
//Derive the domain id from the template owner as
updateTemplatePermissions is not cross domain operation
- Account owner = _accountMgr.getAccount(ownerId);
final Domain domain = _domainDao.findById(owner.getDomainId());
if ("add".equalsIgnoreCase(operation)) {
final List<String> accountNamesFinal = accountNames;
diff --git a/ui/css/cloudstack3.css b/ui/css/cloudstack3.css
index 76ba97c..a40e252 100644
--- a/ui/css/cloudstack3.css
+++ b/ui/css/cloudstack3.css
@@ -12421,6 +12421,14 @@ div.ui-dialog div.autoscaler div.field-group
div.form-container form div.form-it
background-position: -35px -707px;
}
+.shareTemplate .icon {
+ background-position: -165px -122px;
+}
+
+.shareTemplate:hover .icon {
+ background-position: -165px -704px;
+}
+
.createVolume .icon {
background-position: -70px -124px;
}
diff --git a/ui/l10n/en.js b/ui/l10n/en.js
index 6a4bba9..53e9814 100644
--- a/ui/l10n/en.js
+++ b/ui/l10n/en.js
@@ -92,6 +92,7 @@ var dictionary = {
"label.about.app":"About CloudStack",
"label.accept.project.invitation":"Accept project invitation",
"label.account":"Account",
+"label.accounts":"Accounts",
"label.account.and.security.group":"Account, Security group",
"label.account.details":"Account details",
"label.account.id":"Account ID",
@@ -279,6 +280,7 @@ var dictionary = {
"label.action.run.diagnostics":"Run Diagnostics",
"label.action.secure.host":"Provision Host Security Keys",
"label.action.start.instance":"Start Instance",
+"label.action.share.template": "Update Template Permissions",
"label.action.start.instance.processing":"Starting Instance....",
"label.action.start.router":"Start Router",
"label.action.start.router.processing":"Starting Router....",
@@ -1253,6 +1255,7 @@ var dictionary = {
"label.opendaylight.controller":"OpenDaylight Controller",
"label.opendaylight.controllerdetail":"OpenDaylight Controller Details",
"label.opendaylight.controllers":"OpenDaylight Controllers",
+"label.operation": "Operation",
"label.operator":"Operator",
"label.optional":"Optional",
"label.order":"Order",
@@ -1342,6 +1345,7 @@ var dictionary = {
"label.project":"Project",
"label.project.dashboard":"Project dashboard",
"label.project.id":"Project ID",
+"label.project.ids":"Project IDs",
"label.project.invite":"Invite to project",
"label.project.name":"Project name",
"label.project.view":"Project View",
@@ -1576,6 +1580,7 @@ var dictionary = {
"label.setup.network":"Set up Network",
"label.setup.zone":"Set up Zone",
"label.shared":"Shared",
+"label.share.with":"Share With",
"label.show.advanced.settings":"Show advanced settings",
"label.show.ingress.rule":"Show Ingress Rule",
"label.shutdown.provider":"Shutdown provider",
diff --git a/ui/scripts/cloudStack.js b/ui/scripts/cloudStack.js
index 8785cd1..5280e7e 100644
--- a/ui/scripts/cloudStack.js
+++ b/ui/scripts/cloudStack.js
@@ -151,6 +151,8 @@
g_userProjectsEnabled =
json.listcapabilitiesresponse.capability.allowusercreateprojects;
g_cloudstackversion =
json.listcapabilitiesresponse.capability.cloudstackversion;
+ // Allow users to see all accounts within a domain
+ g_allowUserViewAllDomainAccounts =
json.listcapabilitiesresponse.capability.allowuserviewalldomainaccounts;
if
(json.listcapabilitiesresponse.capability.apilimitinterval != null &&
json.listcapabilitiesresponse.capability.apilimitmax != null) {
var intervalLimit =
((json.listcapabilitiesresponse.capability.apilimitinterval * 1000) /
json.listcapabilitiesresponse.capability.apilimitmax) * 3; //multiply 3 to be
on safe side
diff --git a/ui/scripts/docs.js b/ui/scripts/docs.js
index ec0b32a..4d00c83 100755
--- a/ui/scripts/docs.js
+++ b/ui/scripts/docs.js
@@ -1368,29 +1368,41 @@ cloudStack.docs = {
desc: 'Pass user and meta data to VMs (via ConfigDrive)',
externalLink: ''
},
-
helpComputeOfferingMinCPUCores: {
desc: 'This will be used for the setting the range (min-max) of the
number of cpu cores that should be allowed for VMs using this custom offering.',
externalLink: ''
},
-
helpComputeOfferingMaxCPUCores: {
desc: 'This will be used for the setting the range (min-max) of the
number of cpu cores that should be allowed for VMs using this custom offering.',
externalLink: ''
},
-
helpComputeOfferingMinMemory: {
desc: 'This will be used for the setting the range (min-max) amount of
memory that should be allowed for VMs using this custom offering.',
externalLink: ''
},
-
helpComputeOfferingMaxMemory: {
desc: 'This will be used for the setting the range (min-max) amount of
memory that should be allowed for VMs using this custom offering.',
externalLink: ''
},
-
helpComputeOfferingType: {
desc: 'This will be used for setting the type of compute offering -
whether it is fixed, custom constrained or custom unconstrained.',
externalLink: ''
+ },
+
+ // Update Template Permissions Helper
+ helpUpdateTemplateOperation: {
+ desc: 'Select the permission operator. Add is for sharing with
user/project and Reset simply removes all the accounts and projects which
template has been shared with.'
+ },
+ helpUpdateTemplateShareWith: {
+ desc: 'Select account or project with which template is to be shared
with.'
+ },
+ helpUpdateTemplateAccounts: {
+ desc: 'Choose one or more accounts to share this template. Ctrl+Click
to select multiple accounts to share with. Selecting "Add > Accounts" shows
list of accounts that do not have permissions. Selecting "Remove > Accounts"
shows list of accounts that already have permissions.'
+ },
+ helpUpdateTemplateProjectIds: {
+ desc: 'Choose one or more projects to share this template. Ctrl+Click
to select multiple projects to share with. Selecting "Add > Projects" shows
list of projects that do not have permissions. Selecting "Remove > Projects"
shows list of projects that already have permissions.'
+ },
+ helpUpdateTemplateAccountList: {
+ desc: 'A comma seperated list of accounts to share the template with.
Must be specified with the Add/Remove operation, leave Project ID blank if this
is specified.'
}
};
diff --git a/ui/scripts/sharedFunctions.js b/ui/scripts/sharedFunctions.js
index 9fe5151..84e233f 100644
--- a/ui/scripts/sharedFunctions.js
+++ b/ui/scripts/sharedFunctions.js
@@ -36,6 +36,7 @@ var g_queryAsyncJobResultInterval = 3000;
var g_idpList = null;
var g_appendIdpDomain = false;
var g_sortKeyIsAscending = false;
+var g_allowUserViewAllDomainAccounts= false;
//keyboard keycode
var keycode_Enter = 13;
diff --git a/ui/scripts/templates.js b/ui/scripts/templates.js
old mode 100755
new mode 100644
index c64efc9..a05e001
--- a/ui/scripts/templates.js
+++ b/ui/scripts/templates.js
@@ -1507,8 +1507,316 @@
notification: {
poll: pollAsyncJobResult
}
- }
+ },
+ // Share template
+ shareTemplate: {
+ label: 'label.action.share.template',
+ messages: {
+ notification: function (args) {
+ return 'label.action.share.template';
+ }
+ },
+
+ createForm: {
+ title: 'label.action.share.template',
+ desc: '',
+ fields: {
+ operation: {
+ label: 'label.operation',
+ docID:
'helpUpdateTemplateOperation',
+ validation: {
+ required: true
+ },
+ select: function (args) {
+ var items = [];
+ items.push({
+ id: "add",
+ description: "Add"
+ });
+ items.push({
+ id: "remove",
+ description: "Remove"
+ });
+ items.push({
+ id: "reset",
+ description: "Reset"
+ });
+
+ args.response.success({
+ data: items
+ });
+
+ // Select change
+ args.$select.change(function
() {
+ var $form =
$(this).closest('form');
+ var selectedOperation =
$(this).val();
+ if (selectedOperation ===
"reset") {
+
$form.find('[rel=projects]').hide();
+
$form.find('[rel=sharewith]').hide();
+
$form.find('[rel=accounts]').hide();
+
$form.find('[rel=accountlist]').hide();
+ } else {
+ //
allow.user.view.domain.accounts = true
+ // Populate List of
accounts in domain as dropdown multiselect
+
$form.find('[rel=sharewith]').css('display', 'inline-block');
+ if (!isUser() ||
g_allowUserViewAllDomainAccounts === true) {
+
$form.find('[rel=projects]').css('display', 'inline-block');
+
$form.find('[rel=accounts]').css('display', 'inline-block');
+
$form.find('[rel=accountlist]').hide();
+ } else {
+ // If users are
not allowed to see accounts in the domain, show input text field for Accounts
+ // Projects will
always be shown as dropdown multiselect
+
$form.find('[rel=projects]').css('display', 'inline-block');
+
$form.find('[rel=accountslist]').css('display', 'inline-block');
+
$form.find('[rel=accounts]').hide();
+ }
+ }
+ });
+ }
+ },
+ shareWith: {
+ label: 'label.share.with',
+ docID:
'helpUpdateTemplateShareWith',
+ validation: {
+ required: true
+ },
+ dependsOn: 'operation',
+ select: function (args) {
+ var items = [];
+ items.push({
+ id: "account",
+ description: "Account"
+ });
+ items.push({
+ id: "project",
+ description: "Project"
+ });
+
+ args.response.success({ data:
items });
+
+ // Select change
+ args.$select.change(function
() {
+ var $form =
$(this).closest('form');
+ var sharedWith =
$(this).val();
+ if (args.operation !==
"reset") {
+ if (sharedWith ===
"project") {
+
$form.find('[rel=accounts]').hide();
+
$form.find('[rel=accountlist]').hide();
+
$form.find('[rel=projects]').css('display', 'inline-block');
+ } else {
+ //
allow.user.view.domain.accounts = true
+ // Populate List
of accounts in domain as dropdown multiselect
+ if (!isUser() ||
g_allowUserViewAllDomainAccounts === true) {
+
$form.find('[rel=projects]').hide();
+
$form.find('[rel=accountlist]').hide();
+
$form.find('[rel=accounts]').css('display', 'inline-block');
+ } else {
+ // If users
are not allowed to see accounts in the domain, show input text field for
Accounts
+ // Projects
will always be shown as dropdown multiselect
+
$form.find('[rel=projects]').hide();
+
$form.find('[rel=accounts]').hide();
+
$form.find('[rel=accountlist]').css('display', 'inline-block');
+ }
+ }
+ }
+ });
+ }
+ },
+
+ accountlist: {
+ label: 'label.accounts',
+ docID:
'helpUpdateTemplateAccountList'
+ },
+
+ accounts: {
+ label: 'label.accounts',
+ docID:
'helpUpdateTemplateAccounts',
+ dependsOn: 'shareWith',
+ isMultiple: true,
+ select: function (args) {
+ var operation = args.operation;
+ if (operation !== "reset") {
+ $.ajax({
+ url:
createURL("listAccounts&listall=true"),
+ dataType: "json",
+ async: true,
+ success: function
(jsonAccounts) {
+ var accountByName
= {};
+
$.each(jsonAccounts.listaccountsresponse.account, function(idx, account) {
+ // Only add
current domain's accounts as update template permissions supports that
+ if
(account.domainid === g_domainid) {
+
accountByName[account.name] = {
+
projName: account.name,
+
hasPermission: false
+ };
+ }
+ });
+ $.ajax({
+ url:
createURL('listTemplatePermissions&id=' + args.context.templates[0].id),
+ dataType:
"json",
+ async: true,
+ success:
function (json) {
+ items =
json.listtemplatepermissionsresponse.templatepermission.account;
+
$.each(items, function(idx, accountName) {
+
accountByName[accountName].hasPermission = true;
+ });
+
+ var
accountObjs = [];
+ if
(operation === "add") {
+ //
Skip already permitted accounts
+
$.each(Object.keys(accountByName), function(idx, accountName) {
+ if
(accountByName[accountName].hasPermission == false) {
+
accountObjs.push({
+
name: accountName,
+
description: accountName
+
});
+ }
+ });
+ } else if
(items != null) {
+
$.each(items, function(idx, accountName) {
+ if
(accountName !== g_account) {
+
accountObjs.push({
+
name: accountName,
+
description: accountName
+
});
+ }
+ });
+ }
+
args.$select.html('');
+
args.response.success({data: accountObjs});
+ }
+ });
+ }
+ });
+ }
+ }
+ },
+
+ projects: {
+ label: 'label.projects',
+ docID:
'helpUpdateTemplateProjectIds',
+ dependsOn: 'shareWith',
+ isMultiple: true,
+ select: function (args) {
+ var operation = args.operation;
+ if (operation !== "reset") {
+ $.ajax({
+ url:
createURL("listProjects&listall=true"),
+ dataType: "json",
+ async: true,
+ success: function
(jsonProjects) {
+ var projectsByIds
= {};
+
$.each(jsonProjects.listprojectsresponse.project, function(idx, project) {
+ // Only add
current domain's projects as update template permissions supports that
+ if
(project.domainid === g_domainid) {
+
projectsByIds[project.id] = {
+
projName: project.name,
+
hasPermission: false
+ };
+ }
+ });
+
+ $.ajax({
+ url:
createURL('listTemplatePermissions&id=' + args.context.templates[0].id),
+ dataType:
"json",
+ async: true,
+ success:
function (json) {
+ items =
json.listtemplatepermissionsresponse.templatepermission.projectids;
+
$.each(items, function(idx, projectId) {
+
projectsByIds[projectId].hasPermission = true;
+ });
+ var
projectObjs = [];
+ if
(operation === "add") {
+ //
Skip already permitted accounts
+
$.each(Object.keys(projectsByIds), function(idx, projectId) {
+ if
(projectsByIds[projectId].hasPermission == false) {
+
projectObjs.push({
+
id: projectId,
+
description: projectsByIds[projectId].projName
+
});
+ }
+ });
+ } else if
(items != null) {
+
$.each(items, function(idx, projectId) {
+ if
(projectId !== g_account) {
+
projectObjs.push({
+
id: projectId,
+
description: projectsByIds[projectId] ? projectsByIds[projectId].projName
: projectId
+
});
+ }
+ });
+ }
+
args.$select.html('');
+
args.response.success({data: projectObjs});
+ }
+ });
+ }
+ });
+ }
+ }
+ }
+ }
+ },
+
+ action: function (args) {
+ // Load data from form
+ var data = {
+ id: args.context.templates[0].id,
+ op: args.data.operation
+ };
+ var selectedOperation =
args.data.operation;
+ if (selectedOperation === "reset") {
+ // Do not append Project ID or Account
to data object
+ } else {
+ var projects = args.data.projects;
+ var accounts = args.data.accounts;
+ var accountList =
args.data.accountlist;
+
+ if (accounts !== undefined ||
(accountList !== undefined && accountList.length > 0)) {
+ var accountNames = "";
+ if (accountList !== undefined &&
accounts === undefined) {
+ accountNames = accountList;
+ } else {
+ if
(Object.prototype.toString.call(accounts) === '[object Array]') {
+ accountNames =
accounts.join(",");
+ } else {
+ accountNames = accounts;
+ }
+ }
+ $.extend(data, {
+ accounts: accountNames
+ });
+ }
+
+ if (projects !== undefined) {
+ var projectIds = "";
+ if
(Object.prototype.toString.call(projects) === '[object Array]') {
+ projectIds =
projects.join(",");
+ } else {
+ projectIds = projects;
+ }
+
+ $.extend(data, {
+ projectids: projectIds
+ });
+ }
+ }
+
+ $.ajax({
+ url:
createURL('updateTemplatePermissions'),
+ data: data,
+ dataType: "json",
+ async: false,
+ success: function (json) {
+ var item =
json.updatetemplatepermissionsresponse.success;
+ args.response.success({
+ data: item
+ });
+ }
+ }); //end ajax
+ }
+ }
},
tabs: {
details: {
@@ -1882,11 +2190,11 @@
}else if(args.page == 1) {
args.response.success({
data: []
-
});
+
});
}
else {
args.response.success({
data: []
-
});
+
});
}
}
});
@@ -2202,7 +2510,7 @@
}
}
newDetails += 'details[0].' + data.name + '=' + data.value;
-
+
$.ajax({
url: createURL('updateTemplate&id=' +
args.context.templates[0].id + '&' + newDetails),
success: function(json) {
@@ -3429,7 +3737,7 @@
allowedActions.push("copyTemplate");
}
- // "Download Template"
+ // "Download Template" , "Update Template Permissions"
if (((isAdmin() == false && !(jsonObj.domainid == g_domainid &&
jsonObj.account == g_account) && !(jsonObj.domainid == g_domainid &&
cloudStack.context.projects && jsonObj.projectid ==
cloudStack.context.projects[0].id))) //if neither root-admin, nor the same
account, nor the same project
|| (jsonObj.isready == false) || jsonObj.templatetype == "SYSTEM")
{
//do nothing
@@ -3437,6 +3745,7 @@
if (jsonObj.isextractable){
allowedActions.push("downloadTemplate");
}
+ allowedActions.push("shareTemplate");
}
// "Delete Template"