keycloak-aplcache

Details

diff --git a/core/src/main/java/org/keycloak/representations/idm/AdminEventRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/AdminEventRepresentation.java
new file mode 100644
index 0000000..f615cbd
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/AdminEventRepresentation.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.representations.idm;
+
+/**
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+public class AdminEventRepresentation {
+
+    private long time;
+    private String realmId;
+    private AuthDetailsRepresentation authDetails;
+    private String operationType;
+    private String resourcePath;
+    private String representation;
+    private String error;
+
+    public long getTime() {
+        return time;
+    }
+
+    public void setTime(long time) {
+        this.time = time;
+    }
+
+    public String getRealmId() {
+        return realmId;
+    }
+
+    public void setRealmId(String realmId) {
+        this.realmId = realmId;
+    }
+
+    public AuthDetailsRepresentation getAuthDetails() {
+        return authDetails;
+    }
+
+    public void setAuthDetails(AuthDetailsRepresentation authDetails) {
+        this.authDetails = authDetails;
+    }
+
+    public String getOperationType() {
+        return operationType;
+    }
+
+    public void setOperationType(String operationType) {
+        this.operationType = operationType;
+    }
+
+    public String getResourcePath() {
+        return resourcePath;
+    }
+
+    public void setResourcePath(String resourcePath) {
+        this.resourcePath = resourcePath;
+    }
+
+    public String getRepresentation() {
+        return representation;
+    }
+
+    public void setRepresentation(String representation) {
+        this.representation = representation;
+    }
+
+    public String getError() {
+        return error;
+    }
+
+    public void setError(String error) {
+        this.error = error;
+    }
+
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/AuthDetailsRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/AuthDetailsRepresentation.java
new file mode 100644
index 0000000..746aa71
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/AuthDetailsRepresentation.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.representations.idm;
+
+/**
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+public class AuthDetailsRepresentation {
+
+    private String realmId;
+    private String clientId;
+    private String userId;
+    private String ipAddress;
+
+    public String getRealmId() {
+        return realmId;
+    }
+
+    public void setRealmId(String realmId) {
+        this.realmId = realmId;
+    }
+
+    public String getClientId() {
+        return clientId;
+    }
+
+    public void setClientId(String clientId) {
+        this.clientId = clientId;
+    }
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public String getIpAddress() {
+        return ipAddress;
+    }
+
+    public void setIpAddress(String ipAddress) {
+        this.ipAddress = ipAddress;
+    }
+
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/EventRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/EventRepresentation.java
new file mode 100644
index 0000000..93be310
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/EventRepresentation.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.representations.idm;
+
+import java.util.Map;
+
+/**
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+public class EventRepresentation {
+
+    private long time;
+    private String type;
+    private String realmId;
+    private String clientId;
+    private String userId;
+    private String sessionId;
+    private String ipAddress;
+    private String error;
+    private Map<String, String> details;
+
+    public long getTime() {
+        return time;
+    }
+
+    public void setTime(long time) {
+        this.time = time;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public String getRealmId() {
+        return realmId;
+    }
+
+    public void setRealmId(String realmId) {
+        this.realmId = realmId;
+    }
+
+    public String getClientId() {
+        return clientId;
+    }
+
+    public void setClientId(String clientId) {
+        this.clientId = clientId;
+    }
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public String getSessionId() {
+        return sessionId;
+    }
+
+    public void setSessionId(String sessionId) {
+        this.sessionId = sessionId;
+    }
+
+    public String getIpAddress() {
+        return ipAddress;
+    }
+
+    public void setIpAddress(String ipAddress) {
+        this.ipAddress = ipAddress;
+    }
+
+    public String getError() {
+        return error;
+    }
+
+    public void setError(String error) {
+        this.error = error;
+    }
+
+    public Map<String, String> getDetails() {
+        return details;
+    }
+
+    public void setDetails(Map<String, String> details) {
+        this.details = details;
+    }
+}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
index 0242d24..87f6763 100755
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
@@ -19,6 +19,7 @@ package org.keycloak.admin.client.resource;
 
 import org.jboss.resteasy.annotations.cache.NoCache;
 import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.EventRepresentation;
 import org.keycloak.representations.idm.GroupRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
 
@@ -26,6 +27,8 @@ import javax.ws.rs.*;
 import javax.ws.rs.core.MediaType;
 import java.util.List;
 import java.util.Map;
+import org.keycloak.representations.idm.AdminEventRepresentation;
+import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
 
 /**
  * @author rodrigo.sasaki@icarros.com.br
@@ -61,6 +64,52 @@ public interface RealmResource {
     @Path("groups")
     GroupsResource groups();
 
+    @DELETE
+    @Path("events")
+    void clearEvents();
+
+    @GET
+    @Path("events")
+    @Produces(MediaType.APPLICATION_JSON)
+    List<EventRepresentation> getEvents();
+
+    @Path("events")
+    @GET
+    @NoCache
+    @Produces(MediaType.APPLICATION_JSON)
+    public List<EventRepresentation> getEvents(@QueryParam("type") List<String> types, @QueryParam("client") String client,
+            @QueryParam("user") String user, @QueryParam("dateFrom") String dateFrom, @QueryParam("dateTo") String dateTo,
+            @QueryParam("ipAddress") String ipAddress, @QueryParam("first") Integer firstResult,
+            @QueryParam("max") Integer maxResults);
+
+    @DELETE
+    @Path("admin-events")
+    void clearAdminEvents();
+
+    @GET
+    @Path("admin-events")
+    @Produces(MediaType.APPLICATION_JSON)
+    List<AdminEventRepresentation> getAdminEvents();
+
+    @GET
+    @Path("admin-events")
+    @Produces(MediaType.APPLICATION_JSON)
+    List<AdminEventRepresentation> getAdminEvents(@QueryParam("operationTypes") List<String> operationTypes, @QueryParam("authRealm") String authRealm, @QueryParam("authClient") String authClient,
+            @QueryParam("authUser") String authUser, @QueryParam("authIpAddress") String authIpAddress,
+            @QueryParam("resourcePath") String resourcePath, @QueryParam("dateFrom") String dateFrom,
+            @QueryParam("dateTo") String dateTo, @QueryParam("first") Integer firstResult,
+            @QueryParam("max") Integer maxResults);
+
+    @GET
+    @Path("events/config")
+    @Produces(MediaType.APPLICATION_JSON)
+    public RealmEventsConfigRepresentation getRealmEventsConfig();
+
+    @PUT
+    @Path("events/config")
+    @Consumes(MediaType.APPLICATION_JSON)
+    public void updateRealmEventsConfig(RealmEventsConfigRepresentation rep);
+
     @GET
     @Path("group-by-path/{path: .*}")
     @NoCache
diff --git a/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 868aa10..30f7d5f 100755
--- a/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -17,6 +17,9 @@
 
 package org.keycloak.models.utils;
 
+import org.keycloak.events.Event;
+import org.keycloak.events.admin.AdminEvent;
+import org.keycloak.events.admin.AuthDetails;
 import org.keycloak.models.AuthenticationExecutionModel;
 import org.keycloak.models.AuthenticationFlowModel;
 import org.keycloak.models.AuthenticatorConfigModel;
@@ -41,12 +44,15 @@ import org.keycloak.models.UserFederationProviderModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
 
+import org.keycloak.representations.idm.AdminEventRepresentation;
+import org.keycloak.representations.idm.AuthDetailsRepresentation;
 import org.keycloak.representations.idm.AuthenticationExecutionRepresentation;
 import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
 import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
 import org.keycloak.representations.idm.ClientRepresentation;
 import org.keycloak.representations.idm.ClientTemplateRepresentation;
 import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.representations.idm.EventRepresentation;
 import org.keycloak.representations.idm.FederatedIdentityRepresentation;
 import org.keycloak.representations.idm.GroupRepresentation;
 import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
@@ -167,6 +173,43 @@ public class ModelToRepresentation {
         return rep;
     }
 
+    public static EventRepresentation toRepresentation(Event event) {
+        EventRepresentation rep = new EventRepresentation();
+        rep.setTime(event.getTime());
+        rep.setType(event.getType().toString());
+        rep.setRealmId(event.getRealmId());
+        rep.setClientId(event.getClientId());
+        rep.setUserId(event.getUserId());
+        rep.setSessionId(event.getSessionId());
+        rep.setError(event.getError());
+        rep.setDetails(event.getDetails());
+        return rep;
+    }
+
+    public static AdminEventRepresentation toRepresentation(AdminEvent adminEvent) {
+        AdminEventRepresentation rep = new AdminEventRepresentation();
+        rep.setTime(adminEvent.getTime());
+        rep.setRealmId(adminEvent.getRealmId());
+        if (adminEvent.getAuthDetails() != null) {
+            rep.setAuthDetails(toRepresentation(adminEvent.getAuthDetails()));
+        }
+        rep.setOperationType(adminEvent.getOperationType().toString());
+        rep.setResourcePath(adminEvent.getResourcePath());
+        rep.setRepresentation(adminEvent.getRepresentation());
+        rep.setError(adminEvent.getError());
+
+        return rep;
+    }
+
+    public static AuthDetailsRepresentation toRepresentation(AuthDetails authDetails) {
+        AuthDetailsRepresentation rep = new AuthDetailsRepresentation();
+        rep.setRealmId(authDetails.getRealmId());
+        rep.setClientId(authDetails.getClientId());
+        rep.setUserId(authDetails.getUserId());
+        rep.setIpAddress(authDetails.getIpAddress());
+        return rep;
+    }
+
     public static RoleRepresentation toRepresentation(RoleModel role) {
         RoleRepresentation rep = new RoleRepresentation();
         rep.setId(role.getId());
@@ -383,15 +426,15 @@ public class ModelToRepresentation {
         if (realm.getEventsListeners() != null) {
             rep.setEventsListeners(new LinkedList<>(realm.getEventsListeners()));
         }
-        
+
         if(realm.getEnabledEventTypes() != null) {
             rep.setEnabledEventTypes(new LinkedList<>(realm.getEnabledEventTypes()));
         }
-        
+
         rep.setAdminEventsEnabled(realm.isAdminEventsEnabled());
-        
+
         rep.setAdminEventsDetailsEnabled(realm.isAdminEventsDetailsEnabled());
-        
+
         return rep;
     }
 
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index 22eb5db..15b0e81 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -45,8 +45,11 @@ import org.keycloak.models.utils.RepresentationToModel;
 import org.keycloak.protocol.oidc.TokenManager;
 import org.keycloak.provider.ProviderFactory;
 import org.keycloak.representations.adapters.action.GlobalRequestResult;
+import org.keycloak.representations.idm.AdminEventRepresentation;
 import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.EventRepresentation;
 import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.representations.idm.PartialImportRepresentation;
 import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.services.managers.AuthenticationManager;
@@ -75,6 +78,7 @@ import javax.ws.rs.core.UriInfo;
 
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.LinkedList;
@@ -82,7 +86,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.regex.PatternSyntaxException;
 import org.keycloak.partialimport.PartialImportManager;
-import org.keycloak.representations.idm.PartialImportRepresentation;
 
 /**
  * Base resource class for the admin REST api of one realm
@@ -428,6 +431,7 @@ public class RealmAdminResource {
      *
      * Returns all events, or filters them based on URL query parameters listed here
      *
+     * @param types The types of events to return
      * @param client App or oauth client name
      * @param user User id
      * @param ipAddress IP address
@@ -441,7 +445,7 @@ public class RealmAdminResource {
     @GET
     @NoCache
     @Produces(MediaType.APPLICATION_JSON)
-    public List<Event> getEvents(@QueryParam("client") String client,
+    public List<EventRepresentation> getEvents(@QueryParam("type") List<String> types, @QueryParam("client") String client,
             @QueryParam("user") String user, @QueryParam("dateFrom") String dateFrom, @QueryParam("dateTo") String dateTo,
             @QueryParam("ipAddress") String ipAddress, @QueryParam("first") Integer firstResult,
             @QueryParam("max") Integer maxResults) {
@@ -454,8 +458,7 @@ public class RealmAdminResource {
             query.client(client);
         }
 
-        List<String> types = uriInfo.getQueryParameters().get("type");
-        if (types != null) {
+        if (types != null & !types.isEmpty()) {
             EventType[] t = new EventType[types.size()];
             for (int i = 0; i < t.length; i++) {
                 t[i] = EventType.valueOf(types.get(i));
@@ -499,7 +502,15 @@ public class RealmAdminResource {
             query.maxResults(maxResults);
         }
 
-        return query.getResultList();
+        return toEventListRep(query.getResultList());
+    }
+
+    private List<EventRepresentation> toEventListRep(List<Event> events) {
+        List<EventRepresentation> reps = new ArrayList<>();
+        for (Event event : events) {
+            reps.add(ModelToRepresentation.toRepresentation(event));
+        }
+        return reps;
     }
 
     /**
@@ -507,6 +518,7 @@ public class RealmAdminResource {
      *
      * Returns all admin events, or filters events based on URL query parameters listed here
      *
+     * @param operationTypes
      * @param authRealm
      * @param authClient
      * @param authUser user id
@@ -514,7 +526,6 @@ public class RealmAdminResource {
      * @param resourcePath
      * @param dateTo
      * @param dateFrom
-     * @param resourcePath
      * @param firstResult
      * @param maxResults
      * @return
@@ -523,7 +534,7 @@ public class RealmAdminResource {
     @GET
     @NoCache
     @Produces(MediaType.APPLICATION_JSON)
-    public List<AdminEvent> getEvents(@QueryParam("authRealm") String authRealm, @QueryParam("authClient") String authClient,
+    public List<AdminEventRepresentation> getEvents(@QueryParam("operationTypes") List<String> operationTypes, @QueryParam("authRealm") String authRealm, @QueryParam("authClient") String authClient,
             @QueryParam("authUser") String authUser, @QueryParam("authIpAddress") String authIpAddress,
             @QueryParam("resourcePath") String resourcePath, @QueryParam("dateFrom") String dateFrom,
             @QueryParam("dateTo") String dateTo, @QueryParam("first") Integer firstResult,
@@ -553,8 +564,7 @@ public class RealmAdminResource {
             query.resourcePath(resourcePath);
         }
 
-        List<String> operationTypes = uriInfo.getQueryParameters().get("operationTypes");
-        if (operationTypes != null) {
+        if (operationTypes != null && !operationTypes.isEmpty()) {
             OperationType[] t = new OperationType[operationTypes.size()];
             for (int i = 0; i < t.length; i++) {
                 t[i] = OperationType.valueOf(operationTypes.get(i));
@@ -591,7 +601,16 @@ public class RealmAdminResource {
             query.maxResults(maxResults);
         }
 
-        return query.getResultList();
+        return toAdminEventRep(query.getResultList());
+    }
+
+    private List<AdminEventRepresentation> toAdminEventRep(List<AdminEvent> events) {
+        List<AdminEventRepresentation> reps = new ArrayList<>();
+        for (AdminEvent event : events) {
+            reps.add(ModelToRepresentation.toRepresentation(event));
+        }
+
+        return reps;
     }
 
     /**
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractDemoExampleAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractDemoExampleAdapterTest.java
index 345d053..92c5c24 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractDemoExampleAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractDemoExampleAdapterTest.java
@@ -17,6 +17,7 @@
 
 package org.keycloak.testsuite.adapter.example;
 
+import org.apache.commons.io.FileUtils;
 import org.jboss.arquillian.container.test.api.Deployment;
 import org.jboss.arquillian.graphene.page.Page;
 import org.jboss.shrinkwrap.api.spec.WebArchive;
@@ -26,6 +27,7 @@ import org.junit.Test;
 import org.keycloak.admin.client.resource.ClientResource;
 import org.keycloak.representations.idm.ClientRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest;
 import org.keycloak.testsuite.adapter.page.CustomerPortalExample;
 import org.keycloak.testsuite.adapter.page.DatabaseServiceExample;
@@ -43,6 +45,8 @@ import java.io.File;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -269,4 +273,103 @@ public abstract class AbstractDemoExampleAdapterTest extends AbstractExampleAdap
         resultList.get(0).findElement(By.xpath(".//td[text()='username']/../td[text()='bburke@redhat.com']"));
         resultList.get(0).findElement(By.xpath(".//td[text()='consent']/../td[text()='consent_granted']"));
     }
+
+    @Test
+    public void historyOfAccessResourceTest() throws IOException {
+        RealmRepresentation realm = testRealmResource().toRepresentation();
+        realm.setEventsEnabled(true);
+        realm.setEnabledEventTypes(Arrays.asList("LOGIN", "LOGIN_ERROR", "LOGOUT", "CODE_TO_TOKEN"));
+        testRealmResource().update(realm);
+
+        customerPortalExamplePage.navigateTo();
+        customerPortalExamplePage.customerListing();
+
+        testRealmLoginPage.form().login("bburke@redhat.com", "password");
+
+        Assert.assertTrue(driver.getPageSource().contains("Username: bburke@redhat.com")
+                        && driver.getPageSource().contains("Bill Burke")
+                        && driver.getPageSource().contains("Stian Thorgersen")
+        );
+
+        if (isRelative()) { //KEYCLOAK-1546
+            productPortalExamplePage.logOut();
+        } else {
+            driver.navigate().to(testRealmPage.getOIDCLogoutUrl() + "?redirect_uri=" + productPortalExamplePage);
+        }
+
+        loginEventsPage.navigateTo();
+
+        if (!testContext.isAdminLoggedIn()) {
+            loginPage.form().login(adminUser);
+            testContext.setAdminLoggedIn(true);
+        }
+
+        loginEventsPage.table().filter();
+        loginEventsPage.table().filterForm().addEventType("LOGOUT");
+        loginEventsPage.table().update();
+
+        List<WebElement> resultList = loginEventsPage.table().rows();
+
+        assertEquals(1, resultList.size());
+
+        resultList.get(0).findElement(By.xpath(".//td[text()='LOGOUT']"));
+        resultList.get(0).findElement(By.xpath(".//td[text()='Client']/../td[text()='']"));
+        resultList.get(0).findElement(By.xpath(".//td[text()='IP Address']/../td[text()='127.0.0.1']"));
+
+        loginEventsPage.table().reset();
+        loginEventsPage.table().filterForm().addEventType("LOGIN");
+        loginEventsPage.table().update();
+        resultList = loginEventsPage.table().rows();
+
+        assertEquals(1, resultList.size());
+
+        resultList.get(0).findElement(By.xpath(".//td[text()='LOGIN']"));
+        resultList.get(0).findElement(By.xpath(".//td[text()='Client']/../td[text()='customer-portal']"));
+        resultList.get(0).findElement(By.xpath(".//td[text()='IP Address']/../td[text()='127.0.0.1']"));
+        resultList.get(0).findElement(By.xpath(".//td[text()='username']/../td[text()='bburke@redhat.com']"));
+
+        loginEventsPage.table().reset();
+        loginEventsPage.table().filterForm().addEventType("CODE_TO_TOKEN");
+        loginEventsPage.table().update();
+        resultList = loginEventsPage.table().rows();
+
+        assertEquals(1, resultList.size());
+        resultList.get(0).findElement(By.xpath(".//td[text()='CODE_TO_TOKEN']"));
+        resultList.get(0).findElement(By.xpath(".//td[text()='Client']/../td[text()='customer-portal']"));
+        resultList.get(0).findElement(By.xpath(".//td[text()='IP Address']/../td[text()='127.0.0.1']"));
+        resultList.get(0).findElement(By.xpath(".//td[text()='refresh_token_type']/../td[text()='Refresh']"));
+
+        String serverLogPath = null;
+
+        if (System.getProperty("app.server.wildfly", "false").equals("true")) {
+            serverLogPath = System.getProperty("app.server.wildfly.home") + "/standalone/log/server.log";
+        }
+
+        if (System.getProperty("app.server.eap6", "false").equals("true")) {
+            serverLogPath = System.getProperty("app.server.eap6.home") + "/standalone/log/server.log";
+        }
+
+        if (System.getProperty("app.server.eap7", "false").equals("true")) {
+            serverLogPath = System.getProperty("app.server.eap7.home") + "/standalone/log/server.log";
+        }
+
+        String appServerUrl;
+        if (Boolean.parseBoolean(System.getProperty("app.server.ssl.required"))) {
+            appServerUrl = "https://localhost:" + System.getProperty("app.server.https.port", "8543") + "/";
+        } else {
+            appServerUrl = "http://localhost:" + System.getProperty("app.server.http.port", "8280") + "/";
+        }
+
+        if (serverLogPath != null) {
+            File serverLog = new File(serverLogPath);
+            String serverLogContent = FileUtils.readFileToString(serverLog);
+            UserRepresentation bburke = ApiUtil.findUserByUsername(testRealmResource(), "bburke@redhat.com");
+
+            Pattern pattern = Pattern.compile("User '" + bburke.getId() + "' invoking '" + appServerUrl + "customer-portal\\/customers\\/view\\.jsp[^\\s]+' on client 'customer-portal'");
+            Matcher matcher = pattern.matcher(serverLogContent);
+
+            assertTrue(matcher.find());
+            assertTrue(serverLogContent.contains("User '" + bburke.getId() + "' invoking '" + appServerUrl + "database/customers' on client 'database-service'"));
+        }
+    }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/event/AbstractEventTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/event/AbstractEventTest.java
new file mode 100644
index 0000000..77c61b3
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/event/AbstractEventTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.event;
+
+import java.util.Collections;
+import org.junit.Before;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
+import org.keycloak.testsuite.AbstractAuthTest;
+
+/**
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+public abstract class AbstractEventTest extends AbstractAuthTest {
+
+    protected RealmEventsConfigRepresentation configRep;
+
+    @Before
+    public void setConfigRep() {
+        RealmResource testRsc = testRealmResource();
+        configRep = testRsc.getRealmEventsConfig();
+        configRep.setAdminEventsDetailsEnabled(false);
+        configRep.setAdminEventsEnabled(false);
+        configRep.setEventsEnabled(false);
+        configRep.setEnabledEventTypes(Collections.EMPTY_LIST); // resets to all types
+        saveConfig();
+    }
+
+    protected void saveConfig() {
+        RealmResource testRsc = testRealmResource();
+        testRsc.updateRealmEventsConfig(configRep);
+        configRep = testRsc.getRealmEventsConfig();
+    }
+
+    protected void enableEvents() {
+        configRep.setEventsEnabled(true);
+        configRep.setAdminEventsEnabled(Boolean.TRUE);
+
+        saveConfig();
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/event/AdminEventTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/event/AdminEventTest.java
new file mode 100644
index 0000000..bdf699a
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/event/AdminEventTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.event;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.representations.idm.AdminEventRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.admin.ApiUtil;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test getting and filtering admin events.
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+public class AdminEventTest extends AbstractEventTest {
+
+    @Before
+    public void initConfig() {
+        enableEvents();
+        testRealmResource().clearAdminEvents();
+    }
+
+    private List<AdminEventRepresentation> events() {
+        return testRealmResource().getAdminEvents();
+    }
+
+    private void createUser(String username) {
+        UserRepresentation user = createUserRepresentation(username, username + "@foo.com", "foo", "bar", true);
+        ApiUtil.createUserWithAdminClient(testRealmResource(), user);
+    }
+
+    private void updateRealm() {
+        RealmRepresentation realm = testRealmResource().toRepresentation();
+        realm.setDisplayName("Fury Road");
+        testRealmResource().update(realm);
+    }
+
+    private String realmName() {
+        return testRealmResource().toRepresentation().getId();
+    }
+
+    @Test
+    public void clearAdminEventsTest() {
+        createUser("user0");
+        assertEquals(1, events().size());
+        testRealmResource().clearAdminEvents();
+        assertEquals(Collections.EMPTY_LIST, events());
+    }
+
+    @Test
+    public void retrieveAdminEventTest() {
+        createUser("user1");
+        List<AdminEventRepresentation> events = events();
+
+        assertEquals(1, events.size());
+        AdminEventRepresentation event = events().get(0);
+        assertEquals("CREATE", event.getOperationType());
+
+        assertEquals(realmName(), event.getRealmId());
+        assertEquals(realmName(), event.getAuthDetails().getRealmId());
+        assertNull(event.getRepresentation());
+    }
+
+    @Test
+    public void testGetRepresentation() {
+        configRep.setAdminEventsDetailsEnabled(Boolean.TRUE);
+        saveConfig();
+
+        createUser("user2");
+        AdminEventRepresentation event = events().get(0);
+        assertNotNull(event.getRepresentation());
+        assertTrue(event.getRepresentation().contains("foo"));
+    }
+
+    @Test
+    public void testFilterAdminEvents() {
+        // two CREATE and one UPDATE
+        createUser("user3");
+        createUser("user4");
+        updateRealm();
+        assertEquals(3, events().size());
+
+        List<AdminEventRepresentation> events = testRealmResource().getAdminEvents(Arrays.asList("CREATE"), realmName(), null, null, null, null, null, null, null, null);
+        assertEquals(2, events.size());
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/event/EventConfigTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/event/EventConfigTest.java
new file mode 100644
index 0000000..a2bf746
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/event/EventConfigTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.event;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test updates to the events configuration.
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+public class EventConfigTest extends AbstractEventTest {
+
+    @Test
+    public void defaultEventConfigTest() {
+        assertFalse(configRep.isAdminEventsDetailsEnabled());
+        assertFalse(configRep.isAdminEventsEnabled());
+        assertFalse(configRep.isEventsEnabled());
+
+        List<String> eventListeners = configRep.getEventsListeners();
+        assertEquals(1, eventListeners.size());
+        assertEquals("jboss-logging", eventListeners.get(0));
+    }
+
+    @Test
+    public void enableEventsTest() {
+        enableEvents();
+
+        assertTrue(configRep.isEventsEnabled());
+        assertTrue(configRep.isAdminEventsEnabled());
+    }
+
+    @Test
+    public void addRemoveListenerTest() {
+        configRep.setEventsListeners(Collections.EMPTY_LIST);
+        saveConfig();
+        assertEquals(0, configRep.getEventsListeners().size());
+
+        configRep.setEventsListeners(Arrays.asList("email"));
+        saveConfig();
+        List<String> eventListeners = configRep.getEventsListeners();
+        assertEquals(1, eventListeners.size());
+        assertEquals("email", eventListeners.get(0));
+    }
+
+    @Test
+    public void loginEventSettingsTest() {
+        enableEvents();
+
+        assertTrue(hasEventType("LOGIN"));
+        assertTrue(hasEventType("LOGOUT"));
+        assertTrue(hasEventType("CLIENT_DELETE_ERROR"));
+
+        int defaultEventCount = configRep.getEnabledEventTypes().size();
+
+        configRep.setEnabledEventTypes(Arrays.asList("CLIENT_DELETE", "CLEINT_DELETE_ERROR"));
+        saveConfig();
+
+        List<String> enabledEventTypes = configRep.getEnabledEventTypes();
+        assertEquals(2, enabledEventTypes.size());
+
+        // remove all event types
+        configRep.setEnabledEventTypes(Collections.EMPTY_LIST);
+        saveConfig();
+
+        // removing all event types restores default events
+        assertEquals(defaultEventCount, configRep.getEnabledEventTypes().size());
+    }
+
+    private boolean hasEventType(String eventType) {
+        for (String event : configRep.getEnabledEventTypes()) {
+            if (eventType.equals(event)) return true;
+        }
+
+        return false;
+    }
+
+    @Test
+    public void includeRepresentationTest() {
+        enableEvents();
+
+        assertTrue(configRep.isAdminEventsEnabled());
+        assertFalse(configRep.isAdminEventsDetailsEnabled());
+
+        configRep.setAdminEventsDetailsEnabled(Boolean.TRUE);
+        saveConfig();
+
+        assertTrue(configRep.isAdminEventsDetailsEnabled());
+    }
+
+    @Test
+    public void setLoginEventExpirationTest() {
+        enableEvents();
+
+        assertNull(configRep.getEventsExpiration());
+
+        Long oneHour = 3600L;
+        configRep.setEventsExpiration(oneHour);
+        saveConfig();
+
+        assertEquals(oneHour, configRep.getEventsExpiration());
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/event/LoginEventsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/event/LoginEventsTest.java
new file mode 100644
index 0000000..142bfff
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/event/LoginEventsTest.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.event;
+
+import java.util.Arrays;
+import java.util.List;
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.representations.idm.EventRepresentation;
+import org.keycloak.testsuite.console.page.events.LoginEvents;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * Test getting and filtering login-related events.
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+public class LoginEventsTest extends AbstractEventTest {
+
+    @Page
+    private LoginEvents loginEventsPage;
+
+    @Before
+    public void init() {
+        configRep.setEventsEnabled(true);
+        saveConfig();
+        testRealmResource().clearEvents();
+    }
+
+    private List<EventRepresentation> events() {
+        return testRealmResource().getEvents();
+    }
+
+    private void badLogin() {
+        loginEventsPage.navigateTo();
+        loginPage.form().login("bad", "user");
+    }
+
+    private void pause(int seconds) {
+        try {
+            Thread.sleep(seconds * 1000L);
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+    }
+
+    @Test
+    public void clearEventsTest() {
+        assertEquals(0, events().size());
+        badLogin();
+        badLogin();
+        assertEquals(2, events().size());
+        testRealmResource().clearEvents();
+        assertEquals(0, events().size());
+    }
+
+    @Test
+    public void loggingOfCertainTypeTest() {
+        assertEquals(0, events().size());
+        configRep.setEnabledEventTypes(Arrays.asList("REVOKE_GRANT"));
+        saveConfig();
+
+        badLogin();
+        assertEquals(0, events().size());
+
+        configRep.setEnabledEventTypes(Arrays.asList("LOGIN_ERROR"));
+        saveConfig();
+
+        badLogin();
+        assertEquals(1, events().size());
+    }
+
+    @Test
+    public void filterTest() {
+        badLogin();
+        badLogin();
+        assertEquals(2, events().size());
+
+        List<EventRepresentation> filteredEvents = testRealmResource().getEvents(Arrays.asList("REVOKE_GRANT"), null, null, null, null, null, null, null);
+        assertEquals(0, filteredEvents.size());
+
+        filteredEvents = testRealmResource().getEvents(Arrays.asList("LOGIN_ERROR"), null, null, null, null, null, null, null);
+        assertEquals(2, filteredEvents.size());
+    }
+
+    /*
+    Removed this test because it takes too long.  The default interval for
+    event cleanup is 15 minutes (900 seconds).  I don't have time to figure out
+    a way to set the cleanup thread to a lower interval for testing.
+    @Test
+    public void eventExpirationTest() {
+        configRep.setEventsExpiration(1L); //  second
+        saveConfig();
+        badLogin();
+        assertEquals(1, events().size());
+        pause(900); // pause 900 seconds
+        assertEquals(0, events().size());
+    }**/
+
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/wildfly/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/wildfly/pom.xml
index 26b0c48..09f71a2 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/wildfly/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/wildfly/pom.xml
@@ -117,6 +117,32 @@
                     </systemPropertyVariables>
                 </configuration>
             </plugin>
+
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>xml-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>configure-adapter-debug-log</id>
+                        <phase>process-resources</phase>
+                        <goals>
+                            <goal>transform</goal>
+                        </goals>
+                        <configuration>
+                            <transformationSets>
+                                <transformationSet>
+                                    <dir>${app.server.wildfly.home}/standalone/configuration</dir>
+                                    <includes>
+                                        <include>standalone.xml</include>
+                                    </includes>
+                                    <stylesheet>src/main/xslt/add-adapter-log-level.xsl</stylesheet>
+                                    <outputDir>${app.server.wildfly.home}/standalone/configuration</outputDir>
+                                </transformationSet>
+                            </transformationSets>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
         </plugins>
     </build>
     
diff --git a/testsuite/integration-arquillian/tests/other/adapters/wildfly/src/main/xslt/add-adapter-log-level.xsl b/testsuite/integration-arquillian/tests/other/adapters/wildfly/src/main/xslt/add-adapter-log-level.xsl
new file mode 100644
index 0000000..39cec8b
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/wildfly/src/main/xslt/add-adapter-log-level.xsl
@@ -0,0 +1,50 @@
+<!--
+  ~ 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.
+  -->
+
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+                xmlns:xalan="http://xml.apache.org/xalan"
+                xmlns:j="urn:jboss:domain:4.0"
+                xmlns:ds="urn:jboss:domain:datasources:4.0"
+                xmlns:k="urn:jboss:domain:keycloak:1.1"
+                xmlns:sec="urn:jboss:domain:security:1.2"
+                version="2.0"
+                exclude-result-prefixes="xalan j ds k sec">
+
+    <xsl:param name="config"/>
+
+    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" xalan:indent-amount="4" standalone="no"/>
+    <xsl:strip-space elements="*"/>
+
+    <xsl:variable name="nsDS" select="'urn:jboss:domain:logging:'"/>
+
+    <xsl:template match="//*[local-name()='subsystem' and starts-with(namespace-uri(), $nsDS)]
+                        /*[local-name()='root-logger' and starts-with(namespace-uri(), $nsDS)]">
+        <logger category="org.keycloak.adapters">
+            <level name="DEBUG"/>
+        </logger>
+        <xsl:copy>
+            <xsl:apply-templates select="@* | node()" />
+        </xsl:copy>
+    </xsl:template>
+
+    <xsl:template match="@*|node()">
+        <xsl:copy>
+            <xsl:apply-templates select="@*|node()" />
+        </xsl:copy>
+    </xsl:template>
+
+</xsl:stylesheet>
\ No newline at end of file