Mercurial > hg > thermostat-ng > web-gateway
changeset 199:0fe5182f3b1b
Add realms authorization implementation
This patch adds the realms authorization system using Keycloak. The jvm-gc service is updated to follow the realms authorization specification. See the review thread and the link below for more information.
http://icedtea.classpath.org/pipermail/thermostat/2017-June/023560.html
Reviewed-by: jerboaa
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-June/023674.html
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-July/023985.html
line wrap: on
line diff
--- a/common/core/pom.xml Mon Jul 17 11:42:24 2017 +0200 +++ b/common/core/pom.xml Mon Jul 17 11:22:45 2017 -0400 @@ -67,6 +67,13 @@ <version>${javax-rs-api.version}</version> </dependency> + <!-- Keycloak dependencies --> + <dependency> + <groupId>org.keycloak</groupId> + <artifactId>keycloak-core</artifactId> + <version>${keycloak.version}</version> + </dependency> + <!-- test scoped deps --> <dependency> <groupId>junit</groupId>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/core/src/main/java/com/redhat/thermostat/gateway/common/core/auth/keycloak/Action.java Mon Jul 17 11:22:45 2017 -0400 @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.gateway.common.core.auth.keycloak; + +public class Action { + public static final String READ = "r"; + public static final String WRITE = "w"; + public static final String UPDATE = "u"; + public static final String DELETE = "d"; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/core/src/main/java/com/redhat/thermostat/gateway/common/core/auth/keycloak/RealmAuthorizer.java Mon Jul 17 11:22:45 2017 -0400 @@ -0,0 +1,176 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.gateway.common.core.auth.keycloak; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +import org.keycloak.KeycloakSecurityContext; + +public class RealmAuthorizer { + + public static final String REALMS_HEADER = "X-Thermostat-Realms"; + + private final Set<Role> clientRoles; + private final RoleFactory roleFactory = new RoleFactory(); + + public RealmAuthorizer(HttpServletRequest httpServletRequest) throws ServletException { + Set<Role> roles = buildClientRoles(httpServletRequest); + this.clientRoles = Collections.unmodifiableSet(roles); + } + + public boolean readable() { + return checkActionExists(Action.READ); + } + + public boolean writable() { + return checkActionExists(Action.WRITE); + } + + public boolean updatable() { + return checkActionExists(Action.UPDATE); + } + + public boolean deletable() { + return checkActionExists(Action.DELETE); + } + + public boolean checkActionExists(String action) { + for (Role role : clientRoles) { + if (role.getActions().contains(action)) { + return true; + } + } + return false; + } + + public Set<String> getReadableRealms() { + return getRealmsWithAction(Action.READ); + } + + public Set<String> getWritableRealms() { + return getRealmsWithAction(Action.WRITE); + } + public Set<String> getUpdatableRealms() { + return getRealmsWithAction(Action.UPDATE); + } + public Set<String> getDeletableRealms() { + return getRealmsWithAction(Action.DELETE); + } + + public Set<String> getRealmsWithAction(String action) { + Set<String> realms = new HashSet<>(); + for (Role role : clientRoles) { + if (role.getActions().contains(action)) { + realms.add(role.getRealm()); + } + } + return Collections.unmodifiableSet(realms); + } + + protected Set<Role> getAllRoles() { + return clientRoles; + } + + private Set<Role> buildClientRoles(HttpServletRequest httpServletRequest) throws ServletException { + Set<Role> keycloakRoles = buildKeycloakRoles(httpServletRequest); + + String realmsHeader = httpServletRequest.getHeader(REALMS_HEADER); + if (realmsHeader != null) { + return buildClientPreferredRoles(keycloakRoles, realmsHeader); + } + + return keycloakRoles; + } + + /** + * @return the set of roles from the Keycloak security token + */ + private Set<Role> buildKeycloakRoles(HttpServletRequest httpServletRequest) { + Set<Role> keycloakRoles = new HashSet<>(); + + KeycloakSecurityContext keycloakSecurityContext = (KeycloakSecurityContext) httpServletRequest + .getAttribute(KeycloakSecurityContext.class.getName()); + + for (String role : keycloakSecurityContext.getToken().getRealmAccess().getRoles()) { + if (roleFactory.isValidRole(role)) { + keycloakRoles.add(roleFactory.buildRole(role)); + } + } + + return keycloakRoles; + } + + /** + * Builds a set of roles based on a clients preferred set, provided in a comma separated realms header string + * @param trustedRoles : The trusted set of roles that the client has + * @param realmsHeader : The REALMS_HEADER value as a string + * @return The set of roles that the client has selected + * @throws ServletException If realms header contains realms the client does not have or no valid realms + */ + private Set<Role> buildClientPreferredRoles(Set<Role> trustedRoles, String realmsHeader) throws ServletException { + realmsHeader = realmsHeader.replaceAll("\\s+", ""); + Set<String> preferredRealms = new HashSet<>(Arrays.asList(realmsHeader.split(","))); + Set<Role> selectedRoles = new HashSet<>(); + + for (String preferredRealm : preferredRealms) { + boolean found = false; + for (Role role : trustedRoles) { + if (role.getRealm().equals(preferredRealm)) { + selectedRoles.add(role); + found = true; + } + } + if (!found) { + throw new ServletException("Not authorized to access preferred realms."); + } + } + + if (selectedRoles.size() > 0) { + return selectedRoles; + } else { + throw new ServletException("No realms selected"); + } + } + + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/core/src/main/java/com/redhat/thermostat/gateway/common/core/auth/keycloak/Role.java Mon Jul 17 11:22:45 2017 -0400 @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.gateway.common.core.auth.keycloak; + +public class Role { + + public static final String ROLE_DELIMITER = "-"; + public static final String[] RESTRICTED_CHARACTERS = new String[]{","}; + + private final String actions; + private final String realm; + + public Role(String actions, String realm) { + this.actions = actions; + this.realm = realm; + } + + public String getActions() { + return this.actions; + } + + public String getRealm() { + return this.realm; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/core/src/main/java/com/redhat/thermostat/gateway/common/core/auth/keycloak/RoleFactory.java Mon Jul 17 11:22:45 2017 -0400 @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.gateway.common.core.auth.keycloak; + +public class RoleFactory { + + public boolean isValidRole(String role) { + if (!role.contains(Role.ROLE_DELIMITER)) { + return false; + } + for (String restrictedCharacter : Role.RESTRICTED_CHARACTERS) { + if (role.contains(restrictedCharacter)) { + return false; + } + } + + int index = role.indexOf(Role.ROLE_DELIMITER); + + // Make sure there are characters before and after the role delimiter + return index > 0 && index < role.length() - 1; + } + + public Role buildRole(String role) { + + int index = role.indexOf(Role.ROLE_DELIMITER); + String actions = role.substring(0, index); + String realm = role.substring(index + 1); + + return new Role(actions, realm); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/core/src/test/java/com/redhat/thermostat/gateway/common/core/auth/keycloak/RealmAuthorizerTest.java Mon Jul 17 11:22:45 2017 -0400 @@ -0,0 +1,280 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.gateway.common.core.auth.keycloak; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +import org.junit.Before; +import org.junit.Test; +import org.keycloak.KeycloakSecurityContext; +import org.keycloak.representations.AccessToken; + +public class RealmAuthorizerTest { + + HttpServletRequest request; + AccessToken.Access access; + + @Before + public void setup() { + request = mock(HttpServletRequest.class); + KeycloakSecurityContext keycloakSecurityContext = mock(KeycloakSecurityContext.class); + when(request.getAttribute(eq(KeycloakSecurityContext.class.getName()))).thenReturn(keycloakSecurityContext); + + AccessToken accessToken = mock(AccessToken.class); + when(keycloakSecurityContext.getToken()).thenReturn(accessToken); + + access = mock(AccessToken.Access.class); + when(accessToken.getRealmAccess()).thenReturn(access); + } + + @Test + public void testBuildSingleRealm() throws ServletException { + String[] roles = new String[]{"a-realm"}; + when(access.getRoles()).thenReturn(new HashSet<>(Arrays.asList(roles))); + + RealmAuthorizer realmAuthorizer = new RealmAuthorizer(request); + + Set<String> realms = realmAuthorizer.getRealmsWithAction("a"); + assertTrue(realms.contains("realm")); + } + + @Test + public void testBuildMultipleRealms() throws ServletException { + String[] roles = new String[]{"a-realm", "b-another"}; + when(access.getRoles()).thenReturn(new HashSet<>(Arrays.asList(roles))); + + RealmAuthorizer realmAuthorizer = new RealmAuthorizer(request); + + Set<String> realms = realmAuthorizer.getRealmsWithAction("a"); + assertTrue(realms.contains("realm")); + assertFalse(realms.contains("another")); + + realms = realmAuthorizer.getRealmsWithAction("b"); + assertTrue(realms.contains("another")); + assertFalse(realms.contains("realm")); + } + + @Test + public void testBuildMultipleRealmsSameAction() throws ServletException { + String[] roles = new String[]{"r-realm", "w-realm"}; + when(access.getRoles()).thenReturn(new HashSet<>(Arrays.asList(roles))); + + RealmAuthorizer realmAuthorizer = new RealmAuthorizer(request); + + + Set<String> realms = realmAuthorizer.getRealmsWithAction("r"); + assertTrue(realms.contains("realm")); + + realms = realmAuthorizer.getRealmsWithAction("w"); + assertTrue(realms.contains("realm")); + } + + @Test + public void testBuildRoleWithoutRealm() throws ServletException { + String[] roles = new String[]{"a-"}; + when(access.getRoles()).thenReturn(new HashSet<>(Arrays.asList(roles))); + + RealmAuthorizer realmAuthorizer = new RealmAuthorizer(request); + Set<Role> realms = realmAuthorizer.getAllRoles(); + assertTrue(realms.isEmpty()); + } + + @Test + public void testBuildInvalidRoleWithoutAction() throws ServletException { + String[] roles = new String[]{"-realm"}; + when(access.getRoles()).thenReturn(new HashSet<>(Arrays.asList(roles))); + + RealmAuthorizer realmAuthorizer = new RealmAuthorizer(request); + Set<Role> realms = realmAuthorizer.getAllRoles(); + assertTrue(realms.isEmpty()); + } + + + private void setupRealms() { + String[] roles = new String[]{"r-read", "w-write", "d-delete", "u-update"}; + when(access.getRoles()).thenReturn(new HashSet<>(Arrays.asList(roles))); + } + + @Test + public void testReadable() throws ServletException { + setupRealms(); + + RealmAuthorizer realmAuthorizer = new RealmAuthorizer(request); + assertTrue(realmAuthorizer.readable()); + + Set<String> realms = realmAuthorizer.getReadableRealms(); + assertEquals(1, realms.size()); + assertTrue(realms.contains("read")); + } + + @Test + public void testWritable() throws ServletException { + setupRealms(); + + RealmAuthorizer realmAuthorizer = new RealmAuthorizer(request); + assertTrue(realmAuthorizer.writable()); + + Set<String> realms = realmAuthorizer.getWritableRealms(); + assertEquals(1, realms.size()); + assertTrue(realms.contains("write")); + } + + @Test + public void testUpdatable() throws ServletException { + setupRealms(); + + RealmAuthorizer realmAuthorizer = new RealmAuthorizer(request); + assertTrue(realmAuthorizer.updatable()); + + Set<String> realms = realmAuthorizer.getUpdatableRealms(); + assertEquals(1, realms.size()); + assertTrue(realms.contains("update")); + } + + @Test + public void testDeletable() throws ServletException { + setupRealms(); + + RealmAuthorizer realmAuthorizer = new RealmAuthorizer(request); + assertTrue(realmAuthorizer.deletable()); + + Set<String> realms = realmAuthorizer.getDeletableRealms(); + assertEquals(1, realms.size()); + assertTrue(realms.contains("delete")); + } + + @Test + public void testNotReadable() throws ServletException { + String[] roles = new String[]{"w-write", "d-delete", "u-update"}; + when(access.getRoles()).thenReturn(new HashSet<>(Arrays.asList(roles))); + + RealmAuthorizer realmAuthorizer = new RealmAuthorizer(request); + assertFalse(realmAuthorizer.readable()); + + Set<String> realms = realmAuthorizer.getReadableRealms(); + assertEquals(0, realms.size()); + } + + @Test + public void testNotWritable() throws ServletException { + String[] roles = new String[]{"r-read", "d-delete", "u-update"}; + when(access.getRoles()).thenReturn(new HashSet<>(Arrays.asList(roles))); + + RealmAuthorizer realmAuthorizer = new RealmAuthorizer(request); + assertFalse(realmAuthorizer.writable()); + + Set<String> realms = realmAuthorizer.getWritableRealms(); + assertEquals(0, realms.size()); + } + + @Test + public void testNotUpdatable() throws ServletException { + String[] roles = new String[]{"w-write", "d-delete", "r-read"}; + when(access.getRoles()).thenReturn(new HashSet<>(Arrays.asList(roles))); + + RealmAuthorizer realmAuthorizer = new RealmAuthorizer(request); + assertFalse(realmAuthorizer.updatable()); + + Set<String> realms = realmAuthorizer.getUpdatableRealms(); + assertEquals(0, realms.size()); + } + + @Test + public void testNotDeletable() throws ServletException { + String[] roles = new String[]{"w-write", "r-read", "u-update"}; + when(access.getRoles()).thenReturn(new HashSet<>(Arrays.asList(roles))); + + RealmAuthorizer realmAuthorizer = new RealmAuthorizer(request); + assertFalse(realmAuthorizer.deletable()); + + Set<String> realms = realmAuthorizer.getDeletableRealms(); + assertEquals(0, realms.size()); + } + + @Test + public void testRealmsHeaderSubset() throws ServletException { + String[] roles = new String[]{"w-write", "r-read", "u-update"}; + when(access.getRoles()).thenReturn(new HashSet<>(Arrays.asList(roles))); + + when(request.getHeader(eq("X-Thermostat-Realms"))).thenReturn("read,update"); + + RealmAuthorizer realmAuthorizer = new RealmAuthorizer(request); + assertEquals(1, realmAuthorizer.getReadableRealms().size()); + assertEquals(1, realmAuthorizer.getUpdatableRealms().size()); + + assertEquals(0, realmAuthorizer.getWritableRealms().size()); + assertEquals(0, realmAuthorizer.getDeletableRealms().size()); + } + + @Test (expected = ServletException.class) + public void testRealmsHeaderSuperset() throws ServletException { + String[] roles = new String[]{"r-read,","u-update"}; + when(access.getRoles()).thenReturn(new HashSet<>(Arrays.asList(roles))); + + when(request.getHeader(eq("X-Thermostat-Realms"))).thenReturn("read,update,other"); + + new RealmAuthorizer(request); + } + + @Test + public void testRealmsHeaderWhitespace() throws ServletException { + String[] roles = new String[]{"w-write", "r-read", "u-update"}; + when(access.getRoles()).thenReturn(new HashSet<>(Arrays.asList(roles))); + + when(request.getHeader(eq("X-Thermostat-Realms"))).thenReturn(" read, update , write"); + + RealmAuthorizer realmAuthorizer = new RealmAuthorizer(request); + assertEquals(1, realmAuthorizer.getReadableRealms().size()); + assertEquals(1, realmAuthorizer.getUpdatableRealms().size()); + assertEquals(1, realmAuthorizer.getWritableRealms().size()); + + assertEquals(0, realmAuthorizer.getDeletableRealms().size()); + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/core/src/test/java/com/redhat/thermostat/gateway/common/core/auth/keycloak/RoleFactoryTest.java Mon Jul 17 11:22:45 2017 -0400 @@ -0,0 +1,105 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.gateway.common.core.auth.keycloak; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +public class RoleFactoryTest { + + private RoleFactory roleFactory; + + @Before + public void setup() { + roleFactory = new RoleFactory(); + } + + @Test + public void testValidRole() { + String role = "a-role"; + assertTrue(roleFactory.isValidRole(role)); + + Role r = roleFactory.buildRole(role); + verifyRole(r, "a", "role"); + } + + @Test + public void testValidRoleWithActions() { + String role = "rwd-role"; + + assertTrue(roleFactory.isValidRole(role)); + + Role r = roleFactory.buildRole(role); + verifyRole(r, "rwd", "role"); + } + + @Test + public void testNoActionRole() { + String role = "-role"; + assertFalse(roleFactory.isValidRole(role)); + } + + @Test + public void testNoRealmRole() { + String role = "a-"; + assertFalse(roleFactory.isValidRole(role)); + } + + @Test + public void testHyphenRealm() { + String role = "a-realm-with-hyphens"; + assertTrue(roleFactory.isValidRole(role)); + + Role r = roleFactory.buildRole(role); + verifyRole(r, "a", "realm-with-hyphens"); + } + + @Test + public void testRealmWithCommaIsInvalid() { + String role = "a-invalid,realm"; + assertFalse(roleFactory.isValidRole(role)); + } + + private void verifyRole(Role role, String expectedActions, String expectedRole) { + assertEquals(expectedActions, role.getActions()); + assertEquals(expectedRole, role.getRealm()); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/core/src/test/java/com/redhat/thermostat/gateway/common/core/auth/keycloak/RoleTest.java Mon Jul 17 11:22:45 2017 -0400 @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.gateway.common.core.auth.keycloak; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class RoleTest { + + @Test + public void testSimpleRole() { + Role r = new Role("a", "realm"); + + verifyRole(r, "a", "realm"); + } + + @Test + public void testMultipleActionsRole() { + Role r = new Role("rw", "realm-1.2-3"); + + verifyRole(r, "rw", "realm-1.2-3"); + } + + private void verifyRole(Role role, String expectedActions, String expectedRole) { + assertEquals(expectedActions, role.getActions()); + assertEquals(expectedRole, role.getRealm()); + } +}
--- a/common/mongodb/src/main/java/com/redhat/thermostat/gateway/common/mongodb/executor/MongoExecutor.java Mon Jul 17 11:42:24 2017 +0200 +++ b/common/mongodb/src/main/java/com/redhat/thermostat/gateway/common/mongodb/executor/MongoExecutor.java Mon Jul 17 11:22:45 2017 -0400 @@ -40,9 +40,11 @@ import static com.mongodb.client.model.Projections.fields; import static com.mongodb.client.model.Projections.include; +import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Set; import org.bson.Document; import org.bson.conversions.Bson; @@ -54,27 +56,27 @@ import com.mongodb.util.JSON; import com.redhat.thermostat.gateway.common.mongodb.filters.MongoRequestFilters; import com.redhat.thermostat.gateway.common.mongodb.filters.MongoSortFilters; +import com.redhat.thermostat.gateway.common.mongodb.keycloak.KeycloakFields; public class MongoExecutor { - public MongoDataResultContainer execGetRequest(MongoCollection<Document> collection, Integer limit, - Integer offset, String sort, String queries, String projections) { - return execGetRequest(collection, limit, offset, sort, buildQueries(queries), projections); + public MongoDataResultContainer execGetRequest(MongoCollection<Document> collection, Integer limit, Integer offset, + String sort, String queries, String projections, + Set<String> realms) throws IOException { + return execGetRequest(collection, limit, offset, sort, buildClientQueries(queries), projections, realms); } - public MongoDataResultContainer execGetRequest(MongoCollection<Document> collection, Integer limit, - Integer offset, String sort, List<String> queries, String projections) { + public MongoDataResultContainer execGetRequest(MongoCollection<Document> collection, Integer limit, Integer offset, + String sort, List<String> queries, String projections, + Set<String> realms) { FindIterable<Document> documents = collection.find(); MongoDataResultContainer queryDataContainer = new MongoDataResultContainer(); - if (queries != null && !queries.isEmpty()) { - final Bson query = MongoRequestFilters.buildQueriesFilter(queries); - documents = documents.filter(query); - queryDataContainer.setGetReqCount(collection.count(query)); - queryDataContainer.setRemainingNumQueryDocuments((int) (collection.count(query) - (limit + offset))); - } else { - queryDataContainer.setGetReqCount(collection.count()); - queryDataContainer.setRemainingNumQueryDocuments((int) (collection.count() - (limit + offset))); - } + Bson query = MongoRequestFilters.buildQuery(queries, realms); + documents = documents.filter(query); + + long count = collection.count(query); + queryDataContainer.setGetReqCount(count); + queryDataContainer.setRemainingNumQueryDocuments((int) (count - (limit + offset))); if (projections != null) { List<String> projectionsList = Arrays.asList(projections.split(",")); @@ -90,23 +92,21 @@ return queryDataContainer; } - public MongoDataResultContainer execPutRequest(MongoCollection<Document> collection, String body, String queries) { - return execPutRequest(collection, body, buildQueries(queries)); + public MongoDataResultContainer execPutRequest(MongoCollection<Document> collection, String body, + String queries, Set<String> realms) throws IOException { + return execPutRequest(collection, body, buildClientQueries(queries), realms); } - public MongoDataResultContainer execPutRequest(MongoCollection<Document> collection, String body, List<String> queries) { + public MongoDataResultContainer execPutRequest(MongoCollection<Document> collection, String body, + List<String> queries, Set<String> realms) { Document inputDocument = Document.parse(body); MongoDataResultContainer metaDataContainer = new MongoDataResultContainer(); Document setDocument = inputDocument.get("set", Document.class); + setDocument.remove(KeycloakFields.REALMS_KEY); final Bson fields = new Document("$set", setDocument); - Bson bsonQueries; - if (queries != null && !queries.isEmpty()) { - bsonQueries = MongoRequestFilters.buildQueriesFilter(queries); - } else { - bsonQueries = new Document(); - } + final Bson bsonQueries = MongoRequestFilters.buildQuery(queries, realms); collection.updateMany(bsonQueries, fields); @@ -116,17 +116,18 @@ } - public MongoDataResultContainer execDeleteRequest(MongoCollection<Document> collection, String queries) { - return execDeleteRequest(collection, buildQueries(queries)); + public MongoDataResultContainer execDeleteRequest(MongoCollection<Document> collection, String queries, + Set<String> realms) throws IOException { + return execDeleteRequest(collection, buildClientQueries(queries), realms); } - public MongoDataResultContainer execDeleteRequest(MongoCollection<Document> collection, List<String> queries) { + public MongoDataResultContainer execDeleteRequest(MongoCollection<Document> collection, List<String> queries, + Set<String> realms) { MongoDataResultContainer metaDataContainer = new MongoDataResultContainer(); - if (queries != null && !queries.isEmpty()) { - Bson bsonQueries = MongoRequestFilters.buildQueriesFilter(queries); + if (queries != null && !queries.isEmpty() || realms != null && !realms.isEmpty()) { + Bson bsonQueries = MongoRequestFilters.buildQuery(queries, realms); + metaDataContainer.setDeleteReqMatches(collection.count(bsonQueries)); collection.deleteMany(bsonQueries); - - metaDataContainer.setDeleteReqMatches(collection.count(bsonQueries)); } else { metaDataContainer.setDeleteReqMatches(collection.count()); collection.drop(); @@ -135,11 +136,21 @@ return metaDataContainer; } - public MongoDataResultContainer execPostRequest(MongoCollection<DBObject> collection, String body) { + public MongoDataResultContainer execPostRequest(MongoCollection<DBObject> collection, String body, + Set<String> realms) { MongoDataResultContainer metaDataContainer = new MongoDataResultContainer(); if (body.length() > 0) { List<DBObject> inputList = (List<DBObject>) JSON.parse(body); + + for (DBObject object : inputList) { + object.removeField(KeycloakFields.REALMS_KEY); + if (realms != null && !realms.isEmpty()) { + object.put(KeycloakFields.REALMS_KEY, realms); + } + + } + collection.insertMany(inputList); } @@ -147,9 +158,16 @@ } - private List<String> buildQueries(String queries) { + private List<String> buildClientQueries(String queries) throws IOException { if (queries != null) { - return Arrays.asList(queries.split(",")); + List<String> queriesList = Arrays.asList(queries.split(",")); + for (String query : queriesList) { + if (query.startsWith(KeycloakFields.REALMS_KEY)) { + throw new IOException("Cannot query realms property"); + } + } + + return queriesList; } else { return Collections.emptyList(); }
--- a/common/mongodb/src/main/java/com/redhat/thermostat/gateway/common/mongodb/filters/MongoRequestFilters.java Mon Jul 17 11:42:24 2017 +0200 +++ b/common/mongodb/src/main/java/com/redhat/thermostat/gateway/common/mongodb/filters/MongoRequestFilters.java Mon Jul 17 11:22:45 2017 -0400 @@ -36,6 +36,7 @@ package com.redhat.thermostat.gateway.common.mongodb.filters; +import static com.mongodb.client.model.Filters.all; import static com.mongodb.client.model.Filters.and; import static com.mongodb.client.model.Filters.eq; import static com.mongodb.client.model.Filters.gt; @@ -43,15 +44,20 @@ import static com.mongodb.client.model.Filters.lt; import static com.mongodb.client.model.Filters.lte; import static com.mongodb.client.model.Filters.ne; +import static com.mongodb.client.model.Filters.size; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.bson.Document; import org.bson.conversions.Bson; import org.bson.json.JsonParseException; +import com.redhat.thermostat.gateway.common.mongodb.keycloak.KeycloakFields; + public class MongoRequestFilters { public static Bson buildQueriesFilter(List<String> queries) { List<Bson> filters = new ArrayList<>(); @@ -101,4 +107,31 @@ } return and(filters); } + + public static Bson buildRealmsFilter(Set<String> realms) { + return and(all(KeycloakFields.REALMS_KEY, realms), size(KeycloakFields.REALMS_KEY, realms.size())); + } + + public static Bson buildQuery(List<String> clientQueries, Set<String> realms) { + Bson query = null; + + if (clientQueries != null && !clientQueries.isEmpty()) { + query = MongoRequestFilters.buildQueriesFilter(clientQueries); + } + + if (realms != null && realms.size() > 0) { + Bson realmsQuery = MongoRequestFilters.buildRealmsFilter(realms); + if (query != null) { + query = and(query, realmsQuery); + } else { + query = realmsQuery; + } + } + + if (query == null) { + query = new Document(); + } + + return query; + } }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/mongodb/src/main/java/com/redhat/thermostat/gateway/common/mongodb/keycloak/KeycloakFields.java Mon Jul 17 11:22:45 2017 -0400 @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.gateway.common.mongodb.keycloak; + +public class KeycloakFields { + public static final String REALMS_KEY = "realms"; +}
--- a/common/mongodb/src/main/java/com/redhat/thermostat/gateway/common/mongodb/response/MongoResponseBuilder.java Mon Jul 17 11:42:24 2017 +0200 +++ b/common/mongodb/src/main/java/com/redhat/thermostat/gateway/common/mongodb/response/MongoResponseBuilder.java Mon Jul 17 11:22:45 2017 -0400 @@ -44,6 +44,7 @@ import com.google.gson.GsonBuilder; import com.mongodb.Block; import com.mongodb.client.FindIterable; +import com.redhat.thermostat.gateway.common.mongodb.keycloak.KeycloakFields; /* * Builds the appropriate response after executing the request's MongoDB Query. @@ -67,6 +68,9 @@ documents.forEach(new Block<Document>() { @Override public void apply(Document document) { + if (document.containsKey(KeycloakFields.REALMS_KEY)) { + document.remove(KeycloakFields.REALMS_KEY); + } queryDocuments.add(document); } });
--- a/common/mongodb/src/test/java/com/redhat/thermostat/gateway/common/mongodb/filters/MongoRequestFiltersTest.java Mon Jul 17 11:42:24 2017 +0200 +++ b/common/mongodb/src/test/java/com/redhat/thermostat/gateway/common/mongodb/filters/MongoRequestFiltersTest.java Mon Jul 17 11:22:45 2017 -0400 @@ -37,12 +37,17 @@ package com.redhat.thermostat.gateway.common.mongodb.filters; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import org.bson.BsonArray; import org.bson.BsonDocument; +import org.bson.BsonString; import org.bson.conversions.Bson; import org.bson.json.JsonParseException; import org.junit.Test; @@ -164,4 +169,25 @@ public void testInvalidWithSymbols() { MongoRequestFilters.buildQueriesFilter(Collections.singletonList("a=")); } + + @Test + public void testBuildRealmsFilter() { + Set<String> realms = new HashSet<>(); + realms.add("one"); + realms.add("two"); + + Bson filter = MongoRequestFilters.buildRealmsFilter(realms); + BsonDocument bsonDocument = filter.toBsonDocument(BsonDocument.class, MongoClient.getDefaultCodecRegistry()); + + BsonDocument realmsDocument = bsonDocument.getDocument("realms"); + assertTrue(realmsDocument.containsKey("$all")); + assertTrue(realmsDocument.containsKey("$size")); + + assertEquals(2, realmsDocument.get("$size").asInt32().getValue()); + + BsonArray allDocument = realmsDocument.getArray("$all"); + assertEquals(2, allDocument.size()); + assertTrue(allDocument.contains(new BsonString("one"))); + assertTrue(allDocument.contains(new BsonString("two"))); + } }
--- a/common/mongodb/src/test/java/com/redhat/thermostat/gateway/common/mongodb/response/MongoResponseBuilderTest.java Mon Jul 17 11:42:24 2017 +0200 +++ b/common/mongodb/src/test/java/com/redhat/thermostat/gateway/common/mongodb/response/MongoResponseBuilderTest.java Mon Jul 17 11:22:45 2017 -0400 @@ -89,6 +89,21 @@ assertEquals(expected, output); } + @Test + public void testRealmsKeyRemoved() { + Document d1 = Document.parse("{\"hello\" : \"blob\", \"realms\" : [\"a\",\"b\"]}"); + Document d2 = Document.parse("{\"a\" : {\"blob\" : [\"hi\"]}, \"realms\" : [\"c\",\"d\"]}"); + final List<Document> list = new ArrayList<>(); + list.add(d1); + list.add(d2); + + FindIterable<Document> iterable = new TestFindIterable<>(list); + + String output = mongoResponseBuilder.queryDocuments(iterable).build(); + String expected = "{\"response\":[{\"hello\":\"blob\"},{\"a\":{\"blob\":[\"hi\"]}}]}"; + assertEquals(expected, output); + } + private class TestFindIterable<T> implements FindIterable<T> { private final List<T> list;
--- a/server/pom.xml Mon Jul 17 11:42:24 2017 +0200 +++ b/server/pom.xml Mon Jul 17 11:22:45 2017 -0400 @@ -93,6 +93,11 @@ <version>${keycloak.version}</version> </dependency> <dependency> + <groupId>org.keycloak</groupId> + <artifactId>keycloak-core</artifactId> + <version>${keycloak.version}</version> + </dependency> + <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>${google-gson.version}</version>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/server/src/main/java/com/redhat/thermostat/gateway/server/auth/keycloak/KeycloakRequestFilter.java Mon Jul 17 11:22:45 2017 -0400 @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.gateway.server.auth.keycloak; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.redhat.thermostat.gateway.common.core.auth.keycloak.RealmAuthorizer; + +public class KeycloakRequestFilter implements Filter { + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // Do nothing + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + try { + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + RealmAuthorizer realmAuthorizer = new RealmAuthorizer(httpServletRequest); + + httpServletRequest.setAttribute(RealmAuthorizer.class.getName(), realmAuthorizer); + + chain.doFilter(request, response); + } catch (ServletException e) { + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + httpServletResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid realms header"); + } + } + + @Override + public void destroy() { + // Do nothing + } +}
--- a/server/src/main/java/com/redhat/thermostat/gateway/server/services/WebArchiveCoreService.java Mon Jul 17 11:42:24 2017 +0200 +++ b/server/src/main/java/com/redhat/thermostat/gateway/server/services/WebArchiveCoreService.java Mon Jul 17 11:22:45 2017 -0400 @@ -37,10 +37,12 @@ package com.redhat.thermostat.gateway.server.services; import java.util.Collections; +import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; +import javax.servlet.DispatcherType; import javax.servlet.ServletException; import org.eclipse.jetty.security.ConstraintMapping; @@ -60,6 +62,7 @@ import com.redhat.thermostat.gateway.server.auth.basic.BasicUserStore; import com.redhat.thermostat.gateway.server.auth.keycloak.KeycloakConfiguration; import com.redhat.thermostat.gateway.server.auth.keycloak.KeycloakConfigurationFactory; +import com.redhat.thermostat.gateway.server.auth.keycloak.KeycloakRequestFilter; class WebArchiveCoreService implements CoreService { @@ -120,6 +123,9 @@ webAppContext.setInitParameter("org.keycloak.json.adapterConfig", keycloakConfig); webAppContext.setSecurityHandler(securityHandler); webAppContext.addSystemClass("org.keycloak."); + webAppContext.addSystemClass("com.redhat.thermostat.gateway.common.core.auth.keycloak."); + + webAppContext.addFilter(KeycloakRequestFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); } private void setupBasicAuthForContext(WebAppContext webAppContext) {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/server/src/test/java/com/redhat/thermostat/gateway/server/auth/keycloak/KeycloakRequestFilterTest.java Mon Jul 17 11:22:45 2017 -0400 @@ -0,0 +1,118 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.gateway.server.auth.keycloak; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Before; +import org.junit.Test; +import org.keycloak.KeycloakSecurityContext; +import org.keycloak.representations.AccessToken; +import org.mockito.ArgumentMatchers; + +import com.redhat.thermostat.gateway.common.core.auth.keycloak.RealmAuthorizer; + +public class KeycloakRequestFilterTest { + + + HttpServletRequest request; + AccessToken.Access access; + + @Before + public void setup() { + request = mock(HttpServletRequest.class); + KeycloakSecurityContext keycloakSecurityContext = mock(KeycloakSecurityContext.class); + when(request.getAttribute(eq(KeycloakSecurityContext.class.getName()))).thenReturn(keycloakSecurityContext); + + AccessToken accessToken = mock(AccessToken.class); + when(keycloakSecurityContext.getToken()).thenReturn(accessToken); + + access = mock(AccessToken.Access.class); + when(accessToken.getRealmAccess()).thenReturn(access); + } + + @Test + public void verifyRealmsAuthorizerSet() throws IOException, ServletException { + String[] roles = new String[]{"a-realm"}; + when(access.getRoles()).thenReturn(new HashSet<>(Arrays.asList(roles))); + + KeycloakRequestFilter keycloakRequestFilter = new KeycloakRequestFilter(); + + HttpServletResponse httpServletResponse = mock(HttpServletResponse.class); + FilterChain filterChain = mock(FilterChain.class); + + keycloakRequestFilter.doFilter(request, httpServletResponse, filterChain); + + verify(request, times(1)).setAttribute(eq(RealmAuthorizer.class.getName()), ArgumentMatchers.any(RealmAuthorizer.class)); + + verify(filterChain, times(1)).doFilter(eq(request), eq(httpServletResponse)); + } + + @Test + public void verifyBadRequestSent() throws IOException, ServletException { + String[] roles = new String[]{"a-realm"}; + when(access.getRoles()).thenReturn(new HashSet<>(Arrays.asList(roles))); + + when(request.getHeader(eq(RealmAuthorizer.REALMS_HEADER))).thenReturn("blob"); + + KeycloakRequestFilter keycloakRequestFilter = new KeycloakRequestFilter(); + + HttpServletResponse httpServletResponse = mock(HttpServletResponse.class); + FilterChain filterChain = mock(FilterChain.class); + + keycloakRequestFilter.doFilter(request, httpServletResponse, filterChain); + + verify(httpServletResponse, times(1)).sendError(eq(HttpServletResponse.SC_BAD_REQUEST), eq("Invalid realms header")); + + verify(request, times(0)).setAttribute(eq(RealmAuthorizer.class.getName()), ArgumentMatchers.any(RealmAuthorizer.class)); + + verify(filterChain, times(0)).doFilter(eq(request), eq(httpServletResponse)); + } +}
--- a/server/src/test/java/com/redhat/thermostat/gateway/server/services/WebArchiveCoreServiceTest.java Mon Jul 17 11:42:24 2017 +0200 +++ b/server/src/test/java/com/redhat/thermostat/gateway/server/services/WebArchiveCoreServiceTest.java Mon Jul 17 11:22:45 2017 -0400 @@ -50,6 +50,7 @@ import org.eclipse.jetty.security.SecurityHandler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.UserIdentity; +import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.webapp.WebAppContext; import org.junit.Test; @@ -58,6 +59,7 @@ import com.redhat.thermostat.gateway.common.core.config.Configuration; import com.redhat.thermostat.gateway.common.core.config.ServiceConfiguration; import com.redhat.thermostat.gateway.server.auth.basic.BasicLoginService; +import com.redhat.thermostat.gateway.server.auth.keycloak.KeycloakRequestFilter; public class WebArchiveCoreServiceTest { @@ -115,6 +117,11 @@ assertTrue(securityHandler instanceof ConstraintSecurityHandler); assertTrue(securityHandler.getAuthenticator() instanceof KeycloakJettyAuthenticator); assertEquals(securityHandler.getRealmName(), "thermostat"); + + FilterHolder[] filters = webAppContext.getServletHandler().getFilters(); + + assertEquals(1, filters.length); + assertEquals(KeycloakRequestFilter.class, filters[0].getHeldClass()); }
--- a/services/jvm-gc/pom.xml Mon Jul 17 11:42:24 2017 +0200 +++ b/services/jvm-gc/pom.xml Mon Jul 17 11:22:45 2017 -0400 @@ -74,6 +74,11 @@ <!-- Thermostat Web Gateway Dependencies --> <dependency> <groupId>com.redhat.thermostat</groupId> + <artifactId>thermostat-web-gateway-common-core</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>com.redhat.thermostat</groupId> <artifactId>thermostat-web-gateway-common-mongodb</artifactId> <version>${project.version}</version> </dependency>
--- a/services/jvm-gc/src/main/java/com/redhat/thermostat/service/jvm/gc/JvmGcHttpHandler.java Mon Jul 17 11:42:24 2017 +0200 +++ b/services/jvm-gc/src/main/java/com/redhat/thermostat/service/jvm/gc/JvmGcHttpHandler.java Mon Jul 17 11:22:45 2017 -0400 @@ -51,6 +51,7 @@ import javax.ws.rs.core.Response; import com.mongodb.DBObject; +import com.redhat.thermostat.gateway.common.core.auth.keycloak.RealmAuthorizer; import com.redhat.thermostat.gateway.common.mongodb.ThermostatMongoStorage; import com.redhat.thermostat.gateway.common.mongodb.executor.MongoDataResultContainer; import com.redhat.thermostat.gateway.common.mongodb.executor.MongoExecutor; @@ -73,12 +74,25 @@ @QueryParam("q") String queries, @QueryParam("p") String projections, @QueryParam("m") @DefaultValue("false") Boolean metadata, - @Context HttpServletRequest requestInfo, + @Context HttpServletRequest httpServletRequest, @Context ServletContext context) { try { + RealmAuthorizer realmAuthorizer = (RealmAuthorizer) httpServletRequest.getAttribute(RealmAuthorizer.class.getName()); ThermostatMongoStorage storage = (ThermostatMongoStorage) context.getAttribute(ServletContextConstants.MONGODB_CLIENT_ATTRIBUTE); - MongoDataResultContainer execResult = mongoExecutor.execGetRequest( - storage.getDatabase().getCollection(collectionName), limit, offset, sort, queries, projections); + + MongoDataResultContainer execResult; + + if (realmAuthorizer != null) { + if (realmAuthorizer.readable()) { + execResult = mongoExecutor.execGetRequest( + storage.getDatabase().getCollection(collectionName), limit, offset, sort, queries, projections, realmAuthorizer.getReadableRealms()); + } else { + return Response.status(Response.Status.FORBIDDEN).build(); + } + } else { + execResult = mongoExecutor.execGetRequest( + storage.getDatabase().getCollection(collectionName), limit, offset, sort, queries, projections, null); + } MongoResponseBuilder.Builder response = new MongoResponseBuilder.Builder(); response.queryDocuments(execResult.getQueryDataResult()); @@ -86,7 +100,7 @@ if (metadata) { MongoMetaDataResponseBuilder.MetaBuilder metaDataResponse = new MongoMetaDataResponseBuilder.MetaBuilder(); MongoMetaDataGenerator metaDataGenerator = new MongoMetaDataGenerator(limit, offset, sort, queries, - projections, requestInfo, execResult); + projections, httpServletRequest, execResult); metaDataGenerator.setDocAndPayloadCount(metaDataResponse); metaDataGenerator.setPrev(metaDataResponse); @@ -96,7 +110,7 @@ } return Response.status(Response.Status.OK).entity(response.build()).build(); } catch (Exception e) { - return Response.status(Response.Status.BAD_REQUEST).entity(e.getStackTrace()).build(); + return Response.status(Response.Status.BAD_REQUEST).build(); } } @@ -106,10 +120,22 @@ public Response putJvmGc(String body, @QueryParam("q") String queries, @QueryParam("m") @DefaultValue("false") String metadata, - @Context ServletContext context) { + @Context ServletContext context, + @Context HttpServletRequest httpServletRequest) { try { + RealmAuthorizer realmAuthorizer = (RealmAuthorizer) httpServletRequest.getAttribute(RealmAuthorizer.class.getName()); ThermostatMongoStorage storage = (ThermostatMongoStorage) context.getAttribute(ServletContextConstants.MONGODB_CLIENT_ATTRIBUTE); - mongoExecutor.execPutRequest(storage.getDatabase().getCollection(collectionName), body, queries); + + if (realmAuthorizer != null) { + if (realmAuthorizer.updatable()) { + mongoExecutor.execPutRequest(storage.getDatabase().getCollection(collectionName), body, queries, realmAuthorizer.getUpdatableRealms()); + } else { + return Response.status(Response.Status.FORBIDDEN).build(); + } + } else { + mongoExecutor.execPutRequest(storage.getDatabase().getCollection(collectionName), body, queries, null); + } + return Response.status(Response.Status.OK).build(); } catch (Exception e) { return Response.status(Response.Status.BAD_REQUEST).build(); @@ -121,10 +147,21 @@ @Produces({ "application/json", "text/html; charset=utf-8" }) public Response postJvmGc(String body, @QueryParam("m") @DefaultValue("false") String metadata, - @Context ServletContext context) { + @Context ServletContext context, + @Context HttpServletRequest httpServletRequest) { try { + RealmAuthorizer realmAuthorizer = (RealmAuthorizer) httpServletRequest.getAttribute(RealmAuthorizer.class.getName()); ThermostatMongoStorage storage = (ThermostatMongoStorage) context.getAttribute(ServletContextConstants.MONGODB_CLIENT_ATTRIBUTE); - mongoExecutor.execPostRequest(storage.getDatabase().getCollection(collectionName, DBObject.class), body); + + if (realmAuthorizer != null) { + if (realmAuthorizer.writable()) { + mongoExecutor.execPostRequest(storage.getDatabase().getCollection(collectionName, DBObject.class), body, realmAuthorizer.getWritableRealms()); + } else { + return Response.status(Response.Status.FORBIDDEN).build(); + } + } else { + mongoExecutor.execPostRequest(storage.getDatabase().getCollection(collectionName, DBObject.class), body, null); + } return Response.status(Response.Status.OK).build(); } catch (Exception e) { return Response.status(Response.Status.BAD_REQUEST).build(); @@ -136,10 +173,22 @@ @Produces({ "application/json", "text/html; charset=utf-8" }) public Response deleteJvmGc(@QueryParam("q") String queries, @QueryParam("m") @DefaultValue("false") String metadata, - @Context ServletContext context) { + @Context ServletContext context, + @Context HttpServletRequest httpServletRequest) { try { + RealmAuthorizer realmAuthorizer = (RealmAuthorizer) httpServletRequest.getAttribute(RealmAuthorizer.class.getName()); ThermostatMongoStorage storage = (ThermostatMongoStorage) context.getAttribute(ServletContextConstants.MONGODB_CLIENT_ATTRIBUTE); - mongoExecutor.execDeleteRequest(storage.getDatabase().getCollection(collectionName), queries); + + if (realmAuthorizer != null) { + if (realmAuthorizer.deletable()) { + mongoExecutor.execDeleteRequest(storage.getDatabase().getCollection(collectionName), queries, realmAuthorizer.getDeletableRealms()); + } else { + return Response.status(Response.Status.FORBIDDEN).build(); + } + } else { + mongoExecutor.execDeleteRequest(storage.getDatabase().getCollection(collectionName), queries, null); + } + return Response.status(Response.Status.OK).build(); } catch (Exception e) { return Response.status(Response.Status.BAD_REQUEST).build();
--- a/services/jvm-gc/src/main/resources/jvm-gc-swagger.yaml Mon Jul 17 11:42:24 2017 +0200 +++ b/services/jvm-gc/src/main/resources/jvm-gc-swagger.yaml Mon Jul 17 11:22:45 2017 -0400 @@ -13,6 +13,8 @@ basePath: /jvm-gc/0.0.2 paths: /: + parameters: + - $ref: '#/parameters/thermostat-realms' get: description: Get jvm gc information. parameters: @@ -167,4 +169,9 @@ name: m type: boolean in: query - description: "Metadata flag. If set to 'true', the subsequent request response will return metadata information. If set to 'false', such metadata information will be omitted." \ No newline at end of file + description: "Metadata flag. If set to 'true', the subsequent request response will return metadata information. If set to 'false', such metadata information will be omitted." + thermostat-realms: + name: X-Thermostat-Realms + type: string + in: header + description: "Realms Header used to specify a subset of roles to use for Keycloak authorization. Attempts to specify realms that the client does not have, or no valid realms at all will result in a 400 Bad Request response. Expects a comma separated list of realms Example 'X-Thermostat-Realms: realm-one, realm-two'" \ No newline at end of file
--- a/services/jvm-memory/src/main/java/com/redhat/thermostat/service/jvm/memory/JvmMemoryHttpHandler.java Mon Jul 17 11:42:24 2017 +0200 +++ b/services/jvm-memory/src/main/java/com/redhat/thermostat/service/jvm/memory/JvmMemoryHttpHandler.java Mon Jul 17 11:22:45 2017 -0400 @@ -80,7 +80,7 @@ Integer offset = offsetParam.getValue(); ThermostatMongoStorage storage = (ThermostatMongoStorage) context.getAttribute(ServletContextConstants.MONGODB_CLIENT_ATTRIBUTE); MongoDataResultContainer execResult = mongoExecutor.execGetRequest( - storage.getDatabase().getCollection(collectionName), limit, offset, sort, queries, projections); + storage.getDatabase().getCollection(collectionName), limit, offset, sort, queries, projections, null); MongoResponseBuilder.Builder response = new MongoResponseBuilder.Builder(); response.queryDocuments(execResult.getQueryDataResult()); @@ -112,7 +112,7 @@ try { ThermostatMongoStorage storage = (ThermostatMongoStorage) context.getAttribute(ServletContextConstants.MONGODB_CLIENT_ATTRIBUTE); - MongoDataResultContainer putExec = mongoExecutor.execPutRequest(storage.getDatabase().getCollection(collectionName), body, queries); + mongoExecutor.execPutRequest(storage.getDatabase().getCollection(collectionName), body, queries, null); return Response.status(Response.Status.OK).build(); } catch (Exception e) { @@ -129,7 +129,7 @@ try { ThermostatMongoStorage storage = (ThermostatMongoStorage) context.getAttribute(ServletContextConstants.MONGODB_CLIENT_ATTRIBUTE); - mongoExecutor.execPostRequest(storage.getDatabase().getCollection(collectionName, DBObject.class), body); + mongoExecutor.execPostRequest(storage.getDatabase().getCollection(collectionName, DBObject.class), body, null); return Response.status(Response.Status.OK).build(); } catch (Exception e) { return Response.status(Response.Status.BAD_REQUEST).build(); @@ -145,7 +145,7 @@ try { ThermostatMongoStorage storage = (ThermostatMongoStorage) context.getAttribute(ServletContextConstants.MONGODB_CLIENT_ATTRIBUTE); - MongoDataResultContainer delExec = mongoExecutor.execDeleteRequest(storage.getDatabase().getCollection(collectionName), queries); + MongoDataResultContainer delExec = mongoExecutor.execDeleteRequest(storage.getDatabase().getCollection(collectionName), queries, null); return Response.status(Response.Status.OK).build(); } catch (Exception e) {
--- a/services/system-memory/src/main/java/com/redhat/thermostat/service/system/memory/mongo/MongoStorageHandler.java Mon Jul 17 11:42:24 2017 +0200 +++ b/services/system-memory/src/main/java/com/redhat/thermostat/service/system/memory/mongo/MongoStorageHandler.java Mon Jul 17 11:22:45 2017 -0400 @@ -48,7 +48,6 @@ import org.bson.Document; import org.bson.conversions.Bson; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List;
--- a/tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/jvm/gc/JvmGcServiceIntegrationTest.java Mon Jul 17 11:42:24 2017 +0200 +++ b/tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/jvm/gc/JvmGcServiceIntegrationTest.java Mon Jul 17 11:22:45 2017 -0400 @@ -38,15 +38,26 @@ package com.redhat.thermostat.gateway.service.jvm.gc; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import java.lang.reflect.Type; +import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; +import org.bson.Document; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.util.StringContentProvider; import org.eclipse.jetty.http.HttpMethod; import org.junit.Test; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.mongodb.Block; +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoCollection; import com.redhat.thermostat.gateway.tests.integration.MongoIntegrationTest; public class JvmGcServiceIntegrationTest extends MongoIntegrationTest { @@ -327,4 +338,195 @@ makeHttpMethodRequest(HttpMethod.POST,"", data,"application/json","", 200); makeHttpGetRequest(gcUrl +"?q=b==test1&m=true&o=2&l=3", expectedResponse, 200); } + + @Test + public void testUpdateDoesNotAffectRealms() throws InterruptedException, TimeoutException, ExecutionException { + MongoCollection<Document> collection = mongodTestUtil.getCollection(serviceName); + String data = "[{\"item\":1,\"realms\":[\"a\",\"b\"]}," + + "{\"item\":2,\"realms\":[\"a\",\"b\"]}]"; + final Gson gson = new GsonBuilder().create(); + final Type listType = new TypeToken<List<Document>>() {}.getType(); + List<Document> insertDocuments = gson.fromJson(data, listType); + + collection.insertMany(insertDocuments); + + String updateString = "{\"set\" : {\"realms\" : 1, \"a\" : 2}}"; + makeHttpMethodRequest(HttpMethod.PUT,"", updateString,"application/json","", 200); + + FindIterable<Document> documents = collection.find(); + documents.forEach(new Block<Document>() { + @Override + public void apply(Document document) { + assertEquals("[\"a\",\"b\"]", gson.toJson(document.get("realms"), listType)); + } + }); + } + + @Test + public void testUpdateOnlyRealmsDoesNotAffectRealms() throws InterruptedException, TimeoutException, ExecutionException { + MongoCollection<Document> collection = mongodTestUtil.getCollection(serviceName); + String data = "[{\"item\":1,\"realms\":[\"a\",\"b\"]}," + + "{\"item\":2,\"realms\":[\"a\",\"b\"]}]"; + final Gson gson = new GsonBuilder().create(); + final Type listType = new TypeToken<List<Document>>() {}.getType(); + List<Document> insertDocuments = gson.fromJson(data, listType); + + collection.insertMany(insertDocuments); + + String updateString = "{\"set\" : {\"realms\" : 1}}"; + makeHttpMethodRequest(HttpMethod.PUT,"", updateString,"application/json","", 400); + + FindIterable<Document> documents = collection.find(); + documents.forEach(new Block<Document>() { + @Override + public void apply(Document document) { + assertEquals("[\"a\",\"b\"]", gson.toJson(document.get("realms"), listType)); + } + }); + } + + @Test + public void testUpdateRealmsQueryMatchIsNotUsed() throws InterruptedException, TimeoutException, ExecutionException { + MongoCollection<Document> collection = mongodTestUtil.getCollection(serviceName); + String data = "[{\"item\":1,\"realms\":[\"a\"]}," + + "{\"item\":2,\"realms\":[\"b\"]}]"; + final Gson gson = new GsonBuilder().create(); + final Type listType = new TypeToken<List<Document>>() {}.getType(); + List<Document> insertDocuments = gson.fromJson(data, listType); + + collection.insertMany(insertDocuments); + + String updateString = "{\"set\" : {\"item\" : 5}}"; + StringContentProvider stringContentProvider = new StringContentProvider(updateString, "UTF-8"); + ContentResponse response = client.newRequest(gcUrl).param("q", "realms==[\"a\"]") + .method(HttpMethod.PUT).content(stringContentProvider, "application/json") + .send(); + assertEquals(400, response.getStatus()); + + FindIterable<Document> documents = collection.find(); + documents.forEach(new Block<Document>() { + @Override + public void apply(Document document) { + if (document.get("item", Double.class).equals(5)) { + fail(); + } + } + }); + } + + @Test + public void testUpdateMultipleRealmsDoesNotAffectRealms() throws InterruptedException, TimeoutException, ExecutionException { + MongoCollection<Document> collection = mongodTestUtil.getCollection(serviceName); + String data = "[{\"item\":1,\"realms\":[\"a\",\"b\"]}," + + "{\"item\":2,\"realms\":[\"a\",\"b\"]}]"; + final Gson gson = new GsonBuilder().create(); + final Type listType = new TypeToken<List<Document>>() {}.getType(); + List<Document> insertDocuments = gson.fromJson(data, listType); + + collection.insertMany(insertDocuments); + + String updateString = "{\"set\" : {\"realms\" : 1, \"realms\" : 2}}"; + makeHttpMethodRequest(HttpMethod.PUT,"", updateString,"application/json","", 400); + + FindIterable<Document> documents = collection.find(); + documents.forEach(new Block<Document>() { + @Override + public void apply(Document document) { + assertEquals("[\"a\",\"b\"]", gson.toJson(document.get("realms"), listType)); + } + }); + } + + @Test + public void testGetCannotSeeRealms() throws InterruptedException, ExecutionException, TimeoutException { + MongoCollection<Document> collection = mongodTestUtil.getCollection(serviceName); + String data = "[{\"item\":1,\"realms\":[\"a\",\"b\"]}," + + "{\"item\":2,\"realms\":[\"a\",\"b\"]}]"; + final Gson gson = new GsonBuilder().create(); + final Type listType = new TypeToken<List<Document>>() {}.getType(); + List<Document> insertDocuments = gson.fromJson(data, listType); + + collection.insertMany(insertDocuments); + + String expected = "{\"response\":[{\"item\":1.0}]}"; + + makeHttpGetRequest(gcUrl,expected, 200); + } + + @Test + public void testGetProjectionCannotSeeRealms() throws InterruptedException, ExecutionException, TimeoutException { + MongoCollection<Document> collection = mongodTestUtil.getCollection(serviceName); + String data = "[{\"item\":1,\"realms\":[\"a\",\"b\"]}," + + "{\"item\":2,\"realms\":[\"a\",\"b\"]}]"; + final Gson gson = new GsonBuilder().create(); + final Type listType = new TypeToken<List<Document>>() {}.getType(); + List<Document> insertDocuments = gson.fromJson(data, listType); + + collection.insertMany(insertDocuments); + + String expected = "{\"response\":[{\"item\":1.0}]}"; + + makeHttpGetRequest(gcUrl + "?p=realms,item",expected, 200); + } + + @Test + public void testGetQueryCannotMatchRealms() throws InterruptedException, ExecutionException, TimeoutException { + MongoCollection<Document> collection = mongodTestUtil.getCollection(serviceName); + String data = "[{\"item\":1,\"realms\":[\"a\"]}," + + "{\"item\":2,\"realms\":[\"b\"]}]"; + final Gson gson = new GsonBuilder().create(); + final Type listType = new TypeToken<List<Document>>() {}.getType(); + List<Document> insertDocuments = gson.fromJson(data, listType); + + collection.insertMany(insertDocuments); + + ContentResponse response = client.newRequest(gcUrl) + .param("q", "realms==[\"a\"]").method(HttpMethod.GET).send(); + + assertEquals(400, response.getStatus()); + } + + + @Test + public void testPostCannotAddRealms() throws InterruptedException, ExecutionException, TimeoutException { + String data = "[{\"item\":1,\"realms\":[\"a\",\"b\"]}," + + "{\"item\":2,\"realms\":[\"a\",\"b\"]}]"; + + makeHttpMethodRequest(HttpMethod.POST, "", data, "application/json", "", 200); + + MongoCollection<Document> collection = mongodTestUtil.getCollection(serviceName); + + FindIterable<Document> documents = collection.find(); + documents.forEach(new Block<Document>() { + @Override + public void apply(Document document) { + assertNull(document.get("realms")); + } + }); + } + + @Test + public void testDeleteCannotQueryRealms() throws InterruptedException, TimeoutException, ExecutionException { + MongoCollection<Document> collection = mongodTestUtil.getCollection(serviceName); + String data = "[{\"item\":1,\"realms\":[\"a\",\"b\"]}," + + "{\"item\":2,\"realms\":[\"a\",\"b\"]}]"; + final Gson gson = new GsonBuilder().create(); + final Type listType = new TypeToken<List<Document>>() {}.getType(); + List<Document> insertDocuments = gson.fromJson(data, listType); + + collection.insertMany(insertDocuments); + + ContentResponse response = client.newRequest(gcUrl) + .param("q", "realms==[\"a\",\"b\"]").method(HttpMethod.DELETE) + .send(); + assertEquals(400, response.getStatus()); + + FindIterable<Document> documents = collection.find(); + documents.forEach(new Block<Document>() { + @Override + public void apply(Document document) { + assertEquals("[\"a\",\"b\"]", gson.toJson(document.get("realms"), listType)); + } + }); + } }
--- a/tests/test-utils/src/main/java/com/redhat/thermostat/gateway/tests/utils/MongodTestUtil.java Mon Jul 17 11:42:24 2017 +0200 +++ b/tests/test-utils/src/main/java/com/redhat/thermostat/gateway/tests/utils/MongodTestUtil.java Mon Jul 17 11:22:45 2017 -0400 @@ -43,6 +43,7 @@ import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; +import com.mongodb.client.MongoCollection; import com.redhat.thermostat.gateway.common.util.OS; import org.bson.Document; @@ -97,6 +98,11 @@ mongoClient.getDatabase(databaseName).getCollection(collectionName).drop(); } + + public MongoCollection<Document> getCollection(String collectionName) { + return mongoClient.getDatabase(databaseName).getCollection(collectionName); + } + private void finish() throws IOException { Files.walkFileTree(tempDbDir, new SimpleFileVisitor<Path>() { @Override @@ -161,4 +167,5 @@ public boolean isConnectedToDatabase() { return connectedToDatabase; } + } \ No newline at end of file