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 77e5a15b09e9..410ffefb00dd 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 @@ protected String getResponseName() { @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 accountNames; @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = TemplateResponse.class, required = true, description = "the template ID") @@ -80,7 +80,6 @@ public List getAccountNames() { 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 9c526563d44d..40d1a71e9662 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 void execute() { 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 bcdad468fac8..153d7dfca9ae 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 void setAllowUserViewDestroyedVM(boolean allowUserViewDestroyedVM) { 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 618a8f6f8a5a..b9010cb89010 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 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 searchForUsers(ListUsersCmd cmd) throws PermissionDeniedException; ListResponse 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 b4c923675778..571e882dc03f 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -1806,6 +1806,11 @@ public TemplatePermissionsResponse createTemplatePermissionsResponse(ResponseVie List regularAccounts = new ArrayList(); 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 018ed794e76f..6ed7fa2ed060 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -387,6 +387,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 searchForUsers(ListUsersCmd cmd) throws PermissionDeniedException { Pair, Integer> result = searchForUsersInternal(cmd); @@ -1961,7 +1962,8 @@ private Pair, Integer> searchForAccountsInternal(ListAccount // 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(); @@ -2007,6 +2009,7 @@ private Pair, Integer> searchForAccountsInternal(ListAccount 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); @@ -2016,9 +2019,15 @@ private Pair, Integer> searchForAccountsInternal(ListAccount // 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 ssc = _accountJoinDao.createSearchCriteria(); ssc.addOr("accountName", SearchCriteria.Op.LIKE, "%" + keyword + "%"); @@ -3761,6 +3770,6 @@ public String getConfigComponentName() { @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 58b167f6ac3b..0e7374369c33 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.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 UserVmResponse newUserVmResponse(ResponseView view, String objectName, Us } // 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 4beddcc6e02b..4f48ad3c66ac 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -534,6 +534,7 @@ 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; @@ -554,7 +555,6 @@ 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; @@ -3484,9 +3484,11 @@ public Map listCapabilities(final ListCapabilitiesCmd cmd) { 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 imgStores = _imgStoreDao.findRegionImageStores(); @@ -3506,6 +3508,7 @@ public Map listCapabilities(final ListCapabilitiesCmd cmd) { 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 373735ce2012..8d732cb28447 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 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.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.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.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.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.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.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.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.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.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 boolean updateTemplateOrIsoPermissions(BaseUpdateTemplateOrIsoPermissions 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 boolean updateTemplateOrIsoPermissions(BaseUpdateTemplateOrIsoPermissions } } - 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 boolean updateTemplateOrIsoPermissions(BaseUpdateTemplateOrIsoPermissions } //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 accountNamesFinal = accountNames; diff --git a/ui/css/cloudstack3.css b/ui/css/cloudstack3.css index a393ef7597b9..f06c9c945db6 100644 --- a/ui/css/cloudstack3.css +++ b/ui/css/cloudstack3.css @@ -12396,6 +12396,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 cd5f44de4d96..4d99820ac099 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....", @@ -1248,6 +1250,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", @@ -1337,6 +1340,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", @@ -1570,6 +1574,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 8785cd19736f..5280e7e6ff09 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 b032d4eaf3e0..7e3039f4d5ec 100755 --- a/ui/scripts/docs.js +++ b/ui/scripts/docs.js @@ -1338,29 +1338,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 4218bffec95a..698137fc3572 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 c64efc973d83..a05e001ed32e --- 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"