diff --git a/audit/api/src/main/java/org/keycloak/audit/Audit.java b/audit/api/src/main/java/org/keycloak/audit/Audit.java index 682ceb4a91..4cbd4f762e 100644 --- a/audit/api/src/main/java/org/keycloak/audit/Audit.java +++ b/audit/api/src/main/java/org/keycloak/audit/Audit.java @@ -20,24 +20,15 @@ public class Audit { private List listeners; private Event event; - public static Audit create(RealmModel realm, String ipAddress) { - ProviderFactoryLoader loader = ProviderFactoryLoader.load(AuditListenerFactory.class); + public Audit(List listeners, RealmModel realm, String ipAddress) { + this.listeners = listeners; + this.event = new Event(); - List listeners = null; - if (realm.getAuditListeners() != null) { - listeners = new LinkedList(); - - 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 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() { diff --git a/audit/api/src/main/java/org/keycloak/audit/EventQuery.java b/audit/api/src/main/java/org/keycloak/audit/EventQuery.java index 75e07c03cd..339bc64b4a 100644 --- a/audit/api/src/main/java/org/keycloak/audit/EventQuery.java +++ b/audit/api/src/main/java/org/keycloak/audit/EventQuery.java @@ -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); diff --git a/audit/jboss-logging/src/main/java/org/keycloak/audit/log/JBossLoggingAuditListenerFactory.java b/audit/jboss-logging/src/main/java/org/keycloak/audit/log/JBossLoggingAuditListenerFactory.java index 420bd67585..62322fb9b6 100644 --- a/audit/jboss-logging/src/main/java/org/keycloak/audit/log/JBossLoggingAuditListenerFactory.java +++ b/audit/jboss-logging/src/main/java/org/keycloak/audit/log/JBossLoggingAuditListenerFactory.java @@ -31,4 +31,9 @@ public class JBossLoggingAuditListenerFactory implements AuditListenerFactory { return ID; } + @Override + public boolean lazyLoad() { + return false; + } + } diff --git a/audit/jpa/src/src/main/java/org/keycloak/audit/jpa/EventEntity.java b/audit/jpa/src/main/java/org/keycloak/audit/jpa/EventEntity.java similarity index 96% rename from audit/jpa/src/src/main/java/org/keycloak/audit/jpa/EventEntity.java rename to audit/jpa/src/main/java/org/keycloak/audit/jpa/EventEntity.java index a8fd5317cf..1227b70cdd 100644 --- a/audit/jpa/src/src/main/java/org/keycloak/audit/jpa/EventEntity.java +++ b/audit/jpa/src/main/java/org/keycloak/audit/jpa/EventEntity.java @@ -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() { diff --git a/audit/jpa/src/src/main/java/org/keycloak/audit/jpa/JpaAuditProvider.java b/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaAuditProvider.java similarity index 100% rename from audit/jpa/src/src/main/java/org/keycloak/audit/jpa/JpaAuditProvider.java rename to audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaAuditProvider.java diff --git a/audit/jpa/src/src/main/java/org/keycloak/audit/jpa/JpaAuditProviderFactory.java b/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaAuditProviderFactory.java similarity index 91% rename from audit/jpa/src/src/main/java/org/keycloak/audit/jpa/JpaAuditProviderFactory.java rename to audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaAuditProviderFactory.java index e05ab636f9..bce26eecc2 100644 --- a/audit/jpa/src/src/main/java/org/keycloak/audit/jpa/JpaAuditProviderFactory.java +++ b/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaAuditProviderFactory.java @@ -34,4 +34,9 @@ public class JpaAuditProviderFactory implements AuditProviderFactory { return ID; } + @Override + public boolean lazyLoad() { + return true; + } + } diff --git a/audit/jpa/src/src/main/java/org/keycloak/audit/jpa/JpaEventQuery.java b/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaEventQuery.java similarity index 93% rename from audit/jpa/src/src/main/java/org/keycloak/audit/jpa/JpaEventQuery.java rename to audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaEventQuery.java index f4ec884360..c0dd127a4f 100644 --- a/audit/jpa/src/src/main/java/org/keycloak/audit/jpa/JpaEventQuery.java +++ b/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaEventQuery.java @@ -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 query = em.createQuery(cq); diff --git a/audit/jpa/src/src/main/resources/META-INF/services/org.keycloak.audit.AuditProviderFactory b/audit/jpa/src/main/resources/META-INF/services/org.keycloak.audit.AuditProviderFactory similarity index 100% rename from audit/jpa/src/src/main/resources/META-INF/services/org.keycloak.audit.AuditProviderFactory rename to audit/jpa/src/main/resources/META-INF/services/org.keycloak.audit.AuditProviderFactory diff --git a/audit/jpa/src/src/test/java/org/keycloak/audit/jpa/JpaAuditProviderTest.java b/audit/jpa/src/test/java/org/keycloak/audit/jpa/JpaAuditProviderTest.java similarity index 100% rename from audit/jpa/src/src/test/java/org/keycloak/audit/jpa/JpaAuditProviderTest.java rename to audit/jpa/src/test/java/org/keycloak/audit/jpa/JpaAuditProviderTest.java diff --git a/audit/jpa/src/src/test/resources/META-INF/persistence.xml b/audit/jpa/src/test/resources/META-INF/persistence.xml similarity index 100% rename from audit/jpa/src/src/test/resources/META-INF/persistence.xml rename to audit/jpa/src/test/resources/META-INF/persistence.xml diff --git a/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProviderFactory.java b/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProviderFactory.java index 5fb7a83e72..7d84b265a8 100644 --- a/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProviderFactory.java +++ b/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProviderFactory.java @@ -47,4 +47,9 @@ public class MongoAuditProviderFactory implements AuditProviderFactory { return ID; } + @Override + public boolean lazyLoad() { + return true; + } + } diff --git a/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoEventQuery.java b/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoEventQuery.java index e425455f36..5c75e63dca 100644 --- a/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoEventQuery.java +++ b/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoEventQuery.java @@ -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 getResultList() { - DBCursor cur = audit.find(query); + DBCursor cur = audit.find(query).sort(new BasicDBObject("time", -1)); if (firstResult != null) { cur.skip(firstResult); } diff --git a/audit/tests/src/main/java/org/keycloak/audit/tests/AbstractAuditProviderTest.java b/audit/tests/src/main/java/org/keycloak/audit/tests/AbstractAuditProviderTest.java index c5d1bb7fe1..c3ebdaae65 100644 --- a/audit/tests/src/main/java/org/keycloak/audit/tests/AbstractAuditProviderTest.java +++ b/audit/tests/src/main/java/org/keycloak/audit/tests/AbstractAuditProviderTest.java @@ -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 factory; private AuditProvider provider; @Before public void before() { - ProviderFactoryLoader loader = ProviderFactoryLoader.load(AuditProviderFactory.class); + ProviderFactoryLoader 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 diff --git a/core/src/main/java/org/keycloak/provider/ProviderFactory.java b/core/src/main/java/org/keycloak/provider/ProviderFactory.java index 720538dc10..ca8a19b83b 100644 --- a/core/src/main/java/org/keycloak/provider/ProviderFactory.java +++ b/core/src/main/java/org/keycloak/provider/ProviderFactory.java @@ -13,4 +13,6 @@ public interface ProviderFactory { public String getId(); + public boolean lazyLoad(); + } diff --git a/core/src/main/java/org/keycloak/provider/ProviderFactoryLoader.java b/core/src/main/java/org/keycloak/provider/ProviderFactoryLoader.java index 51b00dc417..fdbb171f56 100644 --- a/core/src/main/java/org/keycloak/provider/ProviderFactoryLoader.java +++ b/core/src/main/java/org/keycloak/provider/ProviderFactoryLoader.java @@ -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 Stian Thorgersen */ -public class ProviderFactoryLoader

implements Iterable

{ +public class ProviderFactoryLoader implements Iterable> { - private ServiceLoader

serviceLoader; + private Map> factories = new HashMap>(); - private ProviderFactoryLoader(ServiceLoader

serviceLoader) { - this.serviceLoader = serviceLoader; + private ProviderFactoryLoader(ServiceLoader 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

ProviderFactoryLoader

load(Class

service) { + public static ProviderFactoryLoader create(Class service) { return new ProviderFactoryLoader(ServiceLoader.load(service)); } - public static

ProviderFactoryLoader

load(Class

service, ClassLoader loader) { + public static ProviderFactoryLoader create(Class service, ClassLoader loader) { return new ProviderFactoryLoader(ServiceLoader.load(service, loader)); } - public P find(String id) { - Iterator

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

iterator() { - return new ProviderFactoryIterator(serviceLoader.iterator()); + public Iterator> iterator() { + return factories.values().iterator(); + } + + public Set 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

implements Iterator

{ + private class LazyProviderFactory implements ProviderFactory { - private Iterator

itr; + private volatile boolean initialized = false; - private P next; + private ProviderFactory factory; - private ProviderFactoryIterator(Iterator

itr) { - this.itr = itr; - setNext(); + private LazyProviderFactory(ProviderFactory 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; + } } } diff --git a/forms/account-api/pom.xml b/forms/account-api/pom.xml index e153d52f0d..1ad638a8f8 100755 --- a/forms/account-api/pom.xml +++ b/forms/account-api/pom.xml @@ -24,6 +24,11 @@ keycloak-model-api ${project.version} + + org.keycloak + keycloak-audit-api + ${project.version} + org.jboss.resteasy jaxrs-api diff --git a/forms/account-api/src/main/java/org/keycloak/account/Account.java b/forms/account-api/src/main/java/org/keycloak/account/Account.java index 060b8a5c73..f92b161ecb 100644 --- a/forms/account-api/src/main/java/org/keycloak/account/Account.java +++ b/forms/account-api/src/main/java/org/keycloak/account/Account.java @@ -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 Stian Thorgersen */ 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 events); + + Account setFeatures(boolean social, boolean audit); } diff --git a/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java b/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java index a3e60d2460..ceeca40564 100644 --- a/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java +++ b/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java @@ -5,6 +5,6 @@ package org.keycloak.account; */ public enum AccountPages { - ACCOUNT, PASSWORD, TOTP, SOCIAL; + ACCOUNT, PASSWORD, TOTP, SOCIAL, LOG; } diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java index 143aa1cc71..53f43d4593 100644 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java @@ -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 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 events) { + this.events = events; + return this; + } + + @Override + public Account setFeatures(boolean social, boolean audit) { + this.social = social; + this.audit = audit; + return this; + } } diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java index 5a63ef5949..f9d051cb2f 100644 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java @@ -17,6 +17,8 @@ public class Templates { return "totp.ftl"; case SOCIAL: return "social.ftl"; + case LOG: + return "log.ftl"; default: throw new IllegalArgumentException(); } diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/FeaturesBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/FeaturesBean.java new file mode 100644 index 0000000000..6f8158ba81 --- /dev/null +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/FeaturesBean.java @@ -0,0 +1,24 @@ +package org.keycloak.account.freemarker.model; + +/** + * @author Stian Thorgersen + */ +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; + } + +} diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/LogBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/LogBean.java new file mode 100644 index 0000000000..f138f8d96e --- /dev/null +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/LogBean.java @@ -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 Stian Thorgersen + */ +public class LogBean { + + private List events; + + public LogBean(List events) { + this.events = new LinkedList(); + for (Event e : events) { + this.events.add(new EventBean(e)); + } + } + + public List 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(); + } + + } + +} diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java index 53dde7cd66..e626ddf33f 100644 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java @@ -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(); } diff --git a/forms/common-themes/src/main/resources/theme/account/base/log.ftl b/forms/common-themes/src/main/resources/theme/account/base/log.ftl new file mode 100644 index 0000000000..97523173dc --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/account/base/log.ftl @@ -0,0 +1,32 @@ +<#import "template.ftl" as layout> +<@layout.mainLayout active='log' bodyClass='log'; section> + +

+
+

Account Log

+
+
+ + + + + + + + + + + + + <#list log.events as event> + + + + + + + +
DateEventIPClient
${event.date?datetime}${event.event}${event.ipAddress}${event.client}
+ + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/account/base/template.ftl b/forms/common-themes/src/main/resources/theme/account/base/template.ftl index d6a2f5eb61..6a39817a40 100644 --- a/forms/common-themes/src/main/resources/theme/account/base/template.ftl +++ b/forms/common-themes/src/main/resources/theme/account/base/template.ftl @@ -42,7 +42,8 @@
  • Account
  • Password
  • Authenticator
  • - <#if isSocialRealm?has_content>
  • Social
  • + <#if features.social>
  • Social
  • + <#if features.log>
  • Log
  • diff --git a/model/api/src/main/java/org/keycloak/models/Config.java b/model/api/src/main/java/org/keycloak/models/Config.java index fccf84f6e7..7851ea8b78 100644 --- a/model/api/src/main/java/org/keycloak/models/Config.java +++ b/model/api/src/main/java/org/keycloak/models/Config.java @@ -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); } diff --git a/services/src/main/java/org/keycloak/services/DefaultProviderSession.java b/services/src/main/java/org/keycloak/services/DefaultProviderSession.java new file mode 100755 index 0000000000..f9e2948b3e --- /dev/null +++ b/services/src/main/java/org/keycloak/services/DefaultProviderSession.java @@ -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 Stian Thorgersen + */ +public class DefaultProviderSession implements ProviderSession { + private DefaultProviderSessionFactory factory; + private Map providers = new HashMap(); + + public DefaultProviderSession(DefaultProviderSessionFactory factory) { + this.factory = factory; + } + + public T getProvider(Class clazz) { + String id = factory.getDefaultProvider(clazz); + return id != null ? getProvider(clazz, id) : null; + } + + public T getProvider(Class clazz, String id) { + Integer hash = clazz.hashCode() + id.hashCode(); + T provider = (T) providers.get(hash); + if (provider == null) { + ProviderFactory providerFactory = factory.getProviderFactory(clazz, id); + if (providerFactory != null) { + provider = providerFactory.create(); + providers.put(hash, provider); + } + } + return provider; + } + + public Set listProviderIds(Class clazz) { + return factory.providerIds(clazz); + } + + public void close() { + for (Provider p : providers.values()) { + p.close(); + } + } + +} diff --git a/services/src/main/java/org/keycloak/services/DefaultProviderSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultProviderSessionFactory.java new file mode 100755 index 0000000000..67285593e3 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/DefaultProviderSessionFactory.java @@ -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, ProviderFactoryLoader> loaders = new HashMap, ProviderFactoryLoader>(); + private Map, String> defaultFactories = new HashMap, String>(); + + public ProviderSession createSession() { + return new DefaultProviderSession(this); + } + + public void close() { + for (ProviderFactoryLoader loader : loaders.values()) { + loader.close(); + } + } + + public ProviderFactory getProviderFactory(Class clazz) { + String id = defaultFactories.get(clazz); + if (id == null) { + return null; + } + return getProviderFactory(clazz, id); + } + + public ProviderFactory getProviderFactory(Class clazz, String id) { + ProviderFactoryLoader loader = getLoader(clazz); + return loader != null ? loader.find(id) : null; + } + + public Set providerIds(Class clazz) { + ProviderFactoryLoader loader = getLoader(clazz); + return loader != null ? loader.providerIds() : null; + } + + public String getDefaultProvider(Class clazz) { + return defaultFactories.get(clazz); + } + + public void registerLoader(Class clazz, ProviderFactoryLoader loader) { + loaders.put(clazz, loader); + + } + + public void registerLoader(Class clazz, ProviderFactoryLoader loader, String defaultProvider) { + loaders.put(clazz, loader); + defaultFactories.put(clazz, defaultProvider); + + } + + public void init() { + for (ProviderFactoryLoader l : loaders.values()) { + l.init(); + } + } + + private ProviderFactoryLoader getLoader(Class clazz) { + return loaders.get(clazz); + } + +} diff --git a/services/src/main/java/org/keycloak/services/ProviderSession.java b/services/src/main/java/org/keycloak/services/ProviderSession.java new file mode 100755 index 0000000000..100dc54ea7 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/ProviderSession.java @@ -0,0 +1,20 @@ +package org.keycloak.services; + +import org.keycloak.provider.Provider; + +import java.util.Set; + +/** + * @author Stian Thorgersen + */ +public interface ProviderSession { + + T getProvider(Class clazz); + + T getProvider(Class clazz, String id); + + Set listProviderIds(Class clazz); + + void close(); + +} diff --git a/services/src/main/java/org/keycloak/services/ProviderSessionFactory.java b/services/src/main/java/org/keycloak/services/ProviderSessionFactory.java new file mode 100755 index 0000000000..a1c5707e29 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/ProviderSessionFactory.java @@ -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 Stian Thorgersen + */ +public interface ProviderSessionFactory { + + ProviderSession createSession(); + + void close(); + + ProviderFactory getProviderFactory(Class clazz); + + ProviderFactory getProviderFactory(Class clazz, String id); + + Set providerIds(Class clazz); + + void init(); + +} diff --git a/services/src/main/java/org/keycloak/services/filters/KeycloakSessionServletFilter.java b/services/src/main/java/org/keycloak/services/filters/KeycloakSessionServletFilter.java index 3f859c2447..54338ca3cf 100755 --- a/services/src/main/java/org/keycloak/services/filters/KeycloakSessionServletFilter.java +++ b/services/src/main/java/org/keycloak/services/filters/KeycloakSessionServletFilter.java @@ -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(); } diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index b937674b58..9f47d7c6d9 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -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 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 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 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 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(); } diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index 977d7e9658..31cc1e8e2e 100755 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -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; } diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java index 4601559922..cb11ed517e 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -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 Bill Burke @@ -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 listeners = new LinkedList(); + + 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()); + } + } diff --git a/services/src/main/java/org/keycloak/services/resources/SocialResource.java b/services/src/main/java/org/keycloak/services/resources/SocialResource.java index 3d5bfa52c1..c06fc3de3a 100755 --- a/services/src/main/java/org/keycloak/services/resources/SocialResource.java +++ b/services/src/main/java/org/keycloak/services/resources/SocialResource.java @@ -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 listeners = new LinkedList(); + + 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()); + } + } diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java index 4147adafc6..55137c4e3f 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java @@ -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); } diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index 115c618b32..0b3cd3d2eb 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -42,6 +42,11 @@ keycloak-audit-api ${project.version} + + org.keycloak + keycloak-audit-jpa + ${project.version} + org.keycloak keycloak-audit-jboss-logging diff --git a/testsuite/integration/src/main/resources/META-INF/persistence.xml b/testsuite/integration/src/main/resources/META-INF/persistence.xml index 5ca96af932..56a5ed4517 100755 --- a/testsuite/integration/src/main/resources/META-INF/persistence.xml +++ b/testsuite/integration/src/main/resources/META-INF/persistence.xml @@ -32,6 +32,24 @@ + + org.hibernate.ejb.HibernatePersistence + + org.keycloak.audit.jpa.EventEntity + + true + + + + + + + + + + + +