Added audit log to account mngmt

This commit is contained in:
Stian Thorgersen 2014-04-03 16:27:22 +01:00
parent e6067c915d
commit 3433227fa7
24 changed files with 237 additions and 46 deletions

View file

@ -1,5 +1,6 @@
package org.keycloak.audit.jpa;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
@ -26,6 +27,7 @@ public class EventEntity {
private String error;
@Column(length = 2550)
private String detailsJson;
public String getId() {

View file

@ -77,7 +77,7 @@ public class JpaEventQuery implements EventQuery {
cq.where(cb.and(predicates.toArray(new Predicate[predicates.size()])));
}
cq.orderBy(cb.asc(root.get("time")), cb.asc(root.get("id")));
cq.orderBy(cb.desc(root.get("time")));
TypedQuery<EventEntity> query = em.createQuery(cq);

View file

@ -62,7 +62,7 @@ public class MongoEventQuery implements EventQuery {
@Override
public List<Event> getResultList() {
DBCursor cur = audit.find(query);
DBCursor cur = audit.find(query).sort(new BasicDBObject("time", -1));
if (firstResult != null) {
cur.skip(firstResult);
}

View file

@ -46,10 +46,13 @@ public abstract class AbstractAuditProviderTest {
@Test
public void query() {
long oldest = System.currentTimeMillis() - 30000;
long newest = System.currentTimeMillis() + 30000;
provider.onEvent(create("event", "realmId", "clientId", "userId", "127.0.0.1", "error"));
provider.onEvent(create("event2", "realmId", "clientId", "userId", "127.0.0.1", "error"));
provider.onEvent(create(newest, "event2", "realmId", "clientId", "userId", "127.0.0.1", "error"));
provider.onEvent(create("event", "realmId2", "clientId", "userId", "127.0.0.1", "error"));
provider.onEvent(create("event", "realmId", "clientId2", "userId", "127.0.0.1", "error"));
provider.onEvent(create(oldest, "event", "realmId", "clientId2", "userId", "127.0.0.1", "error"));
provider.onEvent(create("event", "realmId", "clientId", "userId2", "127.0.0.1", "error"));
provider.close();
@ -65,6 +68,9 @@ public abstract class AbstractAuditProviderTest {
Assert.assertEquals(2, provider.createQuery().maxResults(2).getResultList().size());
Assert.assertEquals(1, provider.createQuery().firstResult(4).getResultList().size());
Assert.assertEquals(newest, provider.createQuery().maxResults(1).getResultList().get(0).getTime());
Assert.assertEquals(oldest, provider.createQuery().firstResult(4).maxResults(1).getResultList().get(0).getTime());
}
@Test

View file

@ -13,22 +13,23 @@ import java.util.List;
*/
public interface Account {
public Response createResponse(AccountPages page);
Response createResponse(AccountPages page);
public Account setError(String message);
Account setError(String message);
public Account setSuccess(String message);
Account setSuccess(String message);
public Account setWarning(String message);
Account setWarning(String message);
public Account setUser(UserModel user);
Account setUser(UserModel user);
public Account setStatus(Response.Status status);
Account setStatus(Response.Status status);
public Account setRealm(RealmModel realm);
Account setRealm(RealmModel realm);
public Account setReferrer(String[] referrer);
Account setReferrer(String[] referrer);
public Account setEvents(List<Event> events);
Account setEvents(List<Event> events);
Account setFeatures(boolean social, boolean audit);
}

View file

@ -5,6 +5,7 @@ import org.keycloak.account.Account;
import org.keycloak.account.AccountPages;
import org.keycloak.account.freemarker.model.AccountBean;
import org.keycloak.account.freemarker.model.AccountSocialBean;
import org.keycloak.account.freemarker.model.FeaturesBean;
import org.keycloak.account.freemarker.model.LogBean;
import org.keycloak.account.freemarker.model.MessageBean;
import org.keycloak.account.freemarker.model.ReferrerBean;
@ -41,6 +42,8 @@ public class FreeMarkerAccount implements Account {
private RealmModel realm;
private String[] referrer;
private List<Event> events;
private boolean social;
private boolean audit;
public static enum MessageType {SUCCESS, WARNING, ERROR}
@ -92,9 +95,7 @@ public class FreeMarkerAccount implements Account {
attributes.put("url", new UrlBean(realm, theme, baseUri));
if (realm.isSocial()) {
attributes.put("isSocialRealm", true);
}
attributes.put("features", new FeaturesBean(social, audit));
switch (page) {
case ACCOUNT:
@ -170,4 +171,10 @@ public class FreeMarkerAccount implements Account {
return this;
}
@Override
public Account setFeatures(boolean social, boolean audit) {
this.social = social;
this.audit = audit;
return this;
}
}

View file

@ -0,0 +1,24 @@
package org.keycloak.account.freemarker.model;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class FeaturesBean {
private final boolean social;
private final boolean log;
public FeaturesBean(boolean social, boolean log) {
this.social = social;
this.log = log;
}
public boolean isSocial() {
return social;
}
public boolean isLog() {
return log;
}
}

View file

@ -41,6 +41,10 @@ public class UrlBean {
return Urls.accountTotpPage(baseURI, realm).toString();
}
public String getLogUrl() {
return Urls.accountLogPage(baseURI, realm).toString();
}
public String getTotpRemoveUrl() {
return Urls.accountTotpRemove(baseURI, realm).toString();
}

View file

@ -1,28 +1,32 @@
<#import "template.ftl" as layout>
<@layout.mainLayout active='social' bodyClass='social'; section>
<@layout.mainLayout active='log' bodyClass='log'; section>
<div class="row">
<div class="col-md-10">
<h2>Social Accounts</h2>
<h2>Account Log</h2>
</div>
</div>
<table>
<th>
<td>${event.date}</td>
<td>${event.event}</td>
<td>${event.ipAddress}</td>
<td>${event.clientId}</td>
</th>
<table class="table">
<thead>
<tr>
<td>Date</td>
<td>Event</td>
<td>IP</td>
<td>Client</td>
</tr>
</thead>
<tbody>
<#list log.events as event>
<tr>
<td>${event.date}</td>
<td>${event.date?datetime}</td>
<td>${event.event}</td>
<td>${event.ipAddress}</td>
<td>${event.clientId}</td
<td>${event.client}</td
</tr>
</#list>
</tbody>
</table>
</@layout.mainLayout>

View file

@ -42,7 +42,8 @@
<li class="<#if active=='account'>active</#if>"><a href="${url.accountUrl}">Account</a></li>
<li class="<#if active=='password'>active</#if>"><a href="${url.passwordUrl}">Password</a></li>
<li class="<#if active=='totp'>active</#if>"><a href="${url.totpUrl}">Authenticator</a></li>
<#if isSocialRealm?has_content><li class="<#if active=='social'>active</#if>"><a href="${url.socialUrl}">Social</a></li></#if>
<#if features.social><li class="<#if active=='social'>active</#if>"><a href="${url.socialUrl}">Social</a></li></#if>
<#if features.log><li class="<#if active=='log'>active</#if>"><a href="${url.logUrl}">Log</a></li></#if>
</ul>
</div>

View file

@ -30,7 +30,7 @@ public class Config {
}
public static String getAuditProvider() {
return System.getProperty(MODEL_PROVIDER_KEY);
return System.getProperty(MODEL_PROVIDER_KEY, "jpa");
}
public static void setAuditProvider(String provider) {

View file

@ -11,24 +11,16 @@ import java.util.Set;
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class DefaultProviderSession implements ProviderSession {
private ProviderSessionFactory factory;
private DefaultProviderSessionFactory factory;
private Map<Integer, Provider> providers = new HashMap<Integer, Provider>();
public DefaultProviderSession(ProviderSessionFactory factory) {
public DefaultProviderSession(DefaultProviderSessionFactory factory) {
this.factory = factory;
}
public <T extends Provider> T getProvider(Class<T> clazz) {
Integer hash = clazz.hashCode();
T provider = (T) providers.get(hash);
if (provider == null) {
ProviderFactory<T> providerFactory = factory.getProviderFactory(clazz);
if (providerFactory != null) {
provider = providerFactory.create();
providers.put(hash, provider);
}
}
return provider;
String id = factory.getDefaultProvider(clazz);
return id != null ? getProvider(clazz, id) : null;
}
public <T extends Provider> T getProvider(Class<T> clazz, String id) {

View file

@ -41,6 +41,10 @@ public class DefaultProviderSessionFactory implements ProviderSessionFactory {
return loader != null ? loader.providerIds() : null;
}
public String getDefaultProvider(Class<? extends Provider> clazz) {
return defaultFactories.get(clazz);
}
public void registerLoader(Class<? extends Provider> clazz, ProviderFactoryLoader loader) {
loaders.put(clazz, loader);

View file

@ -110,6 +110,7 @@ public class AccountService {
private final SocialRequestManager socialRequestManager;
private Account account;
private Auth auth;
private AuditProvider auditProvider;
public AccountService(RealmModel realm, ApplicationModel application, TokenManager tokenManager, SocialRequestManager socialRequestManager, Audit audit) {
this.realm = realm;
@ -120,7 +121,9 @@ public class AccountService {
}
public void init() {
account = AccountLoader.load().createAccount(uriInfo).setRealm(realm);
auditProvider = providers.getProvider(AuditProvider.class);
account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setFeatures(realm.isSocial(), auditProvider != null);
auth = authManager.authenticate(realm, headers);
if (auth != null) {
@ -133,7 +136,6 @@ public class AccountService {
return base;
}
private Response forwardToPage(String path, AccountPages page) {
if (auth != null) {
try {
@ -195,9 +197,10 @@ public class AccountService {
@Path("log")
@GET
public Response logPage() {
AuditProvider audit = providers.getProvider(AuditProvider.class);
List<Event> events = audit.createQuery().user(auth.getUser().getId()).maxResults(20).getResultList();
account.setEvents(events);
if (auth != null) {
List<Event> events = auditProvider.createQuery().user(auth.getUser().getId()).maxResults(20).getResultList();
account.setEvents(events);
}
return forwardToPage("log", AccountPages.LOG);
}

View file

@ -74,6 +74,10 @@ public class Urls {
return accountBase(baseUri).path(AccountService.class, "processTotpRemove").build(realmId);
}
public static URI accountLogPage(URI baseUri, String realmId) {
return accountBase(baseUri).path(AccountService.class, "logPage").build(realmId);
}
public static URI accountLogout(URI baseUri, String realmId) {
return accountBase(baseUri).path(AccountService.class, "logout").build(realmId);
}

View file

@ -42,6 +42,11 @@
<artifactId>keycloak-audit-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-audit-jpa</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-audit-jboss-logging</artifactId>

View file

@ -32,6 +32,24 @@
</properties>
</persistence-unit>
<persistence-unit name="jpa-keycloak-audit-store" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<class>org.keycloak.audit.jpa.EventEntity</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
<properties>
<property name="hibernate.connection.url" value="jdbc:h2:mem:test"/>
<property name="hibernate.connection.driver_class" value="org.h2.Driver"/>
<property name="hibernate.connection.username" value="sa"/>
<property name="hibernate.connection.password" value=""/>
<property name="hibernate.hbm2ddl.auto" value="create-drop" />
<property name="hibernate.show_sql" value="false" />
<property name="hibernate.format_sql" value="true" />
</properties>
</persistence-unit>
<!--
<persistence-unit name="picketlink-keycloak-identity-store" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>

View file

@ -28,6 +28,8 @@ import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.audit.Details;
import org.keycloak.audit.Event;
import org.keycloak.audit.jpa.JpaAuditProviderFactory;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
@ -38,6 +40,7 @@ import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.pages.AccountLogPage;
import org.keycloak.testsuite.pages.AccountPasswordPage;
import org.keycloak.testsuite.pages.AccountTotpPage;
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
@ -45,12 +48,18 @@ import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.RegisterPage;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
import org.openqa.selenium.WebDriver;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -96,6 +105,9 @@ public class AccountTest {
@WebResource
protected LoginPage loginPage;
@WebResource
protected RegisterPage registerPage;
@WebResource
protected AccountPasswordPage changePasswordPage;
@ -105,6 +117,9 @@ public class AccountTest {
@WebResource
protected AccountTotpPage totpPage;
@WebResource
protected AccountLogPage logPage;
@WebResource
protected ErrorPage errorPage;
@ -130,6 +145,8 @@ public class AccountTest {
appRealm.updateCredential(user, cred);
}
});
System.out.println(JpaAuditProviderFactory.class);
}
@Test
@ -315,4 +332,43 @@ public class AccountTest {
Assert.assertEquals("No access", errorPage.getError());
}
@Test
public void viewLog() {
List<Event> e = new LinkedList<Event>();
loginPage.open();
loginPage.clickRegister();
registerPage.register("view", "log", "view-log@localhost", "view-log", "password", "password");
e.add(events.poll());
e.add(events.poll());
profilePage.open();
profilePage.updateProfile("view", "log2", "view-log@localhost");
e.add(events.poll());
logPage.open();
e.add(events.poll());
Collections.reverse(e);
Assert.assertTrue(logPage.isCurrent());
List<List<String>> actual = logPage.getEvents();
Assert.assertEquals(e.size(), actual.size());
Iterator<List<String>> itr = actual.iterator();
for (Event event : e) {
List<String> a = itr.next();
Assert.assertEquals(event.getEvent().replace('_', ' '), a.get(1));
Assert.assertEquals(event.getIpAddress(), a.get(2));
Assert.assertEquals(event.getClientId(), a.get(3));
}
}
}

View file

@ -0,0 +1,60 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.keycloak.testsuite.pages;
import org.keycloak.testsuite.Constants;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import java.util.LinkedList;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class AccountLogPage extends AbstractAccountPage {
private static String PATH = Constants.AUTH_SERVER_ROOT + "/rest/realms/test/account/log";
public boolean isCurrent() {
return driver.getTitle().contains("Account Management") && driver.getCurrentUrl().endsWith("/account/log");
}
public void open() {
driver.navigate().to(PATH);
}
public List<List<String>> getEvents() {
List<List<String>> table = new LinkedList<List<String>>();
for (WebElement r : driver.findElements(By.tagName("tr"))) {
List<String> row = new LinkedList<String>();
for (WebElement col : r.findElements(By.tagName("td"))) {
row.add(col.getText());
}
table.add(row);
}
table.remove(0);
return table;
}
}