From 3bc17fc797228fbee81bf183426e5d4d5b0e6154 Mon Sep 17 00:00:00 2001 From: Ondrej Kotek Date: Thu, 30 Jul 2020 15:10:17 +0200 Subject: [PATCH] [WFLY-13716] Integration tests for Elytron failover-realm --- .../elytron/realm/FailoverRealmTestCase.java | 541 ++++++++++++++++++ .../realm/failover-realm-bearer-token-web.xml | 24 + .../elytron/realm/failover-realm-web.xml | 25 + .../common/elytron/FailoverRealm.java | 126 ++++ .../common/elytron/FileSystemRealm.java | 4 + .../common/elytron/PropertiesRealm.java | 19 +- .../security/common/elytron/TokenRealm.java | 4 + 7 files changed, 739 insertions(+), 4 deletions(-) create mode 100644 testsuite/integration/elytron/src/test/java/org/wildfly/test/integration/elytron/realm/FailoverRealmTestCase.java create mode 100644 testsuite/integration/elytron/src/test/java/org/wildfly/test/integration/elytron/realm/failover-realm-bearer-token-web.xml create mode 100644 testsuite/integration/elytron/src/test/java/org/wildfly/test/integration/elytron/realm/failover-realm-web.xml create mode 100644 testsuite/shared/src/main/java/org/wildfly/test/security/common/elytron/FailoverRealm.java diff --git a/testsuite/integration/elytron/src/test/java/org/wildfly/test/integration/elytron/realm/FailoverRealmTestCase.java b/testsuite/integration/elytron/src/test/java/org/wildfly/test/integration/elytron/realm/FailoverRealmTestCase.java new file mode 100644 index 000000000000..302e0c3eab01 --- /dev/null +++ b/testsuite/integration/elytron/src/test/java/org/wildfly/test/integration/elytron/realm/FailoverRealmTestCase.java @@ -0,0 +1,541 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2020, Red Hat Middleware LLC, and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.wildfly.test.integration.elytron.realm; + + +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; +import static org.jboss.as.test.integration.security.common.Utils.makeCallWithTokenAuthn; +import static org.jboss.as.test.shared.CliUtils.asAbsolutePath; +import static org.jboss.as.test.shared.CliUtils.escapePath; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; + +import java.io.File; +import java.io.PrintWriter; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Base64.Encoder; +import java.util.List; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.OperateOnDeployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.as.arquillian.api.ServerSetup; +import org.jboss.as.cli.Util; +import org.jboss.as.controller.client.ModelControllerClient; +import org.jboss.as.test.integration.security.common.Utils; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.wildfly.test.security.common.AbstractElytronSetupTask; +import org.wildfly.test.security.common.elytron.ConfigurableElement; +import org.wildfly.test.security.common.elytron.DirContext; +import org.wildfly.test.security.common.elytron.FailoverRealm; +import org.wildfly.test.security.common.elytron.FileAuditLog; +import org.wildfly.test.security.common.elytron.FileSystemRealm; +import org.wildfly.test.security.common.elytron.JdbcSecurityRealm; +import org.wildfly.test.security.common.elytron.LdapRealm; +import org.wildfly.test.security.common.elytron.MechanismConfiguration; +import org.wildfly.test.security.common.elytron.PropertiesRealm; +import org.wildfly.test.security.common.elytron.SimpleHttpAuthenticationFactory; +import org.wildfly.test.security.common.elytron.SimpleSecurityDomain; +import org.wildfly.test.security.common.elytron.TokenRealm; +import org.wildfly.test.security.common.elytron.UserWithAttributeValues; +import org.wildfly.test.undertow.common.UndertowApplicationSecurityDomain; + +/** + * Tests the {@link FailoverRealm} within the Elytron subsystem. The tests cover filesystem-realm, jdbc-realm, ldap-realm, + * properties-realm, and token-realm, trying the failover to a properties-realm when the primary (delegate) realm is unavailable + * (throws RealmUnavailableException). + * + * It's also tested whether the SecurityRealmUnavailableEvent is emitted when expected. + * + * @author Ondrej Kotek + */ +@RunWith(Arquillian.class) +@RunAsClient +@ServerSetup({FailoverRealmTestCase.ServerSetup.class}) +public class FailoverRealmTestCase { + + private static final String DEPLOYMENT_FIRST_AVAILABLE = "FailoverRealmDeployment-FirstAvailable"; + private static final String DEPLOYMENT_FILESYSTEM = "FailoverRealmDeployment-Filesystem"; + private static final String DEPLOYMENT_JDBC = "FailoverRealmDeployment-JDBC"; + private static final String DEPLOYMENT_LDAP = "FailoverRealmDeployment-LDAP"; + private static final String DEPLOYMENT_PROPERTIES = "FailoverRealmDeployment-properties"; + private static final String DEPLOYMENT_BOTH_UNAVAILABLE = "FailoverRealmDeployment-BothUnvailable"; + private static final String DEPLOYMENT_EVENT_DISABLED = "FailoverRealmDeployment-EventDisabled"; + private static final String DEPLOYMENT_BEARER_TOKEN = "FailoverRealmDeployment-BearerToken"; + private static final String INDEX_PAGE_CONTENT = "index page content"; + + private static final Encoder B64_ENCODER = Base64.getUrlEncoder().withoutPadding(); + private static final String JWT_HEADER_B64 = B64_ENCODER + .encodeToString("{\"alg\":\"none\",\"typ\":\"JWT\"}".getBytes(StandardCharsets.UTF_8)); + + private static final File AUDIT_LOG_FILE = Paths.get("target", + FailoverRealmTestCase.class.getSimpleName() + "-test-audit.log").toFile(); + + @Deployment(name = DEPLOYMENT_FIRST_AVAILABLE) + public static WebArchive deploymentForFirstRealmAvailable() { + return deployment(DEPLOYMENT_FIRST_AVAILABLE, "failover-realm-web.xml"); + } + + @Deployment(name = DEPLOYMENT_FILESYSTEM) + public static WebArchive deploymentForFilesystemRealm() { + return deployment(DEPLOYMENT_FILESYSTEM, "failover-realm-web.xml"); + } + + @Deployment(name = DEPLOYMENT_JDBC) + public static WebArchive deploymentForJdbcRealm() { + return deployment(DEPLOYMENT_JDBC, "failover-realm-web.xml"); + } + + @Deployment(name = DEPLOYMENT_LDAP) + public static WebArchive deploymentForLdapRealm() { + return deployment(DEPLOYMENT_LDAP, "failover-realm-web.xml"); + } + + @Deployment(name = DEPLOYMENT_PROPERTIES) + public static WebArchive deploymentForPropertiesRealmUnavailable() { + return deployment(DEPLOYMENT_PROPERTIES, "failover-realm-web.xml"); + } + + @Deployment(name = DEPLOYMENT_BOTH_UNAVAILABLE) + public static WebArchive deploymentForBothRealmsUnavailable() { + return deployment(DEPLOYMENT_BOTH_UNAVAILABLE, "failover-realm-web.xml"); + } + + @Deployment(name = DEPLOYMENT_EVENT_DISABLED) + public static WebArchive deploymentForEventDisabled() { + return deployment(DEPLOYMENT_EVENT_DISABLED, "failover-realm-web.xml"); + } + + @Deployment(name = DEPLOYMENT_BEARER_TOKEN) + public static WebArchive deploymentForBearerToken() { + return deployment(DEPLOYMENT_BEARER_TOKEN, "failover-realm-bearer-token-web.xml"); + } + + private static WebArchive deployment(String name, String webXml) { + final WebArchive war = ShrinkWrap.create(WebArchive.class, name + ".war"); + war.add(new StringAsset(INDEX_PAGE_CONTENT), "index.html"); + war.addAsWebInfResource(FailoverRealmTestCase.class.getPackage(), webXml, "web.xml"); + war.addAsWebInfResource(Utils.getJBossWebXmlAsset(name), "jboss-web.xml"); + return war; + } + + @Test + @OperateOnDeployment(DEPLOYMENT_FIRST_AVAILABLE) + public void testFirstRealmAvailable(@ArquillianResource URL webAppUrl) throws Exception { + testCredentialBasedRealmsNoFailover(webAppUrl); + } + + @Test + @OperateOnDeployment(DEPLOYMENT_FILESYSTEM) + public void testFilesystemRealmUnavailable(@ArquillianResource URL webAppUrl) throws Exception { + assumeFalse(Util.isWindows()); // no easy and robust way to prepare the conditions on Windows + testCredentialBasedRealmsFailover(webAppUrl); + } + + @Test + @OperateOnDeployment(DEPLOYMENT_JDBC) + public void testJdbcRealmUnavailable(@ArquillianResource URL webAppUrl) throws Exception { + testCredentialBasedRealmsFailover(webAppUrl); + } + + @Test + @OperateOnDeployment(DEPLOYMENT_LDAP) + public void testLdapRealmUnavailable(@ArquillianResource URL webAppUrl) throws Exception { + testCredentialBasedRealmsFailover(webAppUrl); + } + + @Test + @OperateOnDeployment(DEPLOYMENT_PROPERTIES) + public void testPropertiesRealmUnavailable(@ArquillianResource URL webAppUrl) throws Exception { + cleanAuditLog(); + String result = Utils.makeCallWithBasicAuthn(webAppUrl, "user1", "password1", SC_OK); + assertEquals(INDEX_PAGE_CONTENT, result); + assertTrue("SecurityRealmUnavailableEvent should be logged", isSecurityRealmUnavailableEventLogged()); + + cleanAuditLog(); + result = Utils.makeCallWithBasicAuthn(webAppUrl, "user1", "wrongPassword1", SC_UNAUTHORIZED); + assertNotEquals(INDEX_PAGE_CONTENT, result); + assertTrue("SecurityRealmUnavailableEvent should be logged", isSecurityRealmUnavailableEventLogged()); + + // no failover, just user1 is known in the first realm (its unhashed password causes the failover) + cleanAuditLog(); + result = Utils.makeCallWithBasicAuthn(webAppUrl, "non-existing-user1", "password1", SC_UNAUTHORIZED); + assertNotEquals(INDEX_PAGE_CONTENT, result); + assertFalse("SecurityRealmUnavailableEvent should not be logged", isSecurityRealmUnavailableEventLogged()); + } + + @Test + @OperateOnDeployment(DEPLOYMENT_BOTH_UNAVAILABLE) + public void testBothCredentialBasedRealmsUnavailable(@ArquillianResource URL webAppUrl) throws Exception { + cleanAuditLog(); + String result = Utils.makeCallWithBasicAuthn(webAppUrl, "user1", "password1", SC_INTERNAL_SERVER_ERROR); + assertNotEquals(INDEX_PAGE_CONTENT, result); + assertTrue("SecurityRealmUnavailableEvent should be logged", isSecurityRealmUnavailableEventLogged()); + } + + @Test + @OperateOnDeployment(DEPLOYMENT_EVENT_DISABLED) + public void testEventDisabled(@ArquillianResource URL webAppUrl) throws Exception { + // failovers happen but are not logged, like in case of no failovers + testCredentialBasedRealmsNoFailover(webAppUrl); + } + + @Test + @OperateOnDeployment(DEPLOYMENT_BEARER_TOKEN) + public void testTokenRealmUnvailable(@ArquillianResource URL webAppUrl) throws Exception { + cleanAuditLog(); + String result = makeCallWithTokenAuthn(webAppUrl, createJwtToken("userA", "issuer.wildfly.org"), SC_OK); + assertEquals(INDEX_PAGE_CONTENT, result); + assertTrue("SecurityRealmUnavailableEvent should be logged", isSecurityRealmUnavailableEventLogged()); + + cleanAuditLog(); + result = makeCallWithTokenAuthn(webAppUrl, createJwtToken("userC", "unknown_issuer"), SC_UNAUTHORIZED); + assertNotEquals(INDEX_PAGE_CONTENT, result); + assertTrue("SecurityRealmUnavailableEvent should be logged", isSecurityRealmUnavailableEventLogged()); + } + + private void testCredentialBasedRealmsFailover(URL webAppUrl) throws Exception { + cleanAuditLog(); + String result = Utils.makeCallWithBasicAuthn(webAppUrl, "user1", "password1", SC_OK); + assertEquals(INDEX_PAGE_CONTENT, result); + assertTrue("SecurityRealmUnavailableEvent should be logged", isSecurityRealmUnavailableEventLogged()); + + cleanAuditLog(); + result = Utils.makeCallWithBasicAuthn(webAppUrl, "user1", "wrongPassword1", SC_UNAUTHORIZED); + assertNotEquals(INDEX_PAGE_CONTENT, result); + assertTrue("SecurityRealmUnavailableEvent should be logged", isSecurityRealmUnavailableEventLogged()); + + cleanAuditLog(); + result = Utils.makeCallWithBasicAuthn(webAppUrl, "non-existing-user1", "password1", SC_UNAUTHORIZED); + assertNotEquals(INDEX_PAGE_CONTENT, result); + assertTrue("SecurityRealmUnavailableEvent should be logged", isSecurityRealmUnavailableEventLogged()); + } + + private void testCredentialBasedRealmsNoFailover(URL webAppUrl) throws Exception { + cleanAuditLog(); + String result = Utils.makeCallWithBasicAuthn(webAppUrl, "user1", "password1", SC_OK); + assertEquals(INDEX_PAGE_CONTENT, result); + assertFalse("SecurityRealmUnavailableEvent should not be logged", isSecurityRealmUnavailableEventLogged()); + + cleanAuditLog(); + result = Utils.makeCallWithBasicAuthn(webAppUrl, "user1", "wrongPassword1", SC_UNAUTHORIZED); + assertNotEquals(INDEX_PAGE_CONTENT, result); + assertFalse("SecurityRealmUnavailableEvent should not be logged", isSecurityRealmUnavailableEventLogged()); + + cleanAuditLog(); + result = Utils.makeCallWithBasicAuthn(webAppUrl, "non-existing-user1", "password1", SC_UNAUTHORIZED); + assertNotEquals(INDEX_PAGE_CONTENT, result); + assertFalse("SecurityRealmUnavailableEvent should not be logged", isSecurityRealmUnavailableEventLogged()); + } + + private static boolean isSecurityRealmUnavailableEventLogged() throws Exception { + List lines = Files.readAllLines(AUDIT_LOG_FILE.toPath(), StandardCharsets.UTF_8); + for (String line : lines) { + if (line.contains("SecurityRealmUnavailableEvent")) { + return true; + } + } + return false; + } + + private static void cleanAuditLog() throws Exception { + try (PrintWriter writer = new PrintWriter(AUDIT_LOG_FILE)) { + writer.print(""); + } + } + + private static String createJwtToken(String userName, String issuer) { + String jwtPayload = String.format("{" + + "\"active\": true," + + "\"iss\": \"%1$s\"," + + "\"sub\": \"elytron@wildfly.org\"," + + "\"exp\": 2051222399," + + "\"aud\": \"%1$s\"," + + "\"groups\": [\"%2$s\"]" + + "}", issuer, userName); + return JWT_HEADER_B64 + "." + B64_ENCODER.encodeToString(jwtPayload.getBytes(StandardCharsets.UTF_8)) + "."; + } + + static class ServerSetup extends AbstractElytronSetupTask { + File unavailableFileSystemRealmDir; + + @Override + protected ConfigurableElement[] getConfigurableElements() { + + ArrayList configurableElements = new ArrayList<>(); + + // this audit log is checked for presence of SecurityRealmUnavailableEvent + configurableElements.add(FileAuditLog.builder() + .withName("audit_log_for_failover_realm") + .withPath(asAbsolutePath(AUDIT_LOG_FILE)) + .build()); + + // this properties realm is used as a failover realm to test the other credential based failovers + configurableElements.add(PropertiesRealm.builder() + .withName("properties_realm_1") + .withUser(UserWithAttributeValues.builder() + .withName("user1") + .withPassword("password1") + .withValues("user") + .build()) + .build()); + + // filesystem realm + FileSystemRealm unavailableFileSystemRealm = FileSystemRealm.builder() + .withName("unavailable_filesystem_realm") + .build(); + String unavailableFileSystemRealmPath = unavailableFileSystemRealm.getPath().getPath(); + // prevent accessing the temporary directory for the realm + unavailableFileSystemRealmDir = new File(escapePath(unavailableFileSystemRealmPath)); + unavailableFileSystemRealmDir.setExecutable(false, false); + configurableElements.add(unavailableFileSystemRealm); + configurableElements.add(FailoverRealm.builder("failover_realm_filesystem") + .withDelegateRealm("unavailable_filesystem_realm") + .withFailoverRealm("properties_realm_1") + .build()); + configurableElements.add(SimpleSecurityDomain.builder() + .withName("failover_realm_domain_filesystem") + .withDefaultRealm("failover_realm_filesystem") + .withPermissionMapper("default-permission-mapper") + .withSecurityEventListener("audit_log_for_failover_realm") + .withRealms(SimpleSecurityDomain.SecurityDomainRealm.builder() + .withRealm("failover_realm_filesystem") + .withRoleDecoder("groups-to-roles") + .build()) + .build()); + configurableElements.add(UndertowApplicationSecurityDomain.builder() + .withName(DEPLOYMENT_FILESYSTEM) + .withSecurityDomain("failover_realm_domain_filesystem") + .build()); + + // JDBC realm + configurableElements.add(JdbcSecurityRealm.builder("unavailable_jdbc_realm") + .withPrincipalQuery("ExampleDS", "invalid SQL") + .withPasswordMapper("clear-password-mapper", null, 1, -1, -1) + .build() + .build()); + configurableElements.add(FailoverRealm.builder("failover_realm_jdbc") + .withDelegateRealm("unavailable_jdbc_realm") + .withFailoverRealm("properties_realm_1") + .build()); + configurableElements.add(SimpleSecurityDomain.builder() + .withName("failover_realm_domain_jdbc") + .withDefaultRealm("failover_realm_jdbc") + .withPermissionMapper("default-permission-mapper") + .withSecurityEventListener("audit_log_for_failover_realm") + .withRealms(SimpleSecurityDomain.SecurityDomainRealm.builder() + .withRealm("failover_realm_jdbc") + .withRoleDecoder("groups-to-roles") + .build()) + .build()); + configurableElements.add(UndertowApplicationSecurityDomain.builder() + .withName(DEPLOYMENT_JDBC) + .withSecurityDomain("failover_realm_domain_jdbc") + .build()); + + // LDAP realm + configurableElements.add(DirContext.builder("unavailable_dir_context") + .withUrl("invalid_url") + .build()); + configurableElements.add(LdapRealm.builder("unavailable_ldap_realm") + .withDirContext("unavailable_dir_context") + .withIdentityMapping(LdapRealm.identityMappingBuilder() + .withRdnIdentifier("invalid") + .withUserPasswordMapper(new LdapRealm.UserPasswordMapperBuilder() + .withFrom("invalid") + .build()) + .build()) + .build()); + configurableElements.add(FailoverRealm.builder("failover_realm_ldap") + .withDelegateRealm("unavailable_ldap_realm") + .withFailoverRealm("properties_realm_1") + .build()); + configurableElements.add(SimpleSecurityDomain.builder() + .withName("failover_realm_domain_ldap") + .withDefaultRealm("failover_realm_ldap") + .withPermissionMapper("default-permission-mapper") + .withSecurityEventListener("audit_log_for_failover_realm") + .withRealms(SimpleSecurityDomain.SecurityDomainRealm.builder() + .withRealm("failover_realm_ldap") + .withRoleDecoder("groups-to-roles") + .build()) + .build()); + configurableElements.add(UndertowApplicationSecurityDomain.builder() + .withName(DEPLOYMENT_LDAP) + .withSecurityDomain("failover_realm_domain_ldap") + .build()); + + // properties realm + configurableElements.add(PropertiesRealm.builder() + .withName("unavailable_properties_realm") + .withPlainText(false) + .withUser(UserWithAttributeValues.builder() + .withName("user1") + .withPassword("password1") // this is expected to be hashed but it isn't + .withValues("user") + .build()) + .build()); + configurableElements.add(FailoverRealm.builder("failover_realm_properties") + .withDelegateRealm("unavailable_properties_realm") + .withFailoverRealm("properties_realm_1") + .build()); + configurableElements.add(SimpleSecurityDomain.builder() + .withName("failover_realm_domain_properties") + .withDefaultRealm("failover_realm_properties") + .withPermissionMapper("default-permission-mapper") + .withSecurityEventListener("audit_log_for_failover_realm") + .withRealms(SimpleSecurityDomain.SecurityDomainRealm.builder() + .withRealm("failover_realm_properties") + .withRoleDecoder("groups-to-roles") + .build()) + .build()); + configurableElements.add(UndertowApplicationSecurityDomain.builder() + .withName(DEPLOYMENT_PROPERTIES) + .withSecurityDomain("failover_realm_domain_properties") + .build()); + + // the delegate realm is available, the failover realm is not (LDAP) + configurableElements.add(FailoverRealm.builder("failover_realm_first_available") + .withDelegateRealm("properties_realm_1") + .withFailoverRealm("unavailable_ldap_realm") + .build()); + configurableElements.add(SimpleSecurityDomain.builder() + .withName("failover_realm_domain_first_available") + .withDefaultRealm("failover_realm_first_available") + .withPermissionMapper("default-permission-mapper") + .withSecurityEventListener("audit_log_for_failover_realm") + .withRealms(SimpleSecurityDomain.SecurityDomainRealm.builder() + .withRealm("failover_realm_first_available") + .withRoleDecoder("groups-to-roles") + .build()) + .build()); + configurableElements.add(UndertowApplicationSecurityDomain.builder() + .withName(DEPLOYMENT_FIRST_AVAILABLE) + .withSecurityDomain("failover_realm_domain_first_available") + .build()); + + // both the credentaial based realms are not available + configurableElements.add(FailoverRealm.builder("failover_realm_both_unavailable") + .withDelegateRealm("unavailable_jdbc_realm") + .withFailoverRealm("unavailable_ldap_realm") + .build()); + configurableElements.add(SimpleSecurityDomain.builder() + .withName("failover_realm_domain_both_unavailable") + .withDefaultRealm("failover_realm_both_unavailable") + .withPermissionMapper("default-permission-mapper") + .withSecurityEventListener("audit_log_for_failover_realm") + .withRealms(SimpleSecurityDomain.SecurityDomainRealm.builder() + .withRealm("failover_realm_both_unavailable") + .withRoleDecoder("groups-to-roles") + .build()) + .build()); + configurableElements.add(UndertowApplicationSecurityDomain.builder() + .withName(DEPLOYMENT_BOTH_UNAVAILABLE) + .withSecurityDomain("failover_realm_domain_both_unavailable") + .build()); + + // failover with SecurityRealmUnavailableEvent emitting disabled + configurableElements.add(FailoverRealm.builder("failover_realm_event_disabled") + .withDelegateRealm("unavailable_jdbc_realm") + .withFailoverRealm("properties_realm_1") + .withEmitEvents(false) + .build()); + configurableElements.add(SimpleSecurityDomain.builder() + .withName("failover_realm_domain_event_disabled") + .withDefaultRealm("failover_realm_event_disabled") + .withPermissionMapper("default-permission-mapper") + .withSecurityEventListener("audit_log_for_failover_realm") + .withRealms(SimpleSecurityDomain.SecurityDomainRealm.builder() + .withRealm("failover_realm_event_disabled") + .withRoleDecoder("groups-to-roles") + .build()) + .build()); + configurableElements.add(UndertowApplicationSecurityDomain.builder() + .withName(DEPLOYMENT_EVENT_DISABLED) + .withSecurityDomain("failover_realm_domain_event_disabled") + .build()); + + // token realm + configurableElements.add(TokenRealm.builder("token_realm") + .withJwt(TokenRealm.jwtBuilder().withIssuer("issuer.wildfly.org").build()) + .withPrincipalClaim("aud") + .build()); + configurableElements.add(TokenRealm.builder("unavailable_token_realm") + .withOauth2Introspection(TokenRealm.oauth2IntrospectionBuilder() + .withClientId("failover_realm_client") + .withClientSecret("failover_realm_client_secret") + .withIntrospectionUrl("http://invalid") + .build()) + .withPrincipalClaim("aud") + .build()); + configurableElements.add(FailoverRealm.builder("failover_realm_bearer_token") + .withDelegateRealm("unavailable_token_realm") + .withFailoverRealm("token_realm") + .build()); + configurableElements.add(SimpleSecurityDomain.builder() + .withName("failover_realm_domain_bearer_token") + .withDefaultRealm("failover_realm_bearer_token") + .withPermissionMapper("default-permission-mapper") + .withSecurityEventListener("audit_log_for_failover_realm") + .withRealms(SimpleSecurityDomain.SecurityDomainRealm.builder() + .withRealm("failover_realm_bearer_token") + .withRoleDecoder("groups-to-roles") + .build()) + .build()); + configurableElements.add(SimpleHttpAuthenticationFactory.builder() + .withName(DEPLOYMENT_BEARER_TOKEN) + .withHttpServerMechanismFactory("global") + .withSecurityDomain("failover_realm_domain_bearer_token") + .addMechanismConfiguration(MechanismConfiguration.builder() + .withMechanismName("BEARER_TOKEN") + .build()) + .build()); + configurableElements.add(UndertowApplicationSecurityDomain.builder() + .withName(DEPLOYMENT_BEARER_TOKEN) + .httpAuthenticationFactory(DEPLOYMENT_BEARER_TOKEN) + .build()); + + return configurableElements.toArray(new ConfigurableElement[configurableElements.size()]); + } + + @Override + protected void tearDown(ModelControllerClient modelControllerClient) throws Exception { + super.tearDown(modelControllerClient); + Files.deleteIfExists(AUDIT_LOG_FILE.toPath()); + } + } +} diff --git a/testsuite/integration/elytron/src/test/java/org/wildfly/test/integration/elytron/realm/failover-realm-bearer-token-web.xml b/testsuite/integration/elytron/src/test/java/org/wildfly/test/integration/elytron/realm/failover-realm-bearer-token-web.xml new file mode 100644 index 000000000000..a0feb6ecd222 --- /dev/null +++ b/testsuite/integration/elytron/src/test/java/org/wildfly/test/integration/elytron/realm/failover-realm-bearer-token-web.xml @@ -0,0 +1,24 @@ + + + + + BEARER_TOKEN + + + + + secured-area + /* + + + * + + + + + * + + diff --git a/testsuite/integration/elytron/src/test/java/org/wildfly/test/integration/elytron/realm/failover-realm-web.xml b/testsuite/integration/elytron/src/test/java/org/wildfly/test/integration/elytron/realm/failover-realm-web.xml new file mode 100644 index 000000000000..271e8bf0dc40 --- /dev/null +++ b/testsuite/integration/elytron/src/test/java/org/wildfly/test/integration/elytron/realm/failover-realm-web.xml @@ -0,0 +1,25 @@ + + + + + BASIC + Test realm + + + + + secured-area + /* + + + * + + + + + * + + diff --git a/testsuite/shared/src/main/java/org/wildfly/test/security/common/elytron/FailoverRealm.java b/testsuite/shared/src/main/java/org/wildfly/test/security/common/elytron/FailoverRealm.java new file mode 100644 index 000000000000..692839709799 --- /dev/null +++ b/testsuite/shared/src/main/java/org/wildfly/test/security/common/elytron/FailoverRealm.java @@ -0,0 +1,126 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2020, Red Hat Middleware LLC, and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.wildfly.test.security.common.elytron; + +import org.jboss.as.controller.PathAddress; +import org.jboss.as.controller.PathElement; +import org.jboss.as.controller.client.ModelControllerClient; +import org.jboss.as.controller.operations.common.Util; +import org.jboss.as.test.integration.management.util.CLIWrapper; +import org.jboss.as.test.integration.security.common.Utils; +import org.jboss.dmr.ModelNode; + +/** + * A {@link ConfigurableElement} to define the failover-realm resource within the Elytron subsystem. + * + * @author Ondrej Kotek + */ +public class FailoverRealm implements SecurityRealm { + + private final PathAddress address; + private final String name; + private final String delegateRealm; + private final String failoverRealm; + private final Boolean emitEvents; + + FailoverRealm(final String name, final Builder builder) { + this.name = name; + this.address = PathAddress.pathAddress(PathElement.pathElement("subsystem", "elytron"), PathElement.pathElement("failover-realm", name)); + this.delegateRealm = builder.delegateRealm; + this.failoverRealm = builder.failoverRealm; + this.emitEvents = builder.emitEvents; + } + + @Override + public String getName() { + return name; + } + + public ModelNode getAddOperation() { + ModelNode addOperation = Util.createAddOperation(address); + if (this.delegateRealm != null) { + addOperation.get("delegate-realm").set(this.delegateRealm); + } + if (this.failoverRealm != null) { + addOperation.get("failover-realm").set(this.failoverRealm); + } + if (this.emitEvents != null) { + addOperation.get("emit-events").set(this.emitEvents); + } + + return addOperation; + } + + public ModelNode getRemoveOperation() { + return Util.createRemoveOperation(address); + } + + @Override + public void create(ModelControllerClient client, CLIWrapper cli) throws Exception { + Utils.applyUpdate(getAddOperation(), client); + } + + @Override + public void remove(ModelControllerClient client, CLIWrapper cli) throws Exception { + Utils.applyUpdate(getRemoveOperation(), client); + } + + public static Builder builder(final String name) { + return new Builder(name); + } + + public static class Builder { + + private final String name; + private String delegateRealm; + private String failoverRealm; + private Boolean emitEvents; + + Builder(final String name) { + this.name = name; + } + + public Builder withDelegateRealm(final String realm) { + this.delegateRealm = realm; + + return this; + } + + public Builder withFailoverRealm(final String realm) { + this.failoverRealm = realm; + + return this; + } + + public Builder withEmitEvents(final Boolean emitEvents) { + this.emitEvents = emitEvents; + + return this; + } + + public SecurityRealm build() { + return new FailoverRealm(name, this); + } + + } + +} diff --git a/testsuite/shared/src/main/java/org/wildfly/test/security/common/elytron/FileSystemRealm.java b/testsuite/shared/src/main/java/org/wildfly/test/security/common/elytron/FileSystemRealm.java index f30f45efc823..2f5dd96c6408 100644 --- a/testsuite/shared/src/main/java/org/wildfly/test/security/common/elytron/FileSystemRealm.java +++ b/testsuite/shared/src/main/java/org/wildfly/test/security/common/elytron/FileSystemRealm.java @@ -74,6 +74,10 @@ public void remove(CLIWrapper cli) throws Exception { FileUtils.deleteQuietly(tempFolder); } + public Path getPath() { + return this.path; + } + /** * Creates builder to build {@link FileSystemRealm}. * diff --git a/testsuite/shared/src/main/java/org/wildfly/test/security/common/elytron/PropertiesRealm.java b/testsuite/shared/src/main/java/org/wildfly/test/security/common/elytron/PropertiesRealm.java index f7155be7d736..3150818d4813 100644 --- a/testsuite/shared/src/main/java/org/wildfly/test/security/common/elytron/PropertiesRealm.java +++ b/testsuite/shared/src/main/java/org/wildfly/test/security/common/elytron/PropertiesRealm.java @@ -38,11 +38,13 @@ public class PropertiesRealm extends AbstractUserAttributeValuesCapableElement i private static final Logger LOGGER = Logger.getLogger(PropertiesRealm.class); private final String groupsAttribute; + private final boolean plainText; // true by default private File tempFolder; private PropertiesRealm(Builder builder) { super(builder); this.groupsAttribute = builder.groupsAttribute; + this.plainText = builder.plainText; } @Override @@ -61,8 +63,8 @@ public void create(CLIWrapper cli) throws Exception { // groups-properties={path=/tmp/groups.properties}, groups-attribute="groups") final String groupsAttrStr = groupsAttribute == null ? "" : String.format(", groups-attribute=\"%s\"", groupsAttribute); cli.sendLine(String.format( - "/subsystem=elytron/properties-realm=%s:add(users-properties={path=\"%s\", plain-text=true}, groups-properties={path=\"%s\"}%s)", - name, asAbsolutePath(usersFile), asAbsolutePath(rolesFile), groupsAttrStr)); + "/subsystem=elytron/properties-realm=%s:add(users-properties={path=\"%s\", plain-text=%b}, groups-properties={path=\"%s\"}%s)", + name, asAbsolutePath(usersFile), plainText, asAbsolutePath(rolesFile), groupsAttrStr)); } @Override @@ -85,8 +87,11 @@ private File writeProperties(Properties properties, String fileName) throws IOEx File result = new File(tempFolder, fileName); LOGGER.debugv("Creating property file {0}", result); try (FileOutputStream fos = new FileOutputStream(result)) { - // comment $REALM_NAME is just a workaround for https://issues.jboss.org/browse/WFLY-7104 - properties.store(fos, "$REALM_NAME=" + name + "$"); + if (plainText) { + properties.store(fos, null); + } else { + properties.store(fos, "$REALM_NAME=" + name + "$"); + } } return result; } @@ -96,6 +101,7 @@ private File writeProperties(Properties properties, String fileName) throws IOEx */ public static final class Builder extends AbstractUserAttributeValuesCapableElement.Builder { private String groupsAttribute; + private boolean plainText = true; private Builder() { } @@ -105,6 +111,11 @@ public Builder withGroupsAttribute(String groupsAttribute) { return this; } + public Builder withPlainText(boolean plainText) { + this.plainText = plainText; + return this; + } + public PropertiesRealm build() { return new PropertiesRealm(this); } diff --git a/testsuite/shared/src/main/java/org/wildfly/test/security/common/elytron/TokenRealm.java b/testsuite/shared/src/main/java/org/wildfly/test/security/common/elytron/TokenRealm.java index 80671df207cc..cd047ef24cc1 100644 --- a/testsuite/shared/src/main/java/org/wildfly/test/security/common/elytron/TokenRealm.java +++ b/testsuite/shared/src/main/java/org/wildfly/test/security/common/elytron/TokenRealm.java @@ -135,6 +135,10 @@ public static JwtBuilder jwtBuilder() { return new JwtBuilder(); } + public static Oauth2IntrospectionBuilder oauth2IntrospectionBuilder() { + return new Oauth2IntrospectionBuilder(); + } + public static final class Builder { private final String name;