Merge pull request #315 from stianst/master
Added audit log to account mngmt
This commit is contained in:
commit
42975f0edf
41 changed files with 756 additions and 139 deletions
|
@ -20,24 +20,15 @@ public class Audit {
|
|||
private List<AuditListener> listeners;
|
||||
private Event event;
|
||||
|
||||
public static Audit create(RealmModel realm, String ipAddress) {
|
||||
ProviderFactoryLoader<AuditListenerFactory> loader = ProviderFactoryLoader.load(AuditListenerFactory.class);
|
||||
public Audit(List<AuditListener> listeners, RealmModel realm, String ipAddress) {
|
||||
this.listeners = listeners;
|
||||
this.event = new Event();
|
||||
|
||||
List<AuditListener> listeners = null;
|
||||
if (realm.getAuditListeners() != null) {
|
||||
listeners = new LinkedList<AuditListener>();
|
||||
|
||||
for (String id : realm.getAuditListeners()) {
|
||||
listeners.add(loader.find(id).create());
|
||||
}
|
||||
}
|
||||
|
||||
return new Audit(listeners, new Event()).realm(realm).ipAddress(ipAddress);
|
||||
realm(realm);
|
||||
ipAddress(ipAddress);
|
||||
}
|
||||
|
||||
private Audit(List<AuditListener> listeners, Event event) {
|
||||
this.listeners = listeners;
|
||||
this.event = event;
|
||||
Audit() {
|
||||
}
|
||||
|
||||
public Audit realm(RealmModel realm) {
|
||||
|
@ -113,7 +104,10 @@ public class Audit {
|
|||
}
|
||||
|
||||
public Audit clone() {
|
||||
return new Audit(listeners, event.clone());
|
||||
Audit clone = new Audit();
|
||||
clone.listeners = listeners;
|
||||
clone.event = event.clone();
|
||||
return clone;
|
||||
}
|
||||
|
||||
public Audit reset() {
|
||||
|
|
|
@ -7,7 +7,7 @@ import java.util.List;
|
|||
*/
|
||||
public interface EventQuery {
|
||||
|
||||
public EventQuery event(String event);
|
||||
public EventQuery event(String... events);
|
||||
|
||||
public EventQuery realm(String realmId);
|
||||
|
||||
|
|
|
@ -31,4 +31,9 @@ public class JBossLoggingAuditListenerFactory implements AuditListenerFactory {
|
|||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean lazyLoad() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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() {
|
|
@ -34,4 +34,9 @@ public class JpaAuditProviderFactory implements AuditProviderFactory {
|
|||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean lazyLoad() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -36,8 +36,8 @@ public class JpaEventQuery implements EventQuery {
|
|||
}
|
||||
|
||||
@Override
|
||||
public EventQuery event(String event) {
|
||||
predicates.add(cb.equal(root.get("event"), event));
|
||||
public EventQuery event(String... events) {
|
||||
predicates.add(root.get("event").in(events));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
|
@ -47,4 +47,9 @@ public class MongoAuditProviderFactory implements AuditProviderFactory {
|
|||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean lazyLoad() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@ public class MongoEventQuery implements EventQuery {
|
|||
}
|
||||
|
||||
@Override
|
||||
public EventQuery event(String event) {
|
||||
query.put("event", event);
|
||||
public EventQuery event(String... events) {
|
||||
query.put("event", new BasicDBObject("$in", events));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import org.junit.Test;
|
|||
import org.keycloak.audit.AuditProvider;
|
||||
import org.keycloak.audit.AuditProviderFactory;
|
||||
import org.keycloak.audit.Event;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.ProviderFactoryLoader;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
@ -17,12 +18,12 @@ import java.util.Map;
|
|||
*/
|
||||
public abstract class AbstractAuditProviderTest {
|
||||
|
||||
private AuditProviderFactory factory;
|
||||
private ProviderFactory<AuditProvider> factory;
|
||||
private AuditProvider provider;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
ProviderFactoryLoader<AuditProviderFactory> loader = ProviderFactoryLoader.load(AuditProviderFactory.class);
|
||||
ProviderFactoryLoader<AuditProvider> loader = ProviderFactoryLoader.create(AuditProviderFactory.class);
|
||||
factory = loader.find(getProviderId());
|
||||
factory.init();
|
||||
|
||||
|
@ -45,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();
|
||||
|
@ -57,12 +61,16 @@ public abstract class AbstractAuditProviderTest {
|
|||
Assert.assertEquals(4, provider.createQuery().client("clientId").getResultList().size());
|
||||
Assert.assertEquals(4, provider.createQuery().realm("realmId").getResultList().size());
|
||||
Assert.assertEquals(4, provider.createQuery().event("event").getResultList().size());
|
||||
Assert.assertEquals(5, provider.createQuery().event("event", "event2").getResultList().size());
|
||||
Assert.assertEquals(4, provider.createQuery().user("userId").getResultList().size());
|
||||
|
||||
Assert.assertEquals(1, provider.createQuery().user("userId").event("event2").getResultList().size());
|
||||
|
||||
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
|
||||
|
|
|
@ -13,4 +13,6 @@ public interface ProviderFactory<T extends Provider> {
|
|||
|
||||
public String getId();
|
||||
|
||||
public boolean lazyLoad();
|
||||
|
||||
}
|
||||
|
|
|
@ -1,88 +1,100 @@
|
|||
package org.keycloak.provider;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class ProviderFactoryLoader<P extends ProviderFactory> implements Iterable<P> {
|
||||
public class ProviderFactoryLoader<T extends Provider> implements Iterable<ProviderFactory<T>> {
|
||||
|
||||
private ServiceLoader<P> serviceLoader;
|
||||
private Map<String, ProviderFactory<T>> factories = new HashMap<String, ProviderFactory<T>>();
|
||||
|
||||
private ProviderFactoryLoader(ServiceLoader<P> serviceLoader) {
|
||||
this.serviceLoader = serviceLoader;
|
||||
private ProviderFactoryLoader(ServiceLoader<? extends ProviderFactory> serviceLoader) {
|
||||
for (ProviderFactory p : serviceLoader) {
|
||||
if (!System.getProperties().containsKey(p.getClass().getName() + ".disabled")) {
|
||||
if (p.lazyLoad()) {
|
||||
p = new LazyProviderFactory(p);
|
||||
}
|
||||
factories.put(p.getId(), p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static <P extends ProviderFactory> ProviderFactoryLoader<P> load(Class<P> service) {
|
||||
public static ProviderFactoryLoader create(Class<? extends ProviderFactory> service) {
|
||||
return new ProviderFactoryLoader(ServiceLoader.load(service));
|
||||
}
|
||||
|
||||
public static <P extends ProviderFactory> ProviderFactoryLoader<P> load(Class<P> service, ClassLoader loader) {
|
||||
public static ProviderFactoryLoader create(Class<? extends ProviderFactory> service, ClassLoader loader) {
|
||||
return new ProviderFactoryLoader(ServiceLoader.load(service, loader));
|
||||
}
|
||||
|
||||
public P find(String id) {
|
||||
Iterator<P> itr = iterator();
|
||||
while (itr.hasNext()) {
|
||||
P p = itr.next();
|
||||
if (p.getId() != null && p.getId().equals(id)) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
public ProviderFactory find(String id) {
|
||||
return factories.get(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<P> iterator() {
|
||||
return new ProviderFactoryIterator(serviceLoader.iterator());
|
||||
public Iterator<ProviderFactory<T>> iterator() {
|
||||
return factories.values().iterator();
|
||||
}
|
||||
|
||||
public Set<String> providerIds() {
|
||||
return factories.keySet();
|
||||
}
|
||||
|
||||
public void init() {
|
||||
for (ProviderFactory p : factories.values()) {
|
||||
p.init();
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
|
||||
for (ProviderFactory p : factories.values()) {
|
||||
p.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static class ProviderFactoryIterator<P> implements Iterator<P> {
|
||||
private class LazyProviderFactory<T extends Provider> implements ProviderFactory<T> {
|
||||
|
||||
private Iterator<P> itr;
|
||||
private volatile boolean initialized = false;
|
||||
|
||||
private P next;
|
||||
private ProviderFactory<T> factory;
|
||||
|
||||
private ProviderFactoryIterator(Iterator<P> itr) {
|
||||
this.itr = itr;
|
||||
setNext();
|
||||
private LazyProviderFactory(ProviderFactory<T> factory) {
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return next != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public P next() {
|
||||
P n = next;
|
||||
setNext();
|
||||
return n;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
private void setNext() {
|
||||
next = null;
|
||||
while (itr.hasNext()) {
|
||||
if (itr.hasNext()) {
|
||||
P n = itr.next();
|
||||
if (!System.getProperties().containsKey(n.getClass().getName() + ".disabled")) {
|
||||
next = n;
|
||||
return;
|
||||
}
|
||||
}
|
||||
public synchronized T create() {
|
||||
if (!initialized) {
|
||||
factory.init();
|
||||
initialized = true;
|
||||
}
|
||||
return factory.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
factory.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return factory.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean lazyLoad() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -24,6 +24,11 @@
|
|||
<artifactId>keycloak-model-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-audit-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>jaxrs-api</artifactId>
|
||||
|
|
|
@ -1,30 +1,35 @@
|
|||
package org.keycloak.account;
|
||||
|
||||
import org.keycloak.audit.Event;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
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);
|
||||
|
||||
Account setEvents(List<Event> events);
|
||||
|
||||
Account setFeatures(boolean social, boolean audit);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,6 @@ package org.keycloak.account;
|
|||
*/
|
||||
public enum AccountPages {
|
||||
|
||||
ACCOUNT, PASSWORD, TOTP, SOCIAL;
|
||||
ACCOUNT, PASSWORD, TOTP, SOCIAL, LOG;
|
||||
|
||||
}
|
||||
|
|
|
@ -5,10 +5,13 @@ 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;
|
||||
import org.keycloak.account.freemarker.model.TotpBean;
|
||||
import org.keycloak.account.freemarker.model.UrlBean;
|
||||
import org.keycloak.audit.Event;
|
||||
import org.keycloak.freemarker.FreeMarkerException;
|
||||
import org.keycloak.freemarker.FreeMarkerUtil;
|
||||
import org.keycloak.freemarker.Theme;
|
||||
|
@ -23,6 +26,7 @@ import javax.ws.rs.core.UriInfo;
|
|||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
|
@ -37,6 +41,9 @@ public class FreeMarkerAccount implements Account {
|
|||
private Response.Status status = Response.Status.OK;
|
||||
private RealmModel realm;
|
||||
private String[] referrer;
|
||||
private List<Event> events;
|
||||
private boolean social;
|
||||
private boolean audit;
|
||||
|
||||
public static enum MessageType {SUCCESS, WARNING, ERROR}
|
||||
|
||||
|
@ -88,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:
|
||||
|
@ -102,6 +107,8 @@ public class FreeMarkerAccount implements Account {
|
|||
case SOCIAL:
|
||||
attributes.put("social", new AccountSocialBean(realm, user, uriInfo.getBaseUri()));
|
||||
break;
|
||||
case LOG:
|
||||
attributes.put("log", new LogBean(events));
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -157,4 +164,17 @@ public class FreeMarkerAccount implements Account {
|
|||
this.referrer = referrer;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Account setEvents(List<Event> events) {
|
||||
this.events = events;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Account setFeatures(boolean social, boolean audit) {
|
||||
this.social = social;
|
||||
this.audit = audit;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ public class Templates {
|
|||
return "totp.ftl";
|
||||
case SOCIAL:
|
||||
return "social.ftl";
|
||||
case LOG:
|
||||
return "log.ftl";
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package org.keycloak.account.freemarker.model;
|
||||
|
||||
import org.keycloak.audit.Event;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class LogBean {
|
||||
|
||||
private List<EventBean> events;
|
||||
|
||||
public LogBean(List<Event> events) {
|
||||
this.events = new LinkedList<EventBean>();
|
||||
for (Event e : events) {
|
||||
this.events.add(new EventBean(e));
|
||||
}
|
||||
}
|
||||
|
||||
public List<EventBean> getEvents() {
|
||||
return events;
|
||||
}
|
||||
|
||||
public static class EventBean {
|
||||
|
||||
private Event event;
|
||||
|
||||
public EventBean(Event event) {
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
public Date getDate() {
|
||||
return new Date(event.getTime());
|
||||
}
|
||||
|
||||
public String getEvent() {
|
||||
return event.getEvent().replace('_', ' ');
|
||||
}
|
||||
|
||||
public String getClient() {
|
||||
return event.getClientId();
|
||||
}
|
||||
|
||||
public String getIpAddress() {
|
||||
return event.getIpAddress();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.mainLayout active='log' bodyClass='log'; section>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>Account Log</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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?datetime}</td>
|
||||
<td>${event.event}</td>
|
||||
<td>${event.ipAddress}</td>
|
||||
<td>${event.client}</td
|
||||
</tr>
|
||||
</#list>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</@layout.mainLayout>
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@ public class Config {
|
|||
|
||||
public static final String MODEL_PROVIDER_KEY = "keycloak.model";
|
||||
|
||||
public static final String MODEL_AUDIT_KEY = "keycloak.audit";
|
||||
|
||||
public static final String THEME_BASE_KEY = "keycloak.theme.base";
|
||||
public static final String THEME_BASE_DEFAULT = "base";
|
||||
public static final String THEME_DEFAULT_KEY = "keycloak.theme.default";
|
||||
|
@ -27,6 +29,14 @@ public class Config {
|
|||
System.setProperty(ADMIN_REALM_KEY, realm);
|
||||
}
|
||||
|
||||
public static String getAuditProvider() {
|
||||
return System.getProperty(MODEL_PROVIDER_KEY, "jpa");
|
||||
}
|
||||
|
||||
public static void setAuditProvider(String provider) {
|
||||
System.setProperty(MODEL_PROVIDER_KEY, provider);
|
||||
}
|
||||
|
||||
public static String getModelProvider() {
|
||||
return System.getProperty(MODEL_PROVIDER_KEY);
|
||||
}
|
||||
|
|
49
services/src/main/java/org/keycloak/services/DefaultProviderSession.java
Executable file
49
services/src/main/java/org/keycloak/services/DefaultProviderSession.java
Executable file
|
@ -0,0 +1,49 @@
|
|||
package org.keycloak.services;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class DefaultProviderSession implements ProviderSession {
|
||||
private DefaultProviderSessionFactory factory;
|
||||
private Map<Integer, Provider> providers = new HashMap<Integer, Provider>();
|
||||
|
||||
public DefaultProviderSession(DefaultProviderSessionFactory factory) {
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
public <T extends Provider> T getProvider(Class<T> clazz) {
|
||||
String id = factory.getDefaultProvider(clazz);
|
||||
return id != null ? getProvider(clazz, id) : null;
|
||||
}
|
||||
|
||||
public <T extends Provider> T getProvider(Class<T> clazz, String id) {
|
||||
Integer hash = clazz.hashCode() + id.hashCode();
|
||||
T provider = (T) providers.get(hash);
|
||||
if (provider == null) {
|
||||
ProviderFactory<T> providerFactory = factory.getProviderFactory(clazz, id);
|
||||
if (providerFactory != null) {
|
||||
provider = providerFactory.create();
|
||||
providers.put(hash, provider);
|
||||
}
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
public <T extends Provider> Set<String> listProviderIds(Class<T> clazz) {
|
||||
return factory.providerIds(clazz);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
for (Provider p : providers.values()) {
|
||||
p.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package org.keycloak.services;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.ProviderFactoryLoader;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class DefaultProviderSessionFactory implements ProviderSessionFactory {
|
||||
|
||||
private Map<Class<? extends Provider>, ProviderFactoryLoader> loaders = new HashMap<Class<? extends Provider>, ProviderFactoryLoader>();
|
||||
private Map<Class<? extends Provider>, String> defaultFactories = new HashMap<Class<? extends Provider>, String>();
|
||||
|
||||
public ProviderSession createSession() {
|
||||
return new DefaultProviderSession(this);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
for (ProviderFactoryLoader loader : loaders.values()) {
|
||||
loader.close();
|
||||
}
|
||||
}
|
||||
|
||||
public <T extends Provider> ProviderFactory<T> getProviderFactory(Class<T> clazz) {
|
||||
String id = defaultFactories.get(clazz);
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
return getProviderFactory(clazz, id);
|
||||
}
|
||||
|
||||
public <T extends Provider> ProviderFactory<T> getProviderFactory(Class<T> clazz, String id) {
|
||||
ProviderFactoryLoader loader = getLoader(clazz);
|
||||
return loader != null ? loader.find(id) : null;
|
||||
}
|
||||
|
||||
public Set<String> providerIds(Class<? extends Provider> clazz) {
|
||||
ProviderFactoryLoader loader = getLoader(clazz);
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
public void registerLoader(Class<? extends Provider> clazz, ProviderFactoryLoader loader, String defaultProvider) {
|
||||
loaders.put(clazz, loader);
|
||||
defaultFactories.put(clazz, defaultProvider);
|
||||
|
||||
}
|
||||
|
||||
public void init() {
|
||||
for (ProviderFactoryLoader l : loaders.values()) {
|
||||
l.init();
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends Provider> ProviderFactoryLoader getLoader(Class<T> clazz) {
|
||||
return loaders.get(clazz);
|
||||
}
|
||||
|
||||
}
|
20
services/src/main/java/org/keycloak/services/ProviderSession.java
Executable file
20
services/src/main/java/org/keycloak/services/ProviderSession.java
Executable file
|
@ -0,0 +1,20 @@
|
|||
package org.keycloak.services;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public interface ProviderSession {
|
||||
|
||||
<T extends Provider> T getProvider(Class<T> clazz);
|
||||
|
||||
<T extends Provider> T getProvider(Class<T> clazz, String id);
|
||||
|
||||
<T extends Provider> Set<String> listProviderIds(Class<T> clazz);
|
||||
|
||||
void close();
|
||||
|
||||
}
|
28
services/src/main/java/org/keycloak/services/ProviderSessionFactory.java
Executable file
28
services/src/main/java/org/keycloak/services/ProviderSessionFactory.java
Executable file
|
@ -0,0 +1,28 @@
|
|||
package org.keycloak.services;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.ProviderFactoryLoader;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public interface ProviderSessionFactory {
|
||||
|
||||
ProviderSession createSession();
|
||||
|
||||
void close();
|
||||
|
||||
<T extends Provider> ProviderFactory<T> getProviderFactory(Class<T> clazz);
|
||||
|
||||
<T extends Provider> ProviderFactory<T> getProviderFactory(Class<T> clazz, String id);
|
||||
|
||||
Set<String> providerIds(Class<? extends Provider> clazz);
|
||||
|
||||
void init();
|
||||
|
||||
}
|
|
@ -4,6 +4,8 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
|||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.KeycloakTransaction;
|
||||
import org.keycloak.services.ProviderSession;
|
||||
import org.keycloak.services.ProviderSessionFactory;
|
||||
import org.keycloak.util.KeycloakRegistry;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
|
@ -26,6 +28,11 @@ public class KeycloakSessionServletFilter implements Filter {
|
|||
|
||||
@Override
|
||||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
|
||||
ProviderSessionFactory providerSessionFactory = (ProviderSessionFactory) servletRequest.getServletContext().getAttribute(ProviderSessionFactory.class.getName());
|
||||
ProviderSession providerSession = providerSessionFactory.createSession();
|
||||
|
||||
ResteasyProviderFactory.pushContext(ProviderSession.class, providerSession);
|
||||
|
||||
KeycloakRegistry registry = (KeycloakRegistry)servletRequest.getServletContext().getAttribute(KeycloakRegistry.class.getName());
|
||||
ResteasyProviderFactory.pushContext(KeycloakRegistry.class, registry);
|
||||
KeycloakSessionFactory factory = registry.getService(KeycloakSessionFactory.class);
|
||||
|
@ -53,6 +60,7 @@ public class KeycloakSessionServletFilter implements Filter {
|
|||
throw ex;
|
||||
} finally {
|
||||
session.close();
|
||||
providerSession.close();
|
||||
ResteasyProviderFactory.clearContextData();
|
||||
}
|
||||
|
||||
|
|
|
@ -28,13 +28,22 @@ import org.keycloak.account.Account;
|
|||
import org.keycloak.account.AccountLoader;
|
||||
import org.keycloak.account.AccountPages;
|
||||
import org.keycloak.audit.Audit;
|
||||
import org.keycloak.audit.AuditProvider;
|
||||
import org.keycloak.audit.Details;
|
||||
import org.keycloak.audit.Event;
|
||||
import org.keycloak.audit.Events;
|
||||
import org.keycloak.jaxrs.JaxrsOAuthClient;
|
||||
import org.keycloak.models.*;
|
||||
import org.keycloak.models.AccountRoles;
|
||||
import org.keycloak.models.ApplicationModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.SocialLinkModel;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.TimeBasedOTP;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.services.managers.AccessCodeEntry;
|
||||
import org.keycloak.services.ProviderSession;
|
||||
import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.services.managers.Auth;
|
||||
import org.keycloak.services.managers.ModelToRepresentation;
|
||||
|
@ -51,8 +60,23 @@ import org.keycloak.spi.authentication.AuthProviderStatus;
|
|||
import org.keycloak.spi.authentication.AuthenticationProviderException;
|
||||
import org.keycloak.spi.authentication.AuthenticationProviderManager;
|
||||
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.*;
|
||||
import javax.ws.rs.BadRequestException;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.ForbiddenException;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.OPTIONS;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.NewCookie;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import javax.ws.rs.core.Variant;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
@ -77,10 +101,16 @@ public class AccountService {
|
|||
@Context
|
||||
private UriInfo uriInfo;
|
||||
|
||||
@Context
|
||||
private ProviderSession providers;
|
||||
|
||||
private final AppAuthManager authManager;
|
||||
private final ApplicationModel application;
|
||||
private Audit audit;
|
||||
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;
|
||||
|
@ -90,23 +120,30 @@ public class AccountService {
|
|||
this.socialRequestManager = socialRequestManager;
|
||||
}
|
||||
|
||||
public void init() {
|
||||
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) {
|
||||
account.setUser(auth.getUser());
|
||||
}
|
||||
}
|
||||
|
||||
public static UriBuilder accountServiceBaseUrl(UriInfo uriInfo) {
|
||||
UriBuilder base = uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(RealmsResource.class, "getAccountService");
|
||||
return base;
|
||||
}
|
||||
|
||||
|
||||
private Response forwardToPage(String path, AccountPages page) {
|
||||
Auth auth = getAuth(false);
|
||||
if (auth != null) {
|
||||
try {
|
||||
require(auth, AccountRoles.MANAGE_ACCOUNT);
|
||||
require(AccountRoles.MANAGE_ACCOUNT);
|
||||
} catch (ForbiddenException e) {
|
||||
return Flows.forms(realm, request, uriInfo).setError("No access").createErrorPage();
|
||||
}
|
||||
|
||||
Account account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setUser(auth.getUser());
|
||||
|
||||
String[] referrer = getReferrer();
|
||||
if (referrer != null) {
|
||||
account.setReferrer(referrer);
|
||||
|
@ -131,8 +168,7 @@ public class AccountService {
|
|||
if (types.contains(MediaType.WILDCARD_TYPE) || (types.contains(MediaType.TEXT_HTML_TYPE))) {
|
||||
return forwardToPage(null, AccountPages.ACCOUNT);
|
||||
} else if (types.contains(MediaType.APPLICATION_JSON_TYPE)) {
|
||||
Auth auth = getAuth(true);
|
||||
requireOneOf(auth, AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
|
||||
requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
|
||||
|
||||
return Cors.add(request, Response.ok(ModelToRepresentation.toRepresentation(auth.getUser()))).auth().allowedOrigins(auth.getClient()).build();
|
||||
} else {
|
||||
|
@ -158,17 +194,24 @@ public class AccountService {
|
|||
return forwardToPage("social", AccountPages.SOCIAL);
|
||||
}
|
||||
|
||||
@Path("log")
|
||||
@GET
|
||||
public Response logPage() {
|
||||
if (auth != null) {
|
||||
List<Event> events = auditProvider.createQuery().user(auth.getUser().getId()).maxResults(20).getResultList();
|
||||
account.setEvents(events);
|
||||
}
|
||||
return forwardToPage("log", AccountPages.LOG);
|
||||
}
|
||||
|
||||
@Path("/")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
public Response processAccountUpdate(final MultivaluedMap<String, String> formData) {
|
||||
Auth auth = getAuth(true);
|
||||
require(auth, AccountRoles.MANAGE_ACCOUNT);
|
||||
require(AccountRoles.MANAGE_ACCOUNT);
|
||||
|
||||
UserModel user = auth.getUser();
|
||||
|
||||
Account account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setUser(auth.getUser());
|
||||
|
||||
String error = Validation.validateUpdateProfileForm(formData);
|
||||
if (error != null) {
|
||||
return account.setError(error).createResponse(AccountPages.ACCOUNT);
|
||||
|
@ -196,15 +239,13 @@ public class AccountService {
|
|||
@Path("totp-remove")
|
||||
@GET
|
||||
public Response processTotpRemove() {
|
||||
Auth auth = getAuth(true);
|
||||
require(auth, AccountRoles.MANAGE_ACCOUNT);
|
||||
require(AccountRoles.MANAGE_ACCOUNT);
|
||||
|
||||
UserModel user = auth.getUser();
|
||||
user.setTotp(false);
|
||||
|
||||
audit.event(Events.REMOVE_TOTP).client(auth.getClient()).user(auth.getUser()).success();
|
||||
|
||||
Account account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setUser(auth.getUser());
|
||||
return account.setSuccess("successTotpRemoved").createResponse(AccountPages.TOTP);
|
||||
}
|
||||
|
||||
|
@ -212,16 +253,13 @@ public class AccountService {
|
|||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
public Response processTotpUpdate(final MultivaluedMap<String, String> formData) {
|
||||
Auth auth = getAuth(true);
|
||||
require(auth, AccountRoles.MANAGE_ACCOUNT);
|
||||
require(AccountRoles.MANAGE_ACCOUNT);
|
||||
|
||||
UserModel user = auth.getUser();
|
||||
|
||||
String totp = formData.getFirst("totp");
|
||||
String totpSecret = formData.getFirst("totpSecret");
|
||||
|
||||
Account account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setUser(auth.getUser());
|
||||
|
||||
if (Validation.isEmpty(totp)) {
|
||||
return account.setError(Messages.MISSING_TOTP).createResponse(AccountPages.TOTP);
|
||||
} else if (!new TimeBasedOTP().validate(totp, totpSecret.getBytes())) {
|
||||
|
@ -244,13 +282,10 @@ public class AccountService {
|
|||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
public Response processPasswordUpdate(final MultivaluedMap<String, String> formData) {
|
||||
Auth auth = getAuth(true);
|
||||
require(auth, AccountRoles.MANAGE_ACCOUNT);
|
||||
require(AccountRoles.MANAGE_ACCOUNT);
|
||||
|
||||
UserModel user = auth.getUser();
|
||||
|
||||
Account account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setUser(auth.getUser());
|
||||
|
||||
String password = formData.getFirst("password");
|
||||
String passwordNew = formData.getFirst("password-new");
|
||||
String passwordConfirm = formData.getFirst("password-confirm");
|
||||
|
@ -286,12 +321,9 @@ public class AccountService {
|
|||
@GET
|
||||
public Response processSocialUpdate(@QueryParam("action") String action,
|
||||
@QueryParam("provider_id") String providerId) {
|
||||
Auth auth = getAuth(true);
|
||||
require(auth, AccountRoles.MANAGE_ACCOUNT);
|
||||
require(AccountRoles.MANAGE_ACCOUNT);
|
||||
UserModel user = auth.getUser();
|
||||
|
||||
Account account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setUser(auth.getUser());
|
||||
|
||||
if (Validation.isEmpty(providerId)) {
|
||||
return account.setError(Messages.MISSING_SOCIAL_PROVIDER).createResponse(AccountPages.SOCIAL);
|
||||
}
|
||||
|
@ -426,14 +458,6 @@ public class AccountService {
|
|||
return oauth.redirect(uriInfo, accountUri.toString());
|
||||
}
|
||||
|
||||
private Auth getAuth(boolean error) {
|
||||
Auth auth = authManager.authenticate(realm, headers);
|
||||
if (auth == null && error) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
return auth;
|
||||
}
|
||||
|
||||
private String[] getReferrer() {
|
||||
String referrer = uriInfo.getQueryParameters().getFirst("referrer");
|
||||
if (referrer == null) {
|
||||
|
@ -467,13 +491,21 @@ public class AccountService {
|
|||
return null;
|
||||
}
|
||||
|
||||
public void require(Auth auth, String role) {
|
||||
public void require(String role) {
|
||||
if (auth == null) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
if (!auth.hasAppRole(application.getName(), role)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
}
|
||||
|
||||
public void requireOneOf(Auth auth, String... roles) {
|
||||
public void requireOneOf(String... roles) {
|
||||
if (auth == null) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
if (!auth.hasOneOfAppRole(application.getName(), roles)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
|
|
@ -2,8 +2,16 @@ package org.keycloak.services.resources;
|
|||
|
||||
import org.jboss.resteasy.logging.Logger;
|
||||
import org.keycloak.SkeletonKeyContextResolver;
|
||||
import org.keycloak.audit.AuditListener;
|
||||
import org.keycloak.audit.AuditListenerFactory;
|
||||
import org.keycloak.audit.AuditProvider;
|
||||
import org.keycloak.audit.AuditProviderFactory;
|
||||
import org.keycloak.models.Config;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.ModelProvider;
|
||||
import org.keycloak.provider.ProviderFactoryLoader;
|
||||
import org.keycloak.services.DefaultProviderSessionFactory;
|
||||
import org.keycloak.services.ProviderSessionFactory;
|
||||
import org.keycloak.util.KeycloakRegistry;
|
||||
import org.keycloak.services.managers.ApplianceBootstrap;
|
||||
import org.keycloak.services.managers.SocialRequestManager;
|
||||
|
@ -41,6 +49,8 @@ public class KeycloakApplication extends Application {
|
|||
context.setAttribute(KeycloakRegistry.class.getName(), registry);
|
||||
//classes.add(KeycloakSessionCleanupFilter.class);
|
||||
|
||||
context.setAttribute(ProviderSessionFactory.class.getName(), createProviderSessionFactory());
|
||||
|
||||
TokenManager tokenManager = new TokenManager();
|
||||
SocialRequestManager socialRequestManager = new SocialRequestManager();
|
||||
|
||||
|
@ -84,6 +94,15 @@ public class KeycloakApplication extends Application {
|
|||
throw new RuntimeException("Model provider not found");
|
||||
}
|
||||
|
||||
public static DefaultProviderSessionFactory createProviderSessionFactory() {
|
||||
DefaultProviderSessionFactory factory = new DefaultProviderSessionFactory();
|
||||
|
||||
factory.registerLoader(AuditProvider.class, ProviderFactoryLoader.create(AuditProviderFactory.class), Config.getAuditProvider());
|
||||
factory.registerLoader(AuditListener.class, ProviderFactoryLoader.create(AuditListenerFactory.class));
|
||||
|
||||
return factory;
|
||||
}
|
||||
|
||||
public KeycloakSessionFactory getFactory() {
|
||||
return factory;
|
||||
}
|
||||
|
|
|
@ -2,10 +2,14 @@ package org.keycloak.services.resources;
|
|||
|
||||
import org.jboss.resteasy.logging.Logger;
|
||||
import org.keycloak.audit.Audit;
|
||||
import org.keycloak.audit.AuditListener;
|
||||
import org.keycloak.audit.AuditProvider;
|
||||
import org.keycloak.models.ApplicationModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.services.ClientConnection;
|
||||
import org.keycloak.services.ProviderSession;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.services.managers.SocialRequestManager;
|
||||
import org.keycloak.services.managers.TokenManager;
|
||||
|
@ -19,6 +23,8 @@ import javax.ws.rs.core.Context;
|
|||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -41,7 +47,10 @@ public class RealmsResource {
|
|||
protected KeycloakSession session;
|
||||
|
||||
@Context
|
||||
protected HttpServletRequest servletRequest;
|
||||
protected ProviderSession providers;
|
||||
|
||||
@Context
|
||||
protected ClientConnection clientConnection;
|
||||
|
||||
protected TokenManager tokenManager;
|
||||
protected SocialRequestManager socialRequestManager;
|
||||
|
@ -59,7 +68,7 @@ public class RealmsResource {
|
|||
public TokenService getTokenService(final @PathParam("realm") String name) {
|
||||
RealmManager realmManager = new RealmManager(session);
|
||||
RealmModel realm = locateRealm(name, realmManager);
|
||||
Audit audit = Audit.create(realm, servletRequest.getRemoteAddr());
|
||||
Audit audit = createAudit(realm);
|
||||
TokenService tokenService = new TokenService(realm, tokenManager, audit);
|
||||
resourceContext.initResource(tokenService);
|
||||
return tokenService;
|
||||
|
@ -84,10 +93,10 @@ public class RealmsResource {
|
|||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
Audit audit = Audit.create(realm, servletRequest.getRemoteAddr());
|
||||
|
||||
Audit audit = createAudit(realm);
|
||||
AccountService accountService = new AccountService(realm, application, tokenManager, socialRequestManager, audit);
|
||||
resourceContext.initResource(accountService);
|
||||
accountService.init();
|
||||
return accountService;
|
||||
}
|
||||
|
||||
|
@ -100,4 +109,24 @@ public class RealmsResource {
|
|||
return realmResource;
|
||||
}
|
||||
|
||||
private Audit createAudit(RealmModel realm) {
|
||||
List<AuditListener> listeners = new LinkedList<AuditListener>();
|
||||
|
||||
AuditProvider auditProvider = providers.getProvider(AuditProvider.class);
|
||||
if (auditProvider != null) {
|
||||
listeners.add(auditProvider);
|
||||
}
|
||||
|
||||
if (realm.getAuditListeners() != null) {
|
||||
for (String id : realm.getAuditListeners()) {
|
||||
AuditListener listener = providers.getProvider(AuditListener.class, id);
|
||||
if (listener != null) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Audit(listeners, realm, clientConnection.getRemoteAddr());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ import org.jboss.resteasy.logging.Logger;
|
|||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.audit.Audit;
|
||||
import org.keycloak.audit.AuditListener;
|
||||
import org.keycloak.audit.AuditProvider;
|
||||
import org.keycloak.audit.Details;
|
||||
import org.keycloak.audit.Errors;
|
||||
import org.keycloak.audit.Events;
|
||||
|
@ -36,6 +38,8 @@ import org.keycloak.models.RealmModel;
|
|||
import org.keycloak.models.SocialLinkModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.services.ClientConnection;
|
||||
import org.keycloak.services.ProviderSession;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.services.managers.SocialRequestManager;
|
||||
|
@ -67,6 +71,7 @@ import javax.ws.rs.core.UriBuilder;
|
|||
import javax.ws.rs.core.UriInfo;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
@ -95,7 +100,10 @@ public class SocialResource {
|
|||
protected KeycloakSession session;
|
||||
|
||||
@Context
|
||||
protected HttpServletRequest servletRequest;
|
||||
protected ProviderSession providers;
|
||||
|
||||
@Context
|
||||
protected ClientConnection clientConnection;
|
||||
|
||||
private SocialRequestManager socialRequestManager;
|
||||
|
||||
|
@ -121,7 +129,7 @@ public class SocialResource {
|
|||
RealmManager realmManager = new RealmManager(session);
|
||||
RealmModel realm = realmManager.getRealmByName(realmName);
|
||||
|
||||
Audit audit = Audit.create(realm, servletRequest.getRemoteAddr())
|
||||
Audit audit = createAudit(realm)
|
||||
.event(Events.LOGIN)
|
||||
.detail(Details.RESPONSE_TYPE, "code")
|
||||
.detail(Details.AUTH_METHOD, "social");
|
||||
|
@ -260,7 +268,7 @@ public class SocialResource {
|
|||
RealmManager realmManager = new RealmManager(session);
|
||||
RealmModel realm = realmManager.getRealmByName(realmName);
|
||||
|
||||
Audit audit = Audit.create(realm, servletRequest.getRemoteAddr())
|
||||
Audit audit = createAudit(realm)
|
||||
.event(Events.LOGIN).client(clientId)
|
||||
.detail(Details.REDIRECT_URI, redirectUri)
|
||||
.detail(Details.RESPONSE_TYPE, "code")
|
||||
|
@ -327,4 +335,24 @@ public class SocialResource {
|
|||
return queryParams;
|
||||
}
|
||||
|
||||
private Audit createAudit(RealmModel realm) {
|
||||
List<AuditListener> listeners = new LinkedList<AuditListener>();
|
||||
|
||||
AuditProvider auditProvider = providers.getProvider(AuditProvider.class);
|
||||
if (auditProvider != null) {
|
||||
listeners.add(auditProvider);
|
||||
}
|
||||
|
||||
if (realm.getAuditListeners() != null) {
|
||||
for (String id : realm.getAuditListeners()) {
|
||||
AuditListener listener = providers.getProvider(AuditListener.class, id);
|
||||
if (listener != null) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Audit(listeners, realm, clientConnection.getRemoteAddr());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -34,8 +34,6 @@ import java.util.concurrent.TimeUnit;
|
|||
*/
|
||||
public class AssertEvents implements TestRule, AuditListenerFactory {
|
||||
|
||||
private static final Logger log = Logger.getLogger(AssertEvents.class);
|
||||
|
||||
public static String DEFAULT_CLIENT_ID = "test-app";
|
||||
public static String DEFAULT_REDIRECT_URI = "http://localhost:8081/app/auth";
|
||||
public static String DEFAULT_IP_ADDRESS = "127.0.0.1";
|
||||
|
@ -58,6 +56,11 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
|
|||
return "assert-events";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean lazyLoad() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Statement apply(final Statement base, org.junit.runner.Description description) {
|
||||
return new Statement() {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue