diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index c5671d22d07b..cfeccfb044c5 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1397,6 +1397,7 @@ public class ApiConstants { public static final String CSS = "css"; public static final String JSON_CONFIGURATION = "jsonconfiguration"; + public static final String LOGIN_BASE_DOMAIN = "loginbasedomain"; public static final String COMMON_NAMES = "commonnames"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/CreateGuiThemeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/CreateGuiThemeCmd.java index 8566b413cc12..2e3849cc8503 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/CreateGuiThemeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/CreateGuiThemeCmd.java @@ -57,6 +57,10 @@ public class CreateGuiThemeCmd extends BaseCmd { "wildcard) separated by comma that can retrieve the theme; e.g.: *acme.com,acme2.com") private String commonNames; + @Parameter(name = ApiConstants.LOGIN_BASE_DOMAIN, type = CommandType.STRING, length = 65535, description = "The ACS domain to be used as base " + + "for the login when accessing the GUI through the common name defined in the theme. If a common name is not defined, this parameter is ignored on the GUI.") + private String loginBaseDomain; + @Parameter(name = ApiConstants.DOMAIN_IDS, type = CommandType.STRING, length = 65535, description = "A set of domain UUIDs (also known as ID for " + "the end-user) separated by comma that can retrieve the theme.") private String domainIds; @@ -93,6 +97,10 @@ public String getCommonNames() { return commonNames; } + public String getLoginBaseDomain() { + return loginBaseDomain; + } + public String getDomainIds() { return domainIds; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/UpdateGuiThemeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/UpdateGuiThemeCmd.java index daef2235ce89..e50961aa30d1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/UpdateGuiThemeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/UpdateGuiThemeCmd.java @@ -60,6 +60,10 @@ public class UpdateGuiThemeCmd extends BaseCmd { "wildcard) separated by comma that can retrieve the theme; e.g.: *acme.com,acme2.com") private String commonNames; + @Parameter(name = ApiConstants.LOGIN_BASE_DOMAIN, type = CommandType.STRING, length = 65535, description = "The ACS domain to be used as base for " + + "the login when accessing the GUI through the common name defined in the theme. If a common name is not defined, this parameter is ignored on the GUI.") + private String loginBaseDomain; + @Parameter(name = ApiConstants.DOMAIN_IDS, type = CommandType.STRING, length = 65535, description = "A set of domain UUIDs (also known as ID for " + "the end-user) separated by comma that can retrieve the theme.") private String domainIds; @@ -96,6 +100,10 @@ public String getJsonConfiguration() { return jsonConfiguration; } + public String getLoginBaseDomain() { + return loginBaseDomain; + } + public String getCommonNames() { return commonNames; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/GuiThemeResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/GuiThemeResponse.java index fe8a85b4176e..3c53bdb9d93d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/GuiThemeResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/GuiThemeResponse.java @@ -52,6 +52,11 @@ public class GuiThemeResponse extends BaseResponse { @Param(description = "A set of Common Names (CN) (fixed or wildcard) separated by comma that can retrieve the theme; e.g.: *acme.com,acme2.com") private String commonNames; + @SerializedName(ApiConstants.LOGIN_BASE_DOMAIN) + @Param(description = "The ACS domain to be used as base for the login when accessing the GUI through the common name defined in the theme. If a " + + "common name is not defined, this parameter is ignored on the GUI.") + private String loginBaseDomain; + @SerializedName(ApiConstants.DOMAIN_IDS) @Param(description = "A set of domain UUIDs (also known as ID for the end-user) separated by comma that can retrieve the theme.") private String domainIds; @@ -176,4 +181,12 @@ public void setRecursiveDomains(Boolean recursiveDomains) { public void setRemoved(Date removed) { this.removed = removed; } + + public String getLoginBaseDomain() { + return loginBaseDomain; + } + + public void setLoginBaseDomain(String loginBaseDomain) { + this.loginBaseDomain = loginBaseDomain; + } } diff --git a/api/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeJoin.java b/api/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeJoin.java index e54d53138ef6..cb3708cda373 100644 --- a/api/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeJoin.java +++ b/api/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeJoin.java @@ -44,4 +44,6 @@ public interface GuiThemeJoin extends InternalIdentity, Identity { Date getCreated(); Date getRemoved(); + + String getLoginBaseDomain(); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeJoinVO.java b/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeJoinVO.java index 2df23b3d1064..45bad5a44d79 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeJoinVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeJoinVO.java @@ -63,6 +63,9 @@ public class GuiThemeJoinVO implements GuiThemeJoin { @Column(name = "is_public") private boolean isPublic; + @Column(name = "login_base_domain") + private String loginBaseDomain; + @Column(name = GenericDao.CREATED_COLUMN, nullable = false) @Temporal(value = TemporalType.TIMESTAMP) private Date created; @@ -138,4 +141,9 @@ public Date getCreated() { public Date getRemoved() { return removed; } + + @Override + public String getLoginBaseDomain() { + return loginBaseDomain; + } } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeVO.java b/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeVO.java index 887e3886f6c6..729071f60b80 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeVO.java @@ -59,6 +59,9 @@ public class GuiThemeVO implements GuiTheme { @Column(name = "recursive_domains") private boolean recursiveDomains = false; + @Column(name = "login_base_domain", length = 65535) + private String loginBaseDomain; + @Column(name = GenericDao.CREATED_COLUMN, nullable = false) @Temporal(value = TemporalType.TIMESTAMP) private Date created; @@ -71,7 +74,8 @@ public GuiThemeVO() { } - public GuiThemeVO(String name, String description, String css, String jsonConfiguration, boolean recursiveDomains, boolean isPublic, Date created, Date removed) { + public GuiThemeVO(String name, String description, String css, String jsonConfiguration, boolean recursiveDomains, + boolean isPublic, Date created, String loginBaseDomain, Date removed) { this.name = name; this.description = description; this.css = css; @@ -79,6 +83,7 @@ public GuiThemeVO(String name, String description, String css, String jsonConfig this.recursiveDomains = recursiveDomains; this.isPublic = isPublic; this.created = created; + this.loginBaseDomain = loginBaseDomain; this.removed = removed; } @@ -186,4 +191,8 @@ public void setRecursiveDomains(boolean recursiveDomains) { public String toString() { return ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "uuid", "name", "description", "isPublic", "recursiveDomains"); } + + public void setLoginBaseDomain(String loginBaseDomain) { + this.loginBaseDomain = loginBaseDomain; + } } diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index 2d25b3355d8e..a010c886d770 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -208,3 +208,6 @@ INSERT INTO cloud.role_permissions (uuid, role_id, rule, permission, sort_order) SELECT uuid(), role_id, 'quotaResourceStatement', permission, sort_order FROM cloud.role_permissions rp WHERE rule = 'quotaStatement' AND NOT EXISTS(SELECT 1 FROM cloud.role_permissions rp_ WHERE rp.role_id = rp_.role_id AND rp_.rule = 'quotaResourceStatement'); + +--- Gui theme login base domain +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.gui_themes', 'login_base_domain', 'TEXT DEFAULT NULL'); diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.gui_themes_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.gui_themes_view.sql index 3173274623ed..5f49cffbf1f5 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.gui_themes_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.gui_themes_view.sql @@ -27,6 +27,7 @@ SELECT `cloud`.`gui_themes`.`description` AS `description`, `cloud`.`gui_themes`.`css` AS `css`, `cloud`.`gui_themes`.`json_configuration` AS `json_configuration`, + `cloud`.`gui_themes`.`login_base_domain` AS `login_base_domain`, (SELECT group_concat(gtd.`value` separator ',') FROM `cloud`.`gui_themes_details` gtd WHERE gtd.`type` = 'commonName' AND gtd.gui_theme_id = `cloud`.`gui_themes`.`id`) common_names, (SELECT group_concat(gtd.`value` separator ',') FROM `cloud`.`gui_themes_details` gtd WHERE gtd.`type` = 'domain' AND gtd.gui_theme_id = `cloud`.`gui_themes`.`id`) domains, (SELECT group_concat(gtd.`value` separator ',') FROM `cloud`.`gui_themes_details` gtd WHERE gtd.`type` = 'account' AND gtd.gui_theme_id = `cloud`.`gui_themes`.`id`) accounts, diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index a8551b4c6693..b4f9dbf9c583 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -5719,6 +5719,7 @@ public GuiThemeResponse createGuiThemeResponse(GuiThemeJoin guiThemeJoin) { guiThemeResponse.setJsonConfiguration(guiThemeJoin.getJsonConfiguration()); guiThemeResponse.setCss(guiThemeJoin.getCss()); + guiThemeResponse.setLoginBaseDomain(guiThemeJoin.getLoginBaseDomain()); guiThemeResponse.setResponseName("guithemes"); return guiThemeResponse; diff --git a/server/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeServiceImpl.java b/server/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeServiceImpl.java index 9a92b9bef013..5e95bb6a9941 100644 --- a/server/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeServiceImpl.java +++ b/server/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeServiceImpl.java @@ -39,6 +39,7 @@ import org.apache.cloudstack.gui.theme.dao.GuiThemeDetailsDao; import org.apache.cloudstack.gui.theme.dao.GuiThemeJoinDao; import org.apache.cloudstack.gui.theme.json.config.validator.JsonConfigValidator; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -126,21 +127,18 @@ public GuiThemeJoin createGuiTheme(CreateGuiThemeCmd cmd) { String providedAccountIds = cmd.getAccountIds(); boolean isPublic = cmd.getPublic(); Boolean recursiveDomains = cmd.getRecursiveDomains(); + String baseDomainName = cmd.getLoginBaseDomain(); CallContext.current().setEventDetails(String.format("Name: %s, AccountIDs: %s, DomainIDs: %s, RecursiveDomains: %s, CommonNames: %s", name, providedAccountIds, providedDomainIds, recursiveDomains, commonNames)); - if (StringUtils.isAllBlank(css, jsonConfiguration)) { - throw new CloudRuntimeException("Either the `css` or `jsonConfiguration` parameter must be informed."); - } - - validateParameters(jsonConfiguration, providedDomainIds, providedAccountIds, commonNames, null); + validateParameters(css, jsonConfiguration, providedDomainIds, providedAccountIds, commonNames, baseDomainName, null); if (shouldSetGuiThemeToPrivate(providedDomainIds, providedAccountIds)) { isPublic = false; } - GuiThemeVO guiThemeVO = new GuiThemeVO(name, description, css, jsonConfiguration, recursiveDomains, isPublic, new Date(), null); + GuiThemeVO guiThemeVO = new GuiThemeVO(name, description, css, jsonConfiguration, recursiveDomains, isPublic, new Date(), cmd.getLoginBaseDomain(), null); guiThemeDao.persist(guiThemeVO); persistGuiThemeDetails(guiThemeVO.getId(), commonNames, providedDomainIds, providedAccountIds); return guiThemeJoinDao.findById(guiThemeVO.getId()); @@ -224,7 +222,11 @@ protected Pair, Integer> listGuiThemesInternal(ListGuiTheme return guiThemeJoinDao.listGuiThemes(id, name, commonName, domainUuid, accountUuid, listAll, showRemoved, showPublic); } - protected void validateParameters(String jsonConfig, String domainIds, String accountIds, String commonNames, Long idOfThemeToBeUpdated) { + protected void validateParameters(String css, String jsonConfig, String domainIds, String accountIds, String commonNames, String loginBaseDomain, Long idOfThemeToBeUpdated) { + if (StringUtils.isAllBlank(css, jsonConfig, loginBaseDomain)) { + throw new CloudRuntimeException("At least one of the `css`, `jsonconfiguration`, or `loginbasedomain` parameters must be informed."); + } + if (isConsideredDefaultTheme(commonNames, domainIds, accountIds)) { checkIfDefaultThemeIsAllowed(commonNames, domainIds, accountIds, idOfThemeToBeUpdated); } @@ -232,6 +234,7 @@ protected void validateParameters(String jsonConfig, String domainIds, String ac validateObjectUuids(accountIds, Account.class); validateObjectUuids(domainIds, Domain.class); jsonConfigValidator.validateJsonConfiguration(jsonConfig); + validateLoginBaseDomain(loginBaseDomain, commonNames); } /** @@ -254,6 +257,12 @@ protected void validateObjectUuids(String providedIds, Class clazz) { } } + protected void validateLoginBaseDomain(String loginBaseDomain, String commonNames) { + if (loginBaseDomain != null && StringUtils.isBlank(commonNames)) { + throw new CloudRuntimeException("Parameter `loginBaseDomain` must be provided with `commonNames`."); + } + } + @Override @ActionEvent(eventType = EventTypes.EVENT_GUI_THEME_UPDATE, eventDescription = "Updating GUI theme") public GuiThemeJoin updateGuiTheme(UpdateGuiThemeCmd cmd) { @@ -267,23 +276,24 @@ public GuiThemeJoin updateGuiTheme(UpdateGuiThemeCmd cmd) { String commonNames = cmd.getCommonNames() == null ? guiThemeJoinVO.getCommonNames() : cmd.getCommonNames(); String providedDomainIds = cmd.getDomainIds() == null ? guiThemeJoinVO.getDomains() : cmd.getDomainIds(); String providedAccountIds = cmd.getAccountIds() == null ? guiThemeJoinVO.getAccounts() : cmd.getAccountIds(); + String baseDomainName = ObjectUtils.defaultIfNull(cmd.getLoginBaseDomain(), guiThemeJoinVO.getLoginBaseDomain()); Boolean isPublic = cmd.getIsPublic(); Boolean recursiveDomains = cmd.getRecursiveDomains(); CallContext.current().setEventDetails(String.format("ID: %s, Name: %s, AccountIDs: %s, DomainIDs: %s, RecursiveDomains: %s, CommonNames: %s", guiThemeId, name, providedAccountIds, providedDomainIds, recursiveDomains, commonNames)); - validateParameters(jsonConfiguration, providedDomainIds, providedAccountIds, commonNames, guiThemeId); + validateParameters(css, jsonConfiguration, providedDomainIds, providedAccountIds, commonNames, baseDomainName, guiThemeId); if (shouldSetGuiThemeToPrivate(providedDomainIds, providedAccountIds)) { isPublic = false; } - return persistGuiTheme(guiThemeId, name, description, css, jsonConfiguration, commonNames, providedDomainIds, providedAccountIds, isPublic, recursiveDomains); + return persistGuiTheme(guiThemeId, name, description, css, jsonConfiguration, commonNames, providedDomainIds, providedAccountIds, isPublic, recursiveDomains, baseDomainName); } protected GuiThemeJoinVO persistGuiTheme(Long guiThemeId, String name, String description, String css, String jsonConfiguration, String commonNames, String providedDomainIds, - String providedAccountIds, Boolean isPublic, Boolean recursiveDomains){ + String providedAccountIds, Boolean isPublic, Boolean recursiveDomains, String loginBaseDomain){ return Transaction.execute((TransactionCallback) status -> { GuiThemeVO guiThemeVO = guiThemeDao.findById(guiThemeId); @@ -311,6 +321,10 @@ protected GuiThemeJoinVO persistGuiTheme(Long guiThemeId, String name, String de guiThemeVO.setRecursiveDomains(recursiveDomains); } + if (loginBaseDomain != null) { + guiThemeVO.setLoginBaseDomain(loginBaseDomain); + } + logger.trace("Persisting GUI theme [{}] with CSS [{}] and JSON configuration [{}].", guiThemeVO, guiThemeVO.getCss(), guiThemeVO.getJsonConfiguration()); guiThemeDao.persist(guiThemeVO); diff --git a/server/src/test/java/org/apache/cloudstack/gui/theme/GuiThemeServiceImplTest.java b/server/src/test/java/org/apache/cloudstack/gui/theme/GuiThemeServiceImplTest.java index c47fdadd32c7..c7579040be24 100644 --- a/server/src/test/java/org/apache/cloudstack/gui/theme/GuiThemeServiceImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/gui/theme/GuiThemeServiceImplTest.java @@ -65,6 +65,7 @@ public class GuiThemeServiceImplTest { private static final String ACCOUNT_IDS = "4,5,6"; + private static final String LOGIN_BASE_DOMAIN = "acmedomain"; private static final String BLANK_STRING = ""; @Test @@ -172,6 +173,26 @@ public void validateObjectUuidsTestProvidedUuidsAreNotValidShouldThrowCloudRunti guiThemeServiceSpy.validateObjectUuids(ACCOUNT_IDS, Account.class); } + @Test + public void validateLoginBaseDomainTestBaseDomainIsNullCommonNamesIsNullShouldNotThrowCloudRuntimeException() { + guiThemeServiceSpy.validateLoginBaseDomain(null, null); + } + + @Test + public void validateLoginBaseDomainTestBaseDomainIsNullCommonNamesIsNotNullShouldNotThrowCloudRuntimeException() { + guiThemeServiceSpy.validateLoginBaseDomain(null, COMMON_NAME); + } + + @Test + public void validateLoginBaseDomainTestBaseDomainIsNotNullCommonNamesIsNotNullShouldNotThrowCloudRuntimeException() { + guiThemeServiceSpy.validateLoginBaseDomain(LOGIN_BASE_DOMAIN, COMMON_NAME); + } + + @Test(expected = CloudRuntimeException.class) + public void validateLoginBaseDomainTestBaseDomainIsNotNullCommonNamesIsNullShouldNotThrowCloudRuntimeException() { + guiThemeServiceSpy.validateLoginBaseDomain(LOGIN_BASE_DOMAIN, null); + } + @Test public void checkIfDefaultThemeIsAllowedTestThemeIsNotConsideredDefault() { Mockito.when(guiThemeServiceSpy.isConsideredDefaultTheme(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(false); diff --git a/ui/src/utils/guiTheme.js b/ui/src/utils/guiTheme.js index 438ce9333a4e..dab82287d111 100644 --- a/ui/src/utils/guiTheme.js +++ b/ui/src/utils/guiTheme.js @@ -63,6 +63,11 @@ async function applyDynamicCustomization (response) { jsonConfig = JSON.parse(response?.jsonconfiguration) } + vueProps.$config.loginBaseDomain = '' + if (response?.loginbasedomain) { + vueProps.$config.loginBaseDomain = response.loginbasedomain + } + // Sets custom GUI fields only if is not nullish. vueProps.$config.appTitle = jsonConfig?.appTitle ?? vueProps.$config.appTitle vueProps.$config.footer = jsonConfig?.footer ?? vueProps.$config.footer diff --git a/ui/src/views/auth/Login.vue b/ui/src/views/auth/Login.vue index 24065f47b1aa..1b7685262978 100644 --- a/ui/src/views/auth/Login.vue +++ b/ui/src/views/auth/Login.vue @@ -364,11 +364,20 @@ export default { }, handleDomain () { const values = toRaw(this.form) - if (!values.domain) { - this.$store.commit('SET_DOMAIN_USED_TO_LOGIN', '/') - } else { - this.$store.commit('SET_DOMAIN_USED_TO_LOGIN', values.domain) + const domain = this.getLoginDomain(values.domain) + this.$store.commit('SET_DOMAIN_USED_TO_LOGIN', domain) + }, + getLoginDomain (domain) { + if (this.$config.loginBaseDomain) { + if (domain) { + return this.$config.loginBaseDomain + '/' + domain + } + return this.$config.loginBaseDomain } + if (domain) { + return domain + } + return '/' }, getGitHubUrl (from) { const rootURl = 'https://github.com/login/oauth/authorize' @@ -417,10 +426,7 @@ export default { delete loginParams.username loginParams[!this.state.loginType ? 'email' : 'username'] = values.username loginParams.password = values.password - loginParams.domain = values.domain - if (!loginParams.domain) { - loginParams.domain = '/' - } + loginParams.domain = this.getLoginDomain(values.domain) this.Login(loginParams) .then((res) => this.loginSuccess(res)) .catch(err => { @@ -449,10 +455,7 @@ export default { loginParams.email = this.email loginParams.provider = provider loginParams.secretcode = this.secretcode - loginParams.domain = values.domain - if (!loginParams.domain) { - loginParams.domain = '/' - } + loginParams.domain = this.getLoginDomain(values.domain) this.OauthLogin(loginParams) .then((res) => this.loginSuccess(res)) .catch(err => {