KEYCLOAK-15773 Control availability of admin api and admin-console via feature flags
Inline profile checks for enabled admin-console to avoid issues during static initialization with quarkus. Potentially Re-enable admin-api feature if admin-console is enabled via the admin/admin2 feature flag. Add legacy admin console as deprecated feature flag Throw exception if admin-api feature is disabled but admin-console is enabled Adapt ProfileTest Consider adminConsoleEnabled flag in QuarkusWelcomeResource Fix check for Admin-Console / Admin-API feature dependency. Add new features to approved help output files Co-authored-by: Stian Thorgersen <stian@redhat.com> Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
This commit is contained in:
parent
3518362002
commit
962a685b7b
12 changed files with 127 additions and 56 deletions
|
@ -22,6 +22,7 @@ import static org.keycloak.common.Profile.Type.DEPRECATED;
|
|||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
@ -89,6 +90,11 @@ public class Profile {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ((!disabledFeatures.contains(Feature.ADMIN2) || !disabledFeatures.contains(Feature.ADMIN)) && disabledFeatures.contains(Feature.ADMIN_API)) {
|
||||
throw new RuntimeException(String.format("Invalid value for feature: %s needs to be enabled because it is required by feature %s.",
|
||||
Feature.ADMIN_API, Arrays.asList(Feature.ADMIN, Feature.ADMIN2)));
|
||||
}
|
||||
}
|
||||
|
||||
private static Profile getInstance() {
|
||||
|
@ -153,6 +159,22 @@ public class Profile {
|
|||
ACCOUNT2("New Account Management Console", Type.DEFAULT),
|
||||
ACCOUNT_API("Account Management REST API", Type.DEFAULT),
|
||||
ADMIN_FINE_GRAINED_AUTHZ("Fine-Grained Admin Permissions", Type.PREVIEW),
|
||||
/**
|
||||
* Controls the availability of the Admin REST-API.
|
||||
*/
|
||||
ADMIN_API("Admin API", Type.DEFAULT),
|
||||
|
||||
/**
|
||||
* Controls the availability of the legacy admin-console.
|
||||
* Note that the admin-console requires the {@link #ADMIN_API} feature.
|
||||
*/
|
||||
@Deprecated
|
||||
ADMIN("Legacy Admin Console", Type.DEPRECATED),
|
||||
|
||||
/**
|
||||
* Controls the availability of the admin-console.
|
||||
* Note that the admin-console requires the {@link #ADMIN_API} feature.
|
||||
*/
|
||||
ADMIN2("New Admin Console", Type.DEFAULT),
|
||||
DOCKER("Docker Registry protocol", Type.DISABLED_BY_DEFAULT),
|
||||
IMPERSONATION("Ability for admins to impersonate users", Type.DEFAULT),
|
||||
|
|
|
@ -24,7 +24,7 @@ public class ProfileTest {
|
|||
@Test
|
||||
public void checkDefaultsKeycloak() {
|
||||
Assert.assertEquals("community", Profile.getName());
|
||||
assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DYNAMIC_SCOPES, Profile.Feature.DOCKER, Profile.Feature.RECOVERY_CODES, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE, Feature.CLIENT_SECRET_ROTATION, Feature.UPDATE_EMAIL);
|
||||
assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN, Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DYNAMIC_SCOPES, Profile.Feature.DOCKER, Profile.Feature.RECOVERY_CODES, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE, Feature.CLIENT_SECRET_ROTATION, Feature.UPDATE_EMAIL);
|
||||
assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.RECOVERY_CODES, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.DECLARATIVE_USER_PROFILE, Feature.CLIENT_SECRET_ROTATION, Feature.UPDATE_EMAIL);
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,7 @@ public class ProfileTest {
|
|||
Profile.init();
|
||||
|
||||
Assert.assertEquals("product", Profile.getName());
|
||||
assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DYNAMIC_SCOPES, Profile.Feature.DOCKER, Profile.Feature.RECOVERY_CODES, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE, Feature.CLIENT_SECRET_ROTATION, Feature.UPDATE_EMAIL);
|
||||
assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN, Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DYNAMIC_SCOPES, Profile.Feature.DOCKER, Profile.Feature.RECOVERY_CODES, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE, Feature.CLIENT_SECRET_ROTATION, Feature.UPDATE_EMAIL);
|
||||
assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.RECOVERY_CODES, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.DECLARATIVE_USER_PROFILE, Feature.CLIENT_SECRET_ROTATION, Feature.UPDATE_EMAIL);
|
||||
|
||||
System.setProperty("keycloak.profile", "community");
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.quarkus.runtime.services.resources;
|
|||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.Version;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.common.util.MimeTypeUtil;
|
||||
|
@ -173,6 +174,7 @@ public class QuarkusWelcomeResource {
|
|||
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
|
||||
map.put("adminConsoleEnabled", isAdminConsoleEnabled());
|
||||
map.put("productName", Version.NAME);
|
||||
map.put("productNameFull", Version.NAME_FULL);
|
||||
|
||||
|
@ -250,6 +252,10 @@ public class QuarkusWelcomeResource {
|
|||
return shouldBootstrap.get();
|
||||
}
|
||||
|
||||
private static boolean isAdminConsoleEnabled() {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.ADMIN2) || Profile.isFeatureEnabled(Profile.Feature.ADMIN);
|
||||
}
|
||||
|
||||
private boolean isLocal() {
|
||||
try {
|
||||
ClientConnection clientConnection = session.getContext().getConnection();
|
||||
|
|
|
@ -44,18 +44,18 @@ Transaction:
|
|||
Feature:
|
||||
|
||||
--features <feature> Enables a set of one or more features. Possible values are: authorization,
|
||||
account2, account-api, admin-fine-grained-authz, admin2, docker,
|
||||
impersonation, openshift-integration, scripts, token-exchange, web-authn,
|
||||
client-policies, ciba, map-storage, par, declarative-user-profile,
|
||||
dynamic-scopes, client-secret-rotation, step-up-authentication,
|
||||
recovery-codes, update-email, preview.
|
||||
account2, account-api, admin-fine-grained-authz, admin-api, admin, admin2,
|
||||
docker, impersonation, openshift-integration, scripts, token-exchange,
|
||||
web-authn, client-policies, ciba, map-storage, par,
|
||||
declarative-user-profile, dynamic-scopes, client-secret-rotation,
|
||||
step-up-authentication, recovery-codes, update-email, preview.
|
||||
--features-disabled <feature>
|
||||
Disables a set of one or more features. Possible values are: authorization,
|
||||
account2, account-api, admin-fine-grained-authz, admin2, docker,
|
||||
impersonation, openshift-integration, scripts, token-exchange, web-authn,
|
||||
client-policies, ciba, map-storage, par, declarative-user-profile,
|
||||
dynamic-scopes, client-secret-rotation, step-up-authentication,
|
||||
recovery-codes, update-email, preview.
|
||||
account2, account-api, admin-fine-grained-authz, admin-api, admin, admin2,
|
||||
docker, impersonation, openshift-integration, scripts, token-exchange,
|
||||
web-authn, client-policies, ciba, map-storage, par,
|
||||
declarative-user-profile, dynamic-scopes, client-secret-rotation,
|
||||
step-up-authentication, recovery-codes, update-email, preview.
|
||||
|
||||
HTTP/TLS:
|
||||
|
||||
|
|
|
@ -67,18 +67,18 @@ Transaction:
|
|||
Feature:
|
||||
|
||||
--features <feature> Enables a set of one or more features. Possible values are: authorization,
|
||||
account2, account-api, admin-fine-grained-authz, admin2, docker,
|
||||
impersonation, openshift-integration, scripts, token-exchange, web-authn,
|
||||
client-policies, ciba, map-storage, par, declarative-user-profile,
|
||||
dynamic-scopes, client-secret-rotation, step-up-authentication,
|
||||
recovery-codes, update-email, preview.
|
||||
account2, account-api, admin-fine-grained-authz, admin-api, admin, admin2,
|
||||
docker, impersonation, openshift-integration, scripts, token-exchange,
|
||||
web-authn, client-policies, ciba, map-storage, par,
|
||||
declarative-user-profile, dynamic-scopes, client-secret-rotation,
|
||||
step-up-authentication, recovery-codes, update-email, preview.
|
||||
--features-disabled <feature>
|
||||
Disables a set of one or more features. Possible values are: authorization,
|
||||
account2, account-api, admin-fine-grained-authz, admin2, docker,
|
||||
impersonation, openshift-integration, scripts, token-exchange, web-authn,
|
||||
client-policies, ciba, map-storage, par, declarative-user-profile,
|
||||
dynamic-scopes, client-secret-rotation, step-up-authentication,
|
||||
recovery-codes, update-email, preview.
|
||||
account2, account-api, admin-fine-grained-authz, admin-api, admin, admin2,
|
||||
docker, impersonation, openshift-integration, scripts, token-exchange,
|
||||
web-authn, client-policies, ciba, map-storage, par,
|
||||
declarative-user-profile, dynamic-scopes, client-secret-rotation,
|
||||
step-up-authentication, recovery-codes, update-email, preview.
|
||||
|
||||
Hostname:
|
||||
|
||||
|
|
|
@ -128,18 +128,18 @@ Transaction:
|
|||
Feature:
|
||||
|
||||
--features <feature> Enables a set of one or more features. Possible values are: authorization,
|
||||
account2, account-api, admin-fine-grained-authz, admin2, docker,
|
||||
impersonation, openshift-integration, scripts, token-exchange, web-authn,
|
||||
client-policies, ciba, map-storage, par, declarative-user-profile,
|
||||
dynamic-scopes, client-secret-rotation, step-up-authentication,
|
||||
recovery-codes, update-email, preview.
|
||||
account2, account-api, admin-fine-grained-authz, admin-api, admin, admin2,
|
||||
docker, impersonation, openshift-integration, scripts, token-exchange,
|
||||
web-authn, client-policies, ciba, map-storage, par,
|
||||
declarative-user-profile, dynamic-scopes, client-secret-rotation,
|
||||
step-up-authentication, recovery-codes, update-email, preview.
|
||||
--features-disabled <feature>
|
||||
Disables a set of one or more features. Possible values are: authorization,
|
||||
account2, account-api, admin-fine-grained-authz, admin2, docker,
|
||||
impersonation, openshift-integration, scripts, token-exchange, web-authn,
|
||||
client-policies, ciba, map-storage, par, declarative-user-profile,
|
||||
dynamic-scopes, client-secret-rotation, step-up-authentication,
|
||||
recovery-codes, update-email, preview.
|
||||
account2, account-api, admin-fine-grained-authz, admin-api, admin, admin2,
|
||||
docker, impersonation, openshift-integration, scripts, token-exchange,
|
||||
web-authn, client-policies, ciba, map-storage, par,
|
||||
declarative-user-profile, dynamic-scopes, client-secret-rotation,
|
||||
step-up-authentication, recovery-codes, update-email, preview.
|
||||
|
||||
Hostname:
|
||||
|
||||
|
|
|
@ -73,18 +73,18 @@ Transaction:
|
|||
Feature:
|
||||
|
||||
--features <feature> Enables a set of one or more features. Possible values are: authorization,
|
||||
account2, account-api, admin-fine-grained-authz, admin2, docker,
|
||||
impersonation, openshift-integration, scripts, token-exchange, web-authn,
|
||||
client-policies, ciba, map-storage, par, declarative-user-profile,
|
||||
dynamic-scopes, client-secret-rotation, step-up-authentication,
|
||||
recovery-codes, update-email, preview.
|
||||
account2, account-api, admin-fine-grained-authz, admin-api, admin, admin2,
|
||||
docker, impersonation, openshift-integration, scripts, token-exchange,
|
||||
web-authn, client-policies, ciba, map-storage, par,
|
||||
declarative-user-profile, dynamic-scopes, client-secret-rotation,
|
||||
step-up-authentication, recovery-codes, update-email, preview.
|
||||
--features-disabled <feature>
|
||||
Disables a set of one or more features. Possible values are: authorization,
|
||||
account2, account-api, admin-fine-grained-authz, admin2, docker,
|
||||
impersonation, openshift-integration, scripts, token-exchange, web-authn,
|
||||
client-policies, ciba, map-storage, par, declarative-user-profile,
|
||||
dynamic-scopes, client-secret-rotation, step-up-authentication,
|
||||
recovery-codes, update-email, preview.
|
||||
account2, account-api, admin-fine-grained-authz, admin-api, admin, admin2,
|
||||
docker, impersonation, openshift-integration, scripts, token-exchange,
|
||||
web-authn, client-policies, ciba, map-storage, par,
|
||||
declarative-user-profile, dynamic-scopes, client-secret-rotation,
|
||||
step-up-authentication, recovery-codes, update-email, preview.
|
||||
|
||||
Hostname:
|
||||
|
||||
|
|
|
@ -134,18 +134,18 @@ Transaction:
|
|||
Feature:
|
||||
|
||||
--features <feature> Enables a set of one or more features. Possible values are: authorization,
|
||||
account2, account-api, admin-fine-grained-authz, admin2, docker,
|
||||
impersonation, openshift-integration, scripts, token-exchange, web-authn,
|
||||
client-policies, ciba, map-storage, par, declarative-user-profile,
|
||||
dynamic-scopes, client-secret-rotation, step-up-authentication,
|
||||
recovery-codes, update-email, preview.
|
||||
account2, account-api, admin-fine-grained-authz, admin-api, admin, admin2,
|
||||
docker, impersonation, openshift-integration, scripts, token-exchange,
|
||||
web-authn, client-policies, ciba, map-storage, par,
|
||||
declarative-user-profile, dynamic-scopes, client-secret-rotation,
|
||||
step-up-authentication, recovery-codes, update-email, preview.
|
||||
--features-disabled <feature>
|
||||
Disables a set of one or more features. Possible values are: authorization,
|
||||
account2, account-api, admin-fine-grained-authz, admin2, docker,
|
||||
impersonation, openshift-integration, scripts, token-exchange, web-authn,
|
||||
client-policies, ciba, map-storage, par, declarative-user-profile,
|
||||
dynamic-scopes, client-secret-rotation, step-up-authentication,
|
||||
recovery-codes, update-email, preview.
|
||||
account2, account-api, admin-fine-grained-authz, admin-api, admin, admin2,
|
||||
docker, impersonation, openshift-integration, scripts, token-exchange,
|
||||
web-authn, client-policies, ciba, map-storage, par,
|
||||
declarative-user-profile, dynamic-scopes, client-secret-rotation,
|
||||
step-up-authentication, recovery-codes, update-email, preview.
|
||||
|
||||
Hostname:
|
||||
|
||||
|
|
|
@ -19,8 +19,8 @@ package org.keycloak.services.resources;
|
|||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.crypto.CryptoIntegration;
|
||||
import org.keycloak.common.util.BouncyIntegration;
|
||||
import org.keycloak.common.util.Resteasy;
|
||||
import org.keycloak.common.util.StringPropertyReplacer;
|
||||
import org.keycloak.config.ConfigProviderFactory;
|
||||
|
@ -101,7 +101,9 @@ public class KeycloakApplication extends Application {
|
|||
|
||||
singletons.add(new RobotsResource());
|
||||
singletons.add(new RealmsResource());
|
||||
singletons.add(new AdminRoot());
|
||||
if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_API)) {
|
||||
singletons.add(new AdminRoot());
|
||||
}
|
||||
classes.add(ThemeResource.class);
|
||||
classes.add(JsResource.class);
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.keycloak.services.resources;
|
|||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.Version;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.common.util.MimeTypeUtil;
|
||||
|
@ -176,6 +177,7 @@ public class WelcomeResource {
|
|||
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
|
||||
map.put("adminConsoleEnabled", isAdminConsoleEnabled());
|
||||
map.put("productName", Version.NAME);
|
||||
map.put("productNameFull", Version.NAME_FULL);
|
||||
|
||||
|
@ -215,6 +217,10 @@ public class WelcomeResource {
|
|||
}
|
||||
}
|
||||
|
||||
private static boolean isAdminConsoleEnabled() {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.ADMIN2) || Profile.isFeatureEnabled(Profile.Feature.ADMIN);
|
||||
}
|
||||
|
||||
private Theme getTheme() {
|
||||
try {
|
||||
return session.theme().getTheme(Theme.Type.WELCOME);
|
||||
|
|
|
@ -23,9 +23,9 @@ import javax.ws.rs.NotFoundException;
|
|||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||
import javax.ws.rs.NotAuthorizedException;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.JWSInputException;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
|
@ -97,6 +97,11 @@ public class AdminRoot {
|
|||
*/
|
||||
@GET
|
||||
public Response masterRealmAdminConsoleRedirect() {
|
||||
|
||||
if (!isAdminConsoleEnabled()) {
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
|
||||
RealmModel master = new RealmManager(session).getKeycloakAdminstrationRealm();
|
||||
return Response.status(302).location(
|
||||
session.getContext().getUri(UrlType.ADMIN).getBaseUriBuilder().path(AdminRoot.class).path(AdminRoot.class, "getAdminConsole").path("/").build(master.getName())
|
||||
|
@ -112,6 +117,11 @@ public class AdminRoot {
|
|||
@Path("index.{html:html}") // expression is actually "index.html" but this is a hack to get around jax-doclet bug
|
||||
@GET
|
||||
public Response masterRealmAdminConsoleRedirectHtml() {
|
||||
|
||||
if (!isAdminConsoleEnabled()) {
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
|
||||
return masterRealmAdminConsoleRedirect();
|
||||
}
|
||||
|
||||
|
@ -142,6 +152,11 @@ public class AdminRoot {
|
|||
*/
|
||||
@Path("{realm}/console")
|
||||
public AdminConsole getAdminConsole(final @PathParam("realm") String name) {
|
||||
|
||||
if (!isAdminConsoleEnabled()) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
RealmManager realmManager = new RealmManager(session);
|
||||
RealmModel realm = locateRealm(name, realmManager);
|
||||
AdminConsole service = new AdminConsole(realm);
|
||||
|
@ -198,6 +213,11 @@ public class AdminRoot {
|
|||
*/
|
||||
@Path("realms")
|
||||
public Object getRealmsAdmin(@Context final HttpHeaders headers) {
|
||||
|
||||
if (!isAdminApiEnabled()) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (request.getHttpMethod().equals(HttpMethod.OPTIONS)) {
|
||||
return new AdminCorsPreflightService(request);
|
||||
}
|
||||
|
@ -222,6 +242,11 @@ public class AdminRoot {
|
|||
*/
|
||||
@Path("serverinfo")
|
||||
public Object getServerInfo(@Context final HttpHeaders headers) {
|
||||
|
||||
if (!isAdminApiEnabled()) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (request.getHttpMethod().equals(HttpMethod.OPTIONS)) {
|
||||
return new AdminCorsPreflightService(request);
|
||||
}
|
||||
|
@ -277,4 +302,11 @@ public class AdminRoot {
|
|||
}
|
||||
}
|
||||
|
||||
private static boolean isAdminApiEnabled() {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.ADMIN_API);
|
||||
}
|
||||
|
||||
private static boolean isAdminConsoleEnabled() {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.ADMIN2) || Profile.isFeatureEnabled(Profile.Feature.ADMIN);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
<h1>Welcome to <strong>${productNameFull}</strong></h1>
|
||||
</div>
|
||||
<div class="row">
|
||||
<#if adminConsoleEnabled>
|
||||
<div class="col-xs-12 col-sm-4">
|
||||
<div class="card-pf h-l">
|
||||
<#if successMessage?has_content>
|
||||
|
@ -93,6 +94,7 @@
|
|||
<button id="create-button" type="submit" class="btn btn-primary">Create</button>
|
||||
</form>
|
||||
</#if>
|
||||
|
||||
<div class="welcome-primary-link">
|
||||
<h3><a href="${adminUrl}"><img src="welcome-content/user.png">Administration Console <i class="fa fa-angle-right link" aria-hidden="true"></i></a></h3>
|
||||
<div class="description">
|
||||
|
@ -101,6 +103,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</#if> <#-- adminConsoleEnabled -->
|
||||
<div class="col-xs-12 col-sm-4">
|
||||
<div class="card-pf h-l">
|
||||
<h3><a href="${properties.documentationUrl}"><img class="doc-img" src="welcome-content/admin-console.png">Documentation <i class="fa fa-angle-right link" aria-hidden="true"></i></a></h3>
|
||||
|
|
Loading…
Reference in a new issue