/* * Copyright 2016 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.keycloak.testsuite.util; import com.fasterxml.jackson.core.type.TypeReference; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.Matchers; import org.hamcrest.TypeSafeMatcher; import org.junit.rules.TestRule; import org.junit.runners.model.Statement; import org.keycloak.common.util.ObjectUtil; import org.keycloak.common.util.reflections.Reflections; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInputException; import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.AdminEventRepresentation; import org.keycloak.representations.idm.AuthDetailsRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.Assert; import org.keycloak.util.JsonSerialization; import javax.ws.rs.core.Response; import java.io.ByteArrayInputStream; import java.io.IOException; import java.lang.reflect.Method; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> */ public class AssertAdminEvents implements TestRule { private AbstractKeycloakTest context; public AssertAdminEvents(AbstractKeycloakTest ctx) { context = ctx; } @Override public Statement apply(final Statement base, org.junit.runner.Description description) { return new Statement() { @Override public void evaluate() throws Throwable { // TODO: Ideally clear the queue just before testClass rather then before each method clear(); base.evaluate(); // TODO Test should fail if there are leftover events } }; } public AdminEventRepresentation poll() { AdminEventRepresentation event = fetchNextEvent(); Assert.assertNotNull("Admin event expected", event); return event; } public void assertEmpty() { AdminEventRepresentation event = fetchNextEvent(); Assert.assertNull("Empty admin event queue expected, but there is " + event, event); } // Clears both "classic" and admin events for now public void clear() { context.getTestingClient().testing().clearAdminEventQueue(); } private AdminEventRepresentation fetchNextEvent() { return context.getTestingClient().testing().pollAdminEvent(); } public ExpectedAdminEvent expect() { return new ExpectedAdminEvent(); } public AdminEventRepresentation assertEvent(String realmId, OperationType operationType, String resourcePath, ResourceType resourceType) { return assertEvent(realmId, operationType, resourcePath, null, resourceType); } public AdminEventRepresentation assertEvent(String realmId, OperationType operationType, Matcher<String> resourcePath, ResourceType resourceType) { return assertEvent(realmId, operationType, resourcePath, null, resourceType); } public AdminEventRepresentation assertEvent(String realmId, OperationType operationType, String resourcePath, Object representation, ResourceType resourceType) { return assertEvent(realmId, operationType, Matchers.equalTo(resourcePath), representation, resourceType); } public AdminEventRepresentation assertEvent(String realmId, OperationType operationType, Matcher<String> resourcePath, Object representation, ResourceType resourceType) { return expect().realmId(realmId) .operationType(operationType) .resourcePath(resourcePath) .resourceType(resourceType) .representation(representation) .assertEvent(); } public class ExpectedAdminEvent { private AdminEventRepresentation expected = new AdminEventRepresentation(); private Matcher<String> resourcePath; private ResourceType resourceType; private Object expectedRep; public ExpectedAdminEvent realmId(String realmId) { expected.setRealmId(realmId); return this; } public ExpectedAdminEvent realm(RealmRepresentation realm) { return realmId(realm.getId()); } public ExpectedAdminEvent operationType(OperationType operationType) { expected.setOperationType(operationType.toString()); updateOperationTypeIfError(); return this; } public ExpectedAdminEvent resourcePath(String resourcePath) { return resourcePath(Matchers.equalTo(resourcePath)); } public ExpectedAdminEvent resourcePath(Matcher<String> resourcePath) { this.resourcePath = resourcePath; return this; } public ExpectedAdminEvent resourceType(ResourceType resourceType){ expected.setResourceType(resourceType.toString()); return this; } public ExpectedAdminEvent error(String error) { expected.setError(error); updateOperationTypeIfError(); return this; } private void updateOperationTypeIfError() { if (expected.getError() != null && expected.getOperationType() != null) { expected.setOperationType(expected.getOperationType() + "_ERROR"); } } public ExpectedAdminEvent authDetails(String realmId, String clientId, String userId) { AuthDetailsRepresentation authDetails = new AuthDetailsRepresentation(); authDetails.setRealmId(realmId); authDetails.setClientId(clientId); authDetails.setUserId(userId); expected.setAuthDetails(authDetails); return this; } public ExpectedAdminEvent representation(Object representation) { this.expectedRep = representation; return this; } public AdminEventRepresentation assertEvent() { return assertEvent(poll()); } public AdminEventRepresentation assertEvent(AdminEventRepresentation actual) { Assert.assertEquals(expected.getRealmId(), actual.getRealmId()); Assert.assertThat(actual.getResourcePath(), resourcePath); Assert.assertEquals(expected.getResourceType(), actual.getResourceType()); Assert.assertEquals(expected.getOperationType(), actual.getOperationType()); Assert.assertTrue(ObjectUtil.isEqualOrBothNull(expected.getError(), actual.getError())); // AuthDetails AuthDetailsRepresentation expectedAuth = expected.getAuthDetails(); if (expectedAuth == null) { expectedAuth = defaultAuthDetails(); } AuthDetailsRepresentation actualAuth = actual.getAuthDetails(); Assert.assertEquals(expectedAuth.getRealmId(), actualAuth.getRealmId()); Assert.assertEquals(expectedAuth.getUserId(), actualAuth.getUserId()); if (expectedAuth.getClientId() != null) { Assert.assertEquals(expectedAuth.getClientId(), actualAuth.getClientId()); } // Representation comparison if (expectedRep != null) { if (actual.getRepresentation() == null) { Assert.fail("Expected representation " + expectedRep + " but no representation was available on actual event"); } else { try { if (expectedRep instanceof List) { // List of roles. All must be available in actual representation List<RoleRepresentation> expectedRoles = (List<RoleRepresentation>) expectedRep; List<RoleRepresentation> actualRoles = JsonSerialization.readValue(new ByteArrayInputStream(actual.getRepresentation().getBytes()), new TypeReference<List<RoleRepresentation>>() { }); Map<String, String> expectedRolesMap = new HashMap<>(); for (RoleRepresentation role : expectedRoles) { expectedRolesMap.put(role.getId(), role.getName()); } Map<String, String> actualRolesMap = new HashMap<>(); for (RoleRepresentation role : actualRoles) { actualRolesMap.put(role.getId(), role.getName()); } Assert.assertEquals(expectedRolesMap, actualRolesMap); } else if (expectedRep instanceof Map) { Object actualRep = JsonSerialization.readValue(actual.getRepresentation(), Map.class); // Comparing of map representations. All of "expected" key-values must be available on "actual" map from the event Map<?, ?> expectedRepMap = (Map) expectedRep; Map<?, ?> actualRepMap = (Map) actualRep; for (Map.Entry entry : expectedRepMap.entrySet()) { Object expectedValue = entry.getValue(); if (expectedValue != null) { Object actualValue = actualRepMap.get(entry.getKey()); Assert.assertEquals("Map item with key '" + entry.getKey() + "' not equal.", expectedValue, actualValue); } } } else { Object actualRep = JsonSerialization.readValue(actual.getRepresentation(), expectedRep.getClass()); // Reflection-based comparing for other types - compare the non-null fields of "expected" representation with the "actual" representation from the event for (Method method : Reflections.getAllDeclaredMethods(expectedRep.getClass())) { if (method.getName().startsWith("get") || method.getName().startsWith("is")) { Object expectedValue = Reflections.invokeMethod(method, expectedRep); if (expectedValue != null) { Object actualValue = Reflections.invokeMethod(method, actualRep); Assert.assertEquals("Property method '" + method.getName() + "' of representation not equal.", expectedValue, actualValue); } } } } } catch (IOException ioe) { throw new RuntimeException(ioe); } } } return actual; } } private AuthDetailsRepresentation defaultAuthDetails() { String accessTokenString = context.getAdminClient().tokenManager().getAccessTokenString(); try { JWSInput input = new JWSInput(accessTokenString); AccessToken token = input.readJsonContent(AccessToken.class); AuthDetailsRepresentation authDetails = new AuthDetailsRepresentation(); String realmId = token.getIssuer().substring(token.getIssuer().lastIndexOf('/') + 1); authDetails.setRealmId(realmId); authDetails.setUserId(token.getSubject()); return authDetails; } catch (JWSInputException jwe) { throw new RuntimeException(jwe); } } public static Matcher<String> isExpectedPrefixFollowedByUuid(final String prefix) { return new TypeSafeMatcher<String>() { @Override protected boolean matchesSafely(String item) { int expectedLength = prefix.length() + 1 + org.keycloak.models.utils.KeycloakModelUtils.generateId().length(); return item.startsWith(prefix) && expectedLength == item.length(); } @Override public void describeTo(Description description) { description.appendText("resourcePath in the format like \"" + prefix + "/<UUID>\""); } }; } }