From 70f53c6c0684fa684bed6b36f0b38ea880f8199d Mon Sep 17 00:00:00 2001 From: girirajsharma Date: Sun, 3 May 2015 01:46:52 +0530 Subject: [PATCH] [KEYCLOAK-392] - Admin audit events --- .../META-INF/jpa-changelog-1.3.0.Beta1.xml | 34 +++ .../META-INF/jpa-changelog-master.xml | 1 + .../jpa/updater/JpaUpdaterProvider.java | 2 +- .../main/resources/META-INF/persistence.xml | 5 +- .../idm/RealmEventsConfigRepresentation.java | 29 +++ .../idm/RealmRepresentation.java | 30 +++ .../keycloak/events/AdminEventBuilder.java | 205 ++++++++++++++++ .../org/keycloak/events/EventBuilder.java | 3 +- .../events/EventListenerProvider.java | 3 + .../keycloak/events/EventStoreProvider.java | 10 + .../org/keycloak/events/admin/AdminEvent.java | 106 ++++++++ .../events/admin/AdminEventQuery.java | 102 ++++++++ .../keycloak/events/admin/AuthDetails.java | 48 ++++ .../org/keycloak/events/admin/AuthQuery.java | 48 ++++ .../keycloak/events/admin/OperationType.java | 24 ++ .../email/EmailEventListenerProvider.java | 6 + .../JBossLoggingEventListenerProvider.java | 83 +++++-- .../keycloak/events/jpa/AdminEventEntity.java | 126 ++++++++++ .../events/jpa/JpaAdminEventQuery.java | 148 ++++++++++++ .../keycloak/events/jpa/JpaEventQuery.java | 2 +- .../events/jpa/JpaEventStoreProvider.java | 129 +++++++--- .../events/mongo/MongoAdminEventQuery.java | 126 ++++++++++ .../events/mongo/MongoEventQuery.java | 2 +- .../events/mongo/MongoEventStoreProvider.java | 130 ++++++++-- .../mongo/MongoEventStoreProviderFactory.java | 5 +- .../log/SysLoggingEventListenerProvider.java | 28 +++ .../events/SysoutEventListenerProvider.java | 40 +++- .../SysoutEventListenerProviderFactory.java | 12 +- .../providers/events/MemAdminEventQuery.java | 162 +++++++++++++ .../events/MemEventStoreProvider.java | 53 +++- .../events/MemEventStoreProviderFactory.java | 17 +- .../theme/base/admin/resources/js/app.js | 12 + .../admin/resources/js/controllers/realm.js | 120 +++++++++- .../theme/base/admin/resources/js/services.js | 6 + .../modal/realm-events-admin-auth.html | 8 + .../realm-events-admin-representation.html | 3 + .../partials/realm-events-admin.html | 128 ++++++++++ .../partials/realm-events-config.html | 155 ++++++++---- .../resources/partials/realm-events.html | 5 +- .../freemarker/beans/AdminEventBean.java | 37 +++ .../java/org/keycloak/models/RealmModel.java | 12 + .../keycloak/models/entities/RealmEntity.java | 30 ++- .../models/utils/ModelToRepresentation.java | 9 + .../models/utils/RepresentationToModel.java | 6 + .../models/file/adapter/RealmAdapter.java | 34 +++ .../keycloak/models/cache/RealmAdapter.java | 37 +++ .../models/cache/entities/CachedRealm.java | 20 ++ .../org/keycloak/models/jpa/RealmAdapter.java | 36 +++ .../models/jpa/entities/RealmEntity.java | 37 ++- .../mongo/keycloak/adapters/RealmAdapter.java | 39 +++ .../services/managers/RealmManager.java | 6 + .../services/resources/admin/AdminRoot.java | 9 +- .../ClientAttributeCertificateResource.java | 15 +- .../resources/admin/ClientResource.java | 53 +++- .../resources/admin/ClientsByIdResource.java | 5 +- .../resources/admin/ClientsResource.java | 17 +- .../admin/IdentityProviderResource.java | 34 ++- .../admin/IdentityProvidersResource.java | 32 ++- .../admin/ProtocolMappersResource.java | 21 +- .../resources/admin/RealmAdminResource.java | 140 ++++++++++- .../resources/admin/RealmsAdminResource.java | 27 ++- .../resources/admin/RoleByIdResource.java | 21 +- .../admin/RoleContainerResource.java | 55 ++++- .../admin/ScopeMappedClientResource.java | 16 +- .../resources/admin/ScopeMappedResource.java | 25 +- .../admin/ServerInfoAdminResource.java | 41 ++-- .../admin/UserClientRoleMappingsResource.java | 19 +- .../admin/UserFederationResource.java | 29 ++- .../resources/admin/UsersResource.java | 53 +++- .../org/keycloak/testsuite/AssertEvents.java | 7 + .../events/AdminEventStoreProviderTest.java | 226 ++++++++++++++++++ 71 files changed, 3047 insertions(+), 257 deletions(-) create mode 100644 connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.3.0.Beta1.xml create mode 100644 events/api/src/main/java/org/keycloak/events/AdminEventBuilder.java create mode 100644 events/api/src/main/java/org/keycloak/events/admin/AdminEvent.java create mode 100644 events/api/src/main/java/org/keycloak/events/admin/AdminEventQuery.java create mode 100644 events/api/src/main/java/org/keycloak/events/admin/AuthDetails.java create mode 100644 events/api/src/main/java/org/keycloak/events/admin/AuthQuery.java create mode 100755 events/api/src/main/java/org/keycloak/events/admin/OperationType.java create mode 100644 events/jpa/src/main/java/org/keycloak/events/jpa/AdminEventEntity.java create mode 100644 events/jpa/src/main/java/org/keycloak/events/jpa/JpaAdminEventQuery.java create mode 100644 events/mongo/src/main/java/org/keycloak/events/mongo/MongoAdminEventQuery.java create mode 100644 examples/providers/event-store-mem/src/main/java/org/keycloak/examples/providers/events/MemAdminEventQuery.java create mode 100644 forms/common-themes/src/main/resources/theme/base/admin/resources/partials/modal/realm-events-admin-auth.html create mode 100644 forms/common-themes/src/main/resources/theme/base/admin/resources/partials/modal/realm-events-admin-representation.html create mode 100755 forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-events-admin.html create mode 100644 forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/beans/AdminEventBean.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/events/AdminEventStoreProviderTest.java diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.3.0.Beta1.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.3.0.Beta1.xml new file mode 100644 index 0000000000..70860652e2 --- /dev/null +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.3.0.Beta1.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml index 42a702aef2..ee8a6f13df 100755 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml @@ -5,4 +5,5 @@ + diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java index 2b942434d4..ad4101678e 100755 --- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java +++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java @@ -12,7 +12,7 @@ public interface JpaUpdaterProvider extends Provider { public String FIRST_VERSION = "1.0.0.Final"; - public String LAST_VERSION = "1.2.0.RC1"; + public String LAST_VERSION = "1.3.0.Beta1"; public String getCurrentVersionSql(); diff --git a/connections/jpa/src/main/resources/META-INF/persistence.xml b/connections/jpa/src/main/resources/META-INF/persistence.xml index 4dbad93f91..79b730a727 100755 --- a/connections/jpa/src/main/resources/META-INF/persistence.xml +++ b/connections/jpa/src/main/resources/META-INF/persistence.xml @@ -34,9 +34,10 @@ org.keycloak.models.sessions.jpa.entities.UserSessionEntity org.keycloak.models.sessions.jpa.entities.UsernameLoginFailureEntity - + org.keycloak.events.jpa.EventEntity - + org.keycloak.events.jpa.AdminEventEntity + true diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmEventsConfigRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmEventsConfigRepresentation.java index a88a75d93d..5b12c102f5 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmEventsConfigRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmEventsConfigRepresentation.java @@ -11,6 +11,10 @@ public class RealmEventsConfigRepresentation { protected Long eventsExpiration; protected List eventsListeners; protected List enabledEventTypes; + + protected Boolean adminEventsEnabled; + protected List adminEnabledEventOperations; + protected Boolean adminEventsDetailsEnabled; public boolean isEventsEnabled() { return eventsEnabled; @@ -43,4 +47,29 @@ public class RealmEventsConfigRepresentation { public void setEnabledEventTypes(List enabledEventTypes) { this.enabledEventTypes = enabledEventTypes; } + + public Boolean isAdminEventsEnabled() { + return adminEventsEnabled; + } + + public void setAdminEventsEnabled(Boolean adminEventsEnabled) { + this.adminEventsEnabled = adminEventsEnabled; + } + + public List getAdminEnabledEventOperations() { + return adminEnabledEventOperations; + } + + public void setAdminEnabledEventOperations(List adminEnabledEventOperations) { + this.adminEnabledEventOperations = adminEnabledEventOperations; + } + + public Boolean isAdminEventsDetailsEnabled() { + return adminEventsDetailsEnabled; + } + + public void setAdminEventsDetailsEnabled(Boolean adminEventsDetailsEnabled) { + this.adminEventsDetailsEnabled = adminEventsDetailsEnabled; + } + } diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java index d9926fe3ca..2c9be559fa 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -57,10 +57,16 @@ public class RealmRepresentation { protected String accountTheme; protected String adminTheme; protected String emailTheme; + protected Boolean eventsEnabled; protected Long eventsExpiration; protected List eventsListeners; protected List enabledEventTypes; + + protected Boolean adminEventsEnabled; + protected List adminEnabledEventOperations; + protected Boolean adminEventsDetailsEnabled; + private List identityProviders; private List identityProviderMappers; private List protocolMappers; @@ -507,6 +513,30 @@ public class RealmRepresentation { this.enabledEventTypes = enabledEventTypes; } + public Boolean isAdminEventsEnabled() { + return adminEventsEnabled; + } + + public void setAdminEventsEnabled(Boolean adminEventsEnabled) { + this.adminEventsEnabled = adminEventsEnabled; + } + + public List getAdminEnabledEventOperations() { + return adminEnabledEventOperations; + } + + public void setAdminEnabledEventOperations(List adminEnabledEventOperations) { + this.adminEnabledEventOperations = adminEnabledEventOperations; + } + + public Boolean isAdminEventsDetailsEnabled() { + return adminEventsDetailsEnabled; + } + + public void setAdminEventsDetailsEnabled(Boolean adminEventsDetailsEnabled) { + this.adminEventsDetailsEnabled = adminEventsDetailsEnabled; + } + public List getUserFederationProviders() { return userFederationProviders; } diff --git a/events/api/src/main/java/org/keycloak/events/AdminEventBuilder.java b/events/api/src/main/java/org/keycloak/events/AdminEventBuilder.java new file mode 100644 index 0000000000..e822217ef3 --- /dev/null +++ b/events/api/src/main/java/org/keycloak/events/AdminEventBuilder.java @@ -0,0 +1,205 @@ +package org.keycloak.events; + +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +import org.jboss.logging.Logger; +import org.keycloak.ClientConnection; +import org.keycloak.events.admin.AdminEvent; +import org.keycloak.events.admin.AuthDetails; +import org.keycloak.events.admin.OperationType; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.util.JsonSerialization; +import org.keycloak.util.Time; + +public class AdminEventBuilder { + + private static final Logger log = Logger.getLogger(AdminEventBuilder.class); + + private EventStoreProvider store; + private List listeners; + private RealmModel realm; + private AdminEvent adminEvent; + + public AdminEventBuilder(RealmModel realm, KeycloakSession session, ClientConnection clientConnection) { + this.realm = realm; + + adminEvent = new AdminEvent(); + + if (realm.isAdminEventsEnabled()) { + EventStoreProvider store = session.getProvider(EventStoreProvider.class); + if (store != null) { + this.store = store; + } else { + log.error("Admin Events enabled, but no event store provider configured"); + } + } + + if (realm.getEventsListeners() != null && !realm.getEventsListeners().isEmpty()) { + this.listeners = new LinkedList<>(); + for (String id : realm.getEventsListeners()) { + EventListenerProvider listener = session.getProvider(EventListenerProvider.class, id); + if (listener != null) { + listeners.add(listener); + } else { + log.error("Event listener '" + id + "' registered, but provider not found"); + } + } + } + + realm(realm); + ipAddress(clientConnection.getRemoteAddr()); + } + + public AdminEventBuilder operation(OperationType e) { + adminEvent.setOperationType(e); + return this; + } + + public AdminEventBuilder realm(RealmModel realm) { + AuthDetails authDetails = adminEvent.getAuthDetails(); + if(authDetails == null) { + authDetails = new AuthDetails(); + authDetails.setRealmId(realm.getId()); + } else { + authDetails.setRealmId(realm.getId()); + } + adminEvent.setAuthDetails(authDetails); + return this; + } + + public AdminEventBuilder realm(String realmId) { + AuthDetails authDetails = adminEvent.getAuthDetails(); + if(authDetails == null) { + authDetails = new AuthDetails(); + authDetails.setRealmId(realmId); + } else { + authDetails.setRealmId(realmId); + } + adminEvent.setAuthDetails(authDetails); + return this; + } + + public AdminEventBuilder client(ClientModel client) { + AuthDetails authDetails = adminEvent.getAuthDetails(); + if(authDetails == null) { + authDetails = new AuthDetails(); + authDetails.setClientId(client.getId()); + } else { + authDetails.setClientId(client.getId()); + } + adminEvent.setAuthDetails(authDetails); + return this; + } + + public AdminEventBuilder client(String clientId) { + AuthDetails authDetails = adminEvent.getAuthDetails(); + if(authDetails == null) { + authDetails = new AuthDetails(); + authDetails.setClientId(clientId); + } else { + authDetails.setClientId(clientId); + } + adminEvent.setAuthDetails(authDetails); + return this; + } + + public AdminEventBuilder user(UserModel user) { + AuthDetails authDetails = adminEvent.getAuthDetails(); + if(authDetails == null) { + authDetails = new AuthDetails(); + authDetails.setUserId(user.getId()); + } else { + authDetails.setUserId(user.getId()); + } + adminEvent.setAuthDetails(authDetails); + return this; + } + + public AdminEventBuilder user(String userId) { + AuthDetails authDetails = adminEvent.getAuthDetails(); + if(authDetails == null) { + authDetails = new AuthDetails(); + authDetails.setUserId(userId); + } else { + authDetails.setUserId(userId); + } + adminEvent.setAuthDetails(authDetails); + return this; + } + + public AdminEventBuilder ipAddress(String ipAddress) { + AuthDetails authDetails = adminEvent.getAuthDetails(); + if(authDetails == null) { + authDetails = new AuthDetails(); + authDetails.setIpAddress(ipAddress); + } else { + authDetails.setIpAddress(ipAddress); + } + adminEvent.setAuthDetails(authDetails); + return this; + } + + public AdminEventBuilder resourcePath(String resourcePath) { + adminEvent.setResourcePath(resourcePath); + return this; + } + + public void error(String error) { + adminEvent.setOperationType(OperationType.valueOf(adminEvent.getOperationType().name() + "_ERROR")); + adminEvent.setError(error); + send(); + } + + public AdminEventBuilder representation(Object value) { + if (value == null || value.equals("")) { + return this; + } + try { + adminEvent.setRepresentation(JsonSerialization.writeValueAsString(value)); + } catch (IOException e) { + throw new RuntimeException(e); + } + return this; + } + + public AdminEvent getEvent() { + return adminEvent; + } + + public void success() { + send(); + } + + private void send() { + boolean includeRepresentation = false; + if(realm.isAdminEventsDetailsEnabled()) { + includeRepresentation = true; + } + adminEvent.setTime(Time.toMillis(Time.currentTime())); + + if (store != null) { + if (realm.getAdminEnabledEventOperations() != null && !realm.getAdminEnabledEventOperations().isEmpty() ? realm.getAdminEnabledEventOperations().contains(adminEvent.getOperationType().name()) : adminEvent.getOperationType().isSaveByDefault()) { + try { + store.onEvent(adminEvent, includeRepresentation); + } catch (Throwable t) { + log.error("Failed to save event", t); + } + } + } + + if (listeners != null) { + for (EventListenerProvider l : listeners) { + try { + l.onEvent(adminEvent, includeRepresentation); + } catch (Throwable t) { + log.error("Failed to send type to " + l, t); + } + } + } + } +} diff --git a/events/api/src/main/java/org/keycloak/events/EventBuilder.java b/events/api/src/main/java/org/keycloak/events/EventBuilder.java index 4945989e9f..ee7a70a7cf 100644 --- a/events/api/src/main/java/org/keycloak/events/EventBuilder.java +++ b/events/api/src/main/java/org/keycloak/events/EventBuilder.java @@ -7,6 +7,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.util.Time; import java.util.HashMap; import java.util.LinkedList; @@ -149,7 +150,7 @@ public class EventBuilder { } private void send() { - event.setTime(System.currentTimeMillis()); + event.setTime(Time.toMillis(Time.currentTime())); if (store != null) { if (realm.getEnabledEventTypes() != null && !realm.getEnabledEventTypes().isEmpty() ? realm.getEnabledEventTypes().contains(event.getType().name()) : event.getType().isSaveByDefault()) { diff --git a/events/api/src/main/java/org/keycloak/events/EventListenerProvider.java b/events/api/src/main/java/org/keycloak/events/EventListenerProvider.java index 01ab302575..957d63961a 100644 --- a/events/api/src/main/java/org/keycloak/events/EventListenerProvider.java +++ b/events/api/src/main/java/org/keycloak/events/EventListenerProvider.java @@ -1,5 +1,6 @@ package org.keycloak.events; +import org.keycloak.events.admin.AdminEvent; import org.keycloak.provider.Provider; /** @@ -9,4 +10,6 @@ public interface EventListenerProvider extends Provider { public void onEvent(Event event); + public void onEvent(AdminEvent event, boolean includeRepresentation); + } diff --git a/events/api/src/main/java/org/keycloak/events/EventStoreProvider.java b/events/api/src/main/java/org/keycloak/events/EventStoreProvider.java index c742556f64..afdaf92154 100644 --- a/events/api/src/main/java/org/keycloak/events/EventStoreProvider.java +++ b/events/api/src/main/java/org/keycloak/events/EventStoreProvider.java @@ -1,5 +1,7 @@ package org.keycloak.events; +import org.keycloak.events.admin.AdminEventQuery; + /** * @author Stian Thorgersen */ @@ -7,10 +9,18 @@ public interface EventStoreProvider extends EventListenerProvider { public EventQuery createQuery(); + public AdminEventQuery createAdminQuery(); + public void clear(); public void clear(String realmId); public void clear(String realmId, long olderThan); + public void clearAdmin(); + + public void clearAdmin(String realmId); + + public void clearAdmin(String realmId, long olderThan); + } diff --git a/events/api/src/main/java/org/keycloak/events/admin/AdminEvent.java b/events/api/src/main/java/org/keycloak/events/admin/AdminEvent.java new file mode 100644 index 0000000000..fedd2bfadc --- /dev/null +++ b/events/api/src/main/java/org/keycloak/events/admin/AdminEvent.java @@ -0,0 +1,106 @@ +package org.keycloak.events.admin; + +import java.util.Map; + +/** + * @author Stian Thorgersen + */ +public class AdminEvent { + + private long time; + + private AuthDetails authDetails; + + private OperationType operationType; + + private String resourcePath; + + private String representation; + + private String error; + + /** + * Returns the time of the event + * + * @return time in millis + */ + public long getTime() { + return time; + } + + public void setTime(long time) { + this.time = time; + } + + /** + * Returns authentication details + * + * @return + */ + public AuthDetails getAuthDetails() { + return authDetails; + } + + public void setAuthDetails(AuthDetails authDetails) { + this.authDetails = authDetails; + } + + /** + * Returns the type of the operation + * + * @return + */ + public OperationType getOperationType() { + return operationType; + } + + public void setOperationType(OperationType operationType) { + this.operationType = operationType; + } + + /** + * Returns the path of the resource. For example: + *
    + *
  • realms - realm list
  • + *
  • realms/master - master realm
  • + *
  • realms/clients/00d4b16f-f1f9-4e73-8366-d76b18f3e0e1 - client within the master realm
  • + *
+ * + * @return + */ + public String getResourcePath() { + return resourcePath; + } + + public void setResourcePath(String resourcePath) { + this.resourcePath = resourcePath; + } + + /** + * Returns the updated JSON representation if operationType is CREATE or UPDATE. + * Otherwise returns null. + * + * @return + */ + public String getRepresentation() { + return representation; + } + + public void setRepresentation(String representation) { + this.representation = representation; + } + + /** + * If the event was unsuccessful returns the error message. Otherwise returns null. + * + * @return + */ + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + +} diff --git a/events/api/src/main/java/org/keycloak/events/admin/AdminEventQuery.java b/events/api/src/main/java/org/keycloak/events/admin/AdminEventQuery.java new file mode 100644 index 0000000000..45cd9e272d --- /dev/null +++ b/events/api/src/main/java/org/keycloak/events/admin/AdminEventQuery.java @@ -0,0 +1,102 @@ +package org.keycloak.events.admin; + +import java.util.List; + +/** + * @author Stian Thorgersen + */ +public interface AdminEventQuery { + + /** + * Search by authentication realm + * + * @param realm realm name + * @return Associated AdminEventQuery for method chaining + */ + AdminEventQuery authRealm(String realm); + + /** + * Search by authenticated client + * + * @param client client uuid + * @return Associated AdminEventQuery for method chaining + */ + AdminEventQuery authClient(String client); + + /** + * Search by authenticated user + * + * @param user user uuid + * @return Associated AdminEventQuery for method chaining + */ + AdminEventQuery authUser(String user); + + /** + * Search by request ip address + * + * @param ipAddress + * @return Associated AdminEventQuery for method chaining + */ + AdminEventQuery authIpAddress(String ipAddress); + + /** + * Search by operation type + * + * @param operations + * @return this for method chaining + */ + AdminEventQuery operation(OperationType... operations); + + /** + * Search by resource path. Supports wildcards * and **. For example: + *
    + *
  • */master - matches 'realms/master'
  • + *
  • **/00d4b16f - matches 'realms/master/clients/00d4b16f'
  • + *
  • realms/master/** - matches anything under 'realms/master'
  • + *
+ * + * @param resourcePath + * @return this for method chaining + */ + AdminEventQuery resourcePath(String resourcePath); + + /** + * Search by events after the specified time + * + * @param fromTime time in millis + * @return this for method chaining + */ + AdminEventQuery fromTime(String fromTime); + + /** + * Search by events before the specified time + * + * @param toTime time in millis + * @return this for method chaining + */ + AdminEventQuery toTime(String toTime); + + /** + * Used for pagination + * + * @param first first result to return + * @return this for method chaining + */ + AdminEventQuery firstResult(int first); + + /** + * Use for pagination + * + * @param max the maximum results to return + * @return this for method chaining + */ + AdminEventQuery maxResults(int max); + + /** + * Executes the query and returns the results + * + * @return + */ + List getResultList(); + +} diff --git a/events/api/src/main/java/org/keycloak/events/admin/AuthDetails.java b/events/api/src/main/java/org/keycloak/events/admin/AuthDetails.java new file mode 100644 index 0000000000..d32eff1a67 --- /dev/null +++ b/events/api/src/main/java/org/keycloak/events/admin/AuthDetails.java @@ -0,0 +1,48 @@ +package org.keycloak.events.admin; + +/** + * @author Stian Thorgersen + */ +public class AuthDetails { + + private String realmId; + + private String clientId; + + private String userId; + + private String ipAddress; + + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getIpAddress() { + return ipAddress; + } + + public void setIpAddress(String ipAddress) { + this.ipAddress = ipAddress; + } + +} diff --git a/events/api/src/main/java/org/keycloak/events/admin/AuthQuery.java b/events/api/src/main/java/org/keycloak/events/admin/AuthQuery.java new file mode 100644 index 0000000000..3be431fe3a --- /dev/null +++ b/events/api/src/main/java/org/keycloak/events/admin/AuthQuery.java @@ -0,0 +1,48 @@ +package org.keycloak.events.admin; + +/** + * @author Stian Thorgersen + */ +public class AuthQuery { + + private String realmId; + + private String clientId; + + private String userId; + + private String ipAddress; + + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getIpAddress() { + return ipAddress; + } + + public void setIpAddress(String ipAddress) { + this.ipAddress = ipAddress; + } + +} diff --git a/events/api/src/main/java/org/keycloak/events/admin/OperationType.java b/events/api/src/main/java/org/keycloak/events/admin/OperationType.java new file mode 100755 index 0000000000..ddce0e2694 --- /dev/null +++ b/events/api/src/main/java/org/keycloak/events/admin/OperationType.java @@ -0,0 +1,24 @@ +package org.keycloak.events.admin; + +/** + * @author Stian Thorgersen + */ +public enum OperationType { + + VIEW(false), + CREATE(true), + UPDATE(true), + DELETE(true), + ACTION(false); + + private boolean saveByDefault; + + OperationType(boolean saveByDefault) { + this.saveByDefault = saveByDefault; + } + + public boolean isSaveByDefault() { + return saveByDefault; + } + +} diff --git a/events/email/src/main/java/org/keycloak/events/email/EmailEventListenerProvider.java b/events/email/src/main/java/org/keycloak/events/email/EmailEventListenerProvider.java index 71a432acca..400ef04e15 100755 --- a/events/email/src/main/java/org/keycloak/events/email/EmailEventListenerProvider.java +++ b/events/email/src/main/java/org/keycloak/events/email/EmailEventListenerProvider.java @@ -3,6 +3,7 @@ package org.keycloak.events.email; import org.jboss.logging.Logger; import org.keycloak.email.EmailException; import org.keycloak.email.EmailProvider; +import org.keycloak.events.admin.AdminEvent; import org.keycloak.events.Event; import org.keycloak.events.EventListenerProvider; import org.keycloak.events.EventType; @@ -49,6 +50,11 @@ public class EmailEventListenerProvider implements EventListenerProvider { } } + @Override + public void onEvent(AdminEvent event, boolean includeRepresentation) { + + } + @Override public void close() { } diff --git a/events/jboss-logging/src/main/java/org/keycloak/events/log/JBossLoggingEventListenerProvider.java b/events/jboss-logging/src/main/java/org/keycloak/events/log/JBossLoggingEventListenerProvider.java index dba4304bf9..38283f75e5 100755 --- a/events/jboss-logging/src/main/java/org/keycloak/events/log/JBossLoggingEventListenerProvider.java +++ b/events/jboss-logging/src/main/java/org/keycloak/events/log/JBossLoggingEventListenerProvider.java @@ -1,6 +1,7 @@ package org.keycloak.events.log; import org.jboss.logging.Logger; +import org.keycloak.events.admin.AdminEvent; import org.keycloak.events.Event; import org.keycloak.events.EventListenerProvider; import org.keycloak.models.KeycloakContext; @@ -9,8 +10,8 @@ import org.keycloak.models.KeycloakSession; import javax.ws.rs.core.Cookie; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.UriInfo; + import java.util.Map; -import java.util.logging.Level; /** * @author Stian Thorgersen @@ -66,29 +67,42 @@ public class JBossLoggingEventListenerProvider implements EventListenerProvider } } } + + if(logger.isTraceEnabled()) { + setKeycloakContext(sb); + } - if (logger.isTraceEnabled()) { - KeycloakContext context = session.getContext(); - UriInfo uriInfo = context.getUri(); - HttpHeaders headers = context.getRequestHeaders(); - if (uriInfo != null) { - sb.append(", requestUri="); - sb.append(uriInfo.getRequestUri().toString()); - } + logger.log(logger.isTraceEnabled() ? Logger.Level.TRACE : level, sb.toString()); + } + } - if (headers != null) { - sb.append(", cookies=["); - boolean f = true; - for (Map.Entry e : headers.getCookies().entrySet()) { - if (f) { - f = false; - } else { - sb.append(", "); - } - sb.append(e.getValue().toString()); - } - sb.append("]"); - } + @Override + public void onEvent(AdminEvent adminEvent, boolean includeRepresentation) { + Logger.Level level = adminEvent.getError() != null ? errorLevel : successLevel; + + if (logger.isEnabled(level)) { + StringBuilder sb = new StringBuilder(); + + sb.append("operationType="); + sb.append(adminEvent.getOperationType()); + sb.append(", realmId="); + sb.append(adminEvent.getAuthDetails().getRealmId()); + sb.append(", clientId="); + sb.append(adminEvent.getAuthDetails().getClientId()); + sb.append(", userId="); + sb.append(adminEvent.getAuthDetails().getUserId()); + sb.append(", ipAddress="); + sb.append(adminEvent.getAuthDetails().getIpAddress()); + sb.append(", resourcePath="); + sb.append(adminEvent.getResourcePath()); + + if (adminEvent.getError() != null) { + sb.append(", error="); + sb.append(adminEvent.getError()); + } + + if(logger.isTraceEnabled()) { + setKeycloakContext(sb); } logger.log(logger.isTraceEnabled() ? Logger.Level.TRACE : level, sb.toString()); @@ -98,5 +112,30 @@ public class JBossLoggingEventListenerProvider implements EventListenerProvider @Override public void close() { } + + private void setKeycloakContext(StringBuilder sb) { + KeycloakContext context = session.getContext(); + UriInfo uriInfo = context.getUri(); + HttpHeaders headers = context.getRequestHeaders(); + if (uriInfo != null) { + sb.append(", requestUri="); + sb.append(uriInfo.getRequestUri().toString()); + } + + if (headers != null) { + sb.append(", cookies=["); + boolean f = true; + for (Map.Entry e : headers.getCookies().entrySet()) { + if (f) { + f = false; + } else { + sb.append(", "); + } + sb.append(e.getValue().toString()); + } + sb.append("]"); + } + + } } diff --git a/events/jpa/src/main/java/org/keycloak/events/jpa/AdminEventEntity.java b/events/jpa/src/main/java/org/keycloak/events/jpa/AdminEventEntity.java new file mode 100644 index 0000000000..f45cd328e0 --- /dev/null +++ b/events/jpa/src/main/java/org/keycloak/events/jpa/AdminEventEntity.java @@ -0,0 +1,126 @@ +package org.keycloak.events.jpa; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +/** + * @author Giriraj Sharma + */ +@Entity +@Table(name="ADMIN_EVENT_ENTITY") +public class AdminEventEntity { + + @Id + @Column(name="ID", length = 36) + private String id; + + @Column(name="ADMIN_EVENT_TIME") + private long time; + + @Column(name="OPERATION_TYPE") + private String operationType; + + @Column(name="REALM_ID") + private String authRealmId; + + @Column(name="CLIENT_ID") + private String authClientId; + + @Column(name="USER_ID") + private String authUserId; + + @Column(name="IP_ADDRESS") + private String authIpAddress; + + @Column(name="RESOURCE_PATH") + private String resourcePath; + + @Column(name="REPRESENTATION", length = 25500) + private String representation; + + @Column(name="ERROR") + private String error; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public long getTime() { + return time; + } + + public void setTime(long time) { + this.time = time; + } + + public String getOperationType() { + return operationType; + } + + public void setOperationType(String operationType) { + this.operationType = operationType; + } + + public String getAuthRealmId() { + return authRealmId; + } + + public void setAuthRealmId(String authRealmId) { + this.authRealmId = authRealmId; + } + + public String getAuthClientId() { + return authClientId; + } + + public void setAuthClientId(String authClientId) { + this.authClientId = authClientId; + } + + public String getAuthUserId() { + return authUserId; + } + + public void setAuthUserId(String authUserId) { + this.authUserId = authUserId; + } + + public String getAuthIpAddress() { + return authIpAddress; + } + + public void setAuthIpAddress(String authIpAddress) { + this.authIpAddress = authIpAddress; + } + + public String getResourcePath() { + return resourcePath; + } + + public void setResourcePath(String resourcePath) { + this.resourcePath = resourcePath; + } + + public String getRepresentation() { + return representation; + } + + public void setRepresentation(String representation) { + this.representation = representation; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + +} diff --git a/events/jpa/src/main/java/org/keycloak/events/jpa/JpaAdminEventQuery.java b/events/jpa/src/main/java/org/keycloak/events/jpa/JpaAdminEventQuery.java new file mode 100644 index 0000000000..edb19c069b --- /dev/null +++ b/events/jpa/src/main/java/org/keycloak/events/jpa/JpaAdminEventQuery.java @@ -0,0 +1,148 @@ +package org.keycloak.events.jpa; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import javax.persistence.EntityManager; +import javax.persistence.TypedQuery; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Expression; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; + +import org.keycloak.events.admin.AdminEvent; +import org.keycloak.events.admin.AdminEventQuery; +import org.keycloak.events.admin.OperationType; + +/** + * @author Giriraj Sharma + */ +public class JpaAdminEventQuery implements AdminEventQuery { + + private final EntityManager em; + private final CriteriaBuilder cb; + private final CriteriaQuery cq; + private final Root root; + private final ArrayList predicates; + private Integer firstResult; + private Integer maxResults; + + public JpaAdminEventQuery(EntityManager em) { + this.em = em; + + cb = em.getCriteriaBuilder(); + cq = cb.createQuery(AdminEventEntity.class); + root = cq.from(AdminEventEntity.class); + predicates = new ArrayList(); + } + + @Override + public AdminEventQuery operation(OperationType... operations) { + List operationStrings = new LinkedList(); + for (OperationType e : operations) { + operationStrings.add(e.toString()); + } + predicates.add(root.get("operationType").in(operationStrings)); + return this; + } + + @Override + public AdminEventQuery authRealm(String realmId) { + predicates.add(cb.equal(root.get("authRealmId"), realmId)); + return this; + } + + @Override + public AdminEventQuery authClient(String clientId) { + predicates.add(cb.equal(root.get("authClientId"), clientId)); + return this; + } + + @Override + public AdminEventQuery authUser(String userId) { + predicates.add(cb.equal(root.get("authUserId"), userId)); + return this; + } + + @Override + public AdminEventQuery authIpAddress(String ipAddress) { + predicates.add(cb.equal(root.get("authIpAddress"), ipAddress)); + return this; + } + + @Override + public AdminEventQuery resourcePath(String resourcePath) { + Expression rPath = root.get("resourcePath"); + predicates.add(cb.like(rPath, "%"+resourcePath+"%")); + return this; + } + + @Override + public AdminEventQuery fromTime(String fromTime) { + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + Long from = null; + try { + from = df.parse(fromTime).getTime(); + } catch (ParseException e) { + e.printStackTrace(); + } + predicates.add(cb.greaterThanOrEqualTo(root.get("time"), from)); + return this; + } + + @Override + public AdminEventQuery toTime(String toTime) { + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + Long to = null; + try { + to = df.parse(toTime).getTime(); + } catch (ParseException e) { + e.printStackTrace(); + } + predicates.add(cb.lessThanOrEqualTo(root.get("time"), to)); + return this; + } + + @Override + public AdminEventQuery firstResult(int firstResult) { + this.firstResult = firstResult; + return this; + } + + @Override + public AdminEventQuery maxResults(int maxResults) { + this.maxResults = maxResults; + return this; + } + + @Override + public List getResultList() { + if (!predicates.isEmpty()) { + cq.where(cb.and(predicates.toArray(new Predicate[predicates.size()]))); + } + + cq.orderBy(cb.desc(root.get("time"))); + + TypedQuery query = em.createQuery(cq); + + if (firstResult != null) { + query.setFirstResult(firstResult); + } + + if (maxResults != null) { + query.setMaxResults(maxResults); + } + + List events = new LinkedList(); + for (AdminEventEntity e : query.getResultList()) { + events.add(JpaEventStoreProvider.convertAdminEvent(e)); + } + + return events; + } + +} diff --git a/events/jpa/src/main/java/org/keycloak/events/jpa/JpaEventQuery.java b/events/jpa/src/main/java/org/keycloak/events/jpa/JpaEventQuery.java index 5e39d179eb..ffbf619526 100644 --- a/events/jpa/src/main/java/org/keycloak/events/jpa/JpaEventQuery.java +++ b/events/jpa/src/main/java/org/keycloak/events/jpa/JpaEventQuery.java @@ -131,7 +131,7 @@ public class JpaEventQuery implements EventQuery { List events = new LinkedList(); for (EventEntity e : query.getResultList()) { - events.add(JpaEventStoreProvider.convert(e)); + events.add(JpaEventStoreProvider.convertEvent(e)); } return events; diff --git a/events/jpa/src/main/java/org/keycloak/events/jpa/JpaEventStoreProvider.java b/events/jpa/src/main/java/org/keycloak/events/jpa/JpaEventStoreProvider.java index 8fca6ec361..3088f1ea18 100755 --- a/events/jpa/src/main/java/org/keycloak/events/jpa/JpaEventStoreProvider.java +++ b/events/jpa/src/main/java/org/keycloak/events/jpa/JpaEventStoreProvider.java @@ -3,12 +3,17 @@ package org.keycloak.events.jpa; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.type.TypeReference; import org.jboss.logging.Logger; +import org.keycloak.events.admin.AdminEvent; +import org.keycloak.events.admin.AdminEventQuery; +import org.keycloak.events.admin.AuthDetails; +import org.keycloak.events.admin.OperationType; import org.keycloak.events.Event; import org.keycloak.events.EventQuery; import org.keycloak.events.EventStoreProvider; import org.keycloak.events.EventType; import javax.persistence.EntityManager; + import java.io.IOException; import java.util.Map; import java.util.UUID; @@ -51,49 +56,119 @@ public class JpaEventStoreProvider implements EventStoreProvider { @Override public void onEvent(Event event) { - em.persist(convert(event)); + em.persist(convertEvent(event)); + } + + @Override + public AdminEventQuery createAdminQuery() { + return new JpaAdminEventQuery(em); + } + + @Override + public void clearAdmin() { + em.createQuery("delete from AdminEventEntity").executeUpdate(); + } + + @Override + public void clearAdmin(String authRealmId) { + em.createQuery("delete from AdminEventEntity where authRealmId = :authRealmId").setParameter("authRealmId", authRealmId).executeUpdate(); + } + + @Override + public void clearAdmin(String authRealmId, long olderThan) { + em.createQuery("delete from AdminEventEntity where authRealmId = :authRealmId and time < :time").setParameter("authRealmId", authRealmId).setParameter("time", olderThan).executeUpdate(); + } + + @Override + public void onEvent(AdminEvent event, boolean includeRepresentation) { + em.persist(convertAdminEvent(event, includeRepresentation)); } @Override public void close() { } - static EventEntity convert(Event o) { - EventEntity e = new EventEntity(); - e.setId(UUID.randomUUID().toString()); - e.setTime(o.getTime()); - e.setType(o.getType().toString()); - e.setRealmId(o.getRealmId()); - e.setClientId(o.getClientId()); - e.setUserId(o.getUserId()); - e.setSessionId(o.getSessionId()); - e.setIpAddress(o.getIpAddress()); - e.setError(o.getError()); + static EventEntity convertEvent(Event event) { + EventEntity eventEntity = new EventEntity(); + eventEntity.setId(UUID.randomUUID().toString()); + eventEntity.setTime(event.getTime()); + eventEntity.setType(event.getType().toString()); + eventEntity.setRealmId(event.getRealmId()); + eventEntity.setClientId(event.getClientId()); + eventEntity.setUserId(event.getUserId()); + eventEntity.setSessionId(event.getSessionId()); + eventEntity.setIpAddress(event.getIpAddress()); + eventEntity.setError(event.getError()); try { - e.setDetailsJson(mapper.writeValueAsString(o.getDetails())); + eventEntity.setDetailsJson(mapper.writeValueAsString(event.getDetails())); } catch (IOException ex) { logger.error("Failed to write log details", ex); } - return e; + return eventEntity; } - static Event convert(EventEntity o) { - Event e = new Event(); - e.setTime(o.getTime()); - e.setType(EventType.valueOf(o.getType())); - e.setRealmId(o.getRealmId()); - e.setClientId(o.getClientId()); - e.setUserId(o.getUserId()); - e.setSessionId(o.getSessionId()); - e.setIpAddress(o.getIpAddress()); - e.setError(o.getError()); + static Event convertEvent(EventEntity eventEntity) { + Event event = new Event(); + event.setTime(eventEntity.getTime()); + event.setType(EventType.valueOf(eventEntity.getType())); + event.setRealmId(eventEntity.getRealmId()); + event.setClientId(eventEntity.getClientId()); + event.setUserId(eventEntity.getUserId()); + event.setSessionId(eventEntity.getSessionId()); + event.setIpAddress(eventEntity.getIpAddress()); + event.setError(eventEntity.getError()); try { - Map details = mapper.readValue(o.getDetailsJson(), mapType); - e.setDetails(details); + Map details = mapper.readValue(eventEntity.getDetailsJson(), mapType); + event.setDetails(details); } catch (IOException ex) { logger.error("Failed to read log details", ex); } - return e; + return event; + } + + static AdminEventEntity convertAdminEvent(AdminEvent adminEvent, boolean includeRepresentation) { + AdminEventEntity adminEventEntity = new AdminEventEntity(); + adminEventEntity.setId(UUID.randomUUID().toString()); + adminEventEntity.setTime(adminEvent.getTime()); + setAuthDetails(adminEventEntity, adminEvent.getAuthDetails()); + adminEventEntity.setOperationType(adminEvent.getOperationType().toString()); + adminEventEntity.setResourcePath(adminEvent.getResourcePath()); + adminEventEntity.setError(adminEvent.getError()); + + if(includeRepresentation) { + adminEventEntity.setRepresentation(adminEvent.getRepresentation()); + } + return adminEventEntity; + } + + static AdminEvent convertAdminEvent(AdminEventEntity adminEventEntity) { + AdminEvent adminEvent = new AdminEvent(); + adminEvent.setTime(adminEventEntity.getTime()); + setAuthDetails(adminEvent, adminEventEntity); + adminEvent.setOperationType(OperationType.valueOf(adminEventEntity.getOperationType())); + adminEvent.setResourcePath(adminEventEntity.getResourcePath()); + adminEvent.setError(adminEventEntity.getError()); + + if(adminEventEntity.getRepresentation() != null) { + adminEvent.setRepresentation(adminEventEntity.getRepresentation()); + } + return adminEvent; + } + + private static void setAuthDetails(AdminEventEntity adminEventEntity, AuthDetails authDetails) { + adminEventEntity.setAuthRealmId(authDetails.getRealmId()); + adminEventEntity.setAuthClientId(authDetails.getClientId()); + adminEventEntity.setAuthUserId(authDetails.getUserId()); + adminEventEntity.setAuthIpAddress(authDetails.getIpAddress()); + } + + private static void setAuthDetails(AdminEvent adminEvent, AdminEventEntity adminEventEntity) { + AuthDetails authDetails = new AuthDetails(); + authDetails.setRealmId(adminEventEntity.getAuthRealmId()); + authDetails.setClientId(adminEventEntity.getAuthClientId()); + authDetails.setUserId(adminEventEntity.getAuthUserId()); + authDetails.setIpAddress(adminEventEntity.getAuthIpAddress()); + adminEvent.setAuthDetails(authDetails); } } diff --git a/events/mongo/src/main/java/org/keycloak/events/mongo/MongoAdminEventQuery.java b/events/mongo/src/main/java/org/keycloak/events/mongo/MongoAdminEventQuery.java new file mode 100644 index 0000000000..4cc366c688 --- /dev/null +++ b/events/mongo/src/main/java/org/keycloak/events/mongo/MongoAdminEventQuery.java @@ -0,0 +1,126 @@ +package org.keycloak.events.mongo; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Pattern; + +import org.keycloak.events.admin.AdminEvent; +import org.keycloak.events.admin.AdminEventQuery; +import org.keycloak.events.admin.OperationType; + +import com.mongodb.BasicDBObject; +import com.mongodb.BasicDBObjectBuilder; +import com.mongodb.DBCollection; +import com.mongodb.DBCursor; + +public class MongoAdminEventQuery implements AdminEventQuery{ + + private Integer firstResult; + private Integer maxResults; + private DBCollection audit; + private final BasicDBObject query; + + public MongoAdminEventQuery(DBCollection audit) { + this.audit = audit; + query = new BasicDBObject(); + } + + @Override + public AdminEventQuery operation(OperationType... operations) { + List operationStrings = new LinkedList(); + for (OperationType e : operations) { + operationStrings.add(e.toString()); + } + query.put("operationType", new BasicDBObject("$in", operationStrings)); + return this; + } + + @Override + public AdminEventQuery authRealm(String realmId) { + query.put("realmId", realmId); + return this; + } + + @Override + public AdminEventQuery authClient(String clientId) { + query.put("clientId", clientId); + return this; + } + + @Override + public AdminEventQuery authUser(String userId) { + query.put("userId", userId); + return this; + } + + @Override + public AdminEventQuery authIpAddress(String ipAddress) { + query.put("ipAddress", ipAddress); + return this; + } + + @Override + public AdminEventQuery resourcePath(String resourcePath) { + query.put("resourcePath", Pattern.compile(resourcePath)); + return this; + } + + @Override + public AdminEventQuery fromTime(String fromTime) { + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + Long from = null; + try { + from = df.parse(fromTime).getTime(); + } catch (ParseException e) { + e.printStackTrace(); + } + query.put("time", BasicDBObjectBuilder.start("$gte", from).get()); + return this; + } + + @Override + public AdminEventQuery toTime(String toTime) { + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + Long to = null; + try { + to = df.parse(toTime).getTime(); + } catch (ParseException e) { + e.printStackTrace(); + } + query.put("time", BasicDBObjectBuilder.start("$lte", to).get()); + return this; + } + + @Override + public AdminEventQuery firstResult(int firstResult) { + this.firstResult = firstResult; + return this; + } + + @Override + public AdminEventQuery maxResults(int maxResults) { + this.maxResults = maxResults; + return this; + } + + @Override + public List getResultList() { + DBCursor cur = audit.find(query).sort(new BasicDBObject("time", -1)); + if (firstResult != null) { + cur.skip(firstResult); + } + if (maxResults != null) { + cur.limit(maxResults); + } + + List events = new LinkedList(); + while (cur.hasNext()) { + events.add(MongoEventStoreProvider.convertAdminEvent((BasicDBObject) cur.next())); + } + + return events; + } + +} diff --git a/events/mongo/src/main/java/org/keycloak/events/mongo/MongoEventQuery.java b/events/mongo/src/main/java/org/keycloak/events/mongo/MongoEventQuery.java index c2569cc138..75165c751d 100755 --- a/events/mongo/src/main/java/org/keycloak/events/mongo/MongoEventQuery.java +++ b/events/mongo/src/main/java/org/keycloak/events/mongo/MongoEventQuery.java @@ -118,7 +118,7 @@ public class MongoEventQuery implements EventQuery { List events = new LinkedList(); while (cur.hasNext()) { - events.add(MongoEventStoreProvider.convert((BasicDBObject) cur.next())); + events.add(MongoEventStoreProvider.convertEvent((BasicDBObject) cur.next())); } return events; diff --git a/events/mongo/src/main/java/org/keycloak/events/mongo/MongoEventStoreProvider.java b/events/mongo/src/main/java/org/keycloak/events/mongo/MongoEventStoreProvider.java index 65b6573b69..4b0dd2c910 100755 --- a/events/mongo/src/main/java/org/keycloak/events/mongo/MongoEventStoreProvider.java +++ b/events/mongo/src/main/java/org/keycloak/events/mongo/MongoEventStoreProvider.java @@ -3,6 +3,11 @@ package org.keycloak.events.mongo; import com.mongodb.BasicDBObject; import com.mongodb.DBCollection; import com.mongodb.DBObject; + +import org.keycloak.events.admin.AdminEvent; +import org.keycloak.events.admin.AdminEventQuery; +import org.keycloak.events.admin.AuthDetails; +import org.keycloak.events.admin.OperationType; import org.keycloak.events.Event; import org.keycloak.events.EventQuery; import org.keycloak.events.EventStoreProvider; @@ -15,11 +20,13 @@ import java.util.Map; * @author Stian Thorgersen */ public class MongoEventStoreProvider implements EventStoreProvider { - + private DBCollection events; + private DBCollection adminEvents; - public MongoEventStoreProvider(DBCollection events) { + public MongoEventStoreProvider(DBCollection events, DBCollection adminEvents) { this.events = events; + this.adminEvents = adminEvents; } @Override @@ -47,27 +54,55 @@ public class MongoEventStoreProvider implements EventStoreProvider { @Override public void onEvent(Event event) { - events.insert(convert(event)); + events.insert(convertEvent(event)); + } + + @Override + public AdminEventQuery createAdminQuery() { + return new MongoAdminEventQuery(adminEvents); + } + + @Override + public void clearAdmin() { + adminEvents.remove(new BasicDBObject()); + } + + @Override + public void clearAdmin(String realmId) { + adminEvents.remove(new BasicDBObject("realmId", realmId)); + } + + @Override + public void clearAdmin(String realmId, long olderThan) { + BasicDBObject q = new BasicDBObject(); + q.put("realmId", realmId); + q.put("time", new BasicDBObject("$lt", olderThan)); + adminEvents.remove(q); + } + + @Override + public void onEvent(AdminEvent adminEvent, boolean includeRepresentation) { + adminEvents.insert(convertAdminEvent(adminEvent, includeRepresentation)); } @Override public void close() { } - static DBObject convert(Event o) { + static DBObject convertEvent(Event event) { BasicDBObject e = new BasicDBObject(); - e.put("time", o.getTime()); - e.put("type", o.getType().toString()); - e.put("realmId", o.getRealmId()); - e.put("clientId", o.getClientId()); - e.put("userId", o.getUserId()); - e.put("sessionId", o.getSessionId()); - e.put("ipAddress", o.getIpAddress()); - e.put("error", o.getError()); + e.put("time", event.getTime()); + e.put("type", event.getType().toString()); + e.put("realmId", event.getRealmId()); + e.put("clientId", event.getClientId()); + e.put("userId", event.getUserId()); + e.put("sessionId", event.getSessionId()); + e.put("ipAddress", event.getIpAddress()); + e.put("error", event.getError()); BasicDBObject details = new BasicDBObject(); - if (o.getDetails() != null) { - for (Map.Entry entry : o.getDetails().entrySet()) { + if (event.getDetails() != null) { + for (Map.Entry entry : event.getDetails().entrySet()) { details.put(entry.getKey(), entry.getValue()); } } @@ -76,16 +111,16 @@ public class MongoEventStoreProvider implements EventStoreProvider { return e; } - static Event convert(BasicDBObject o) { - Event e = new Event(); - e.setTime(o.getLong("time")); - e.setType(EventType.valueOf(o.getString("type"))); - e.setRealmId(o.getString("realmId")); - e.setClientId(o.getString("clientId")); - e.setUserId(o.getString("userId")); - e.setSessionId(o.getString("sessionId")); - e.setIpAddress(o.getString("ipAddress")); - e.setError(o.getString("error")); + static Event convertEvent(BasicDBObject o) { + Event event = new Event(); + event.setTime(o.getLong("time")); + event.setType(EventType.valueOf(o.getString("type"))); + event.setRealmId(o.getString("realmId")); + event.setClientId(o.getString("clientId")); + event.setUserId(o.getString("userId")); + event.setSessionId(o.getString("sessionId")); + event.setIpAddress(o.getString("ipAddress")); + event.setError(o.getString("error")); BasicDBObject d = (BasicDBObject) o.get("details"); if (d != null) { @@ -93,10 +128,55 @@ public class MongoEventStoreProvider implements EventStoreProvider { for (Object k : d.keySet()) { details.put((String) k, d.getString((String) k)); } - e.setDetails(details); + event.setDetails(details); + } + + return event; + } + + private static DBObject convertAdminEvent(AdminEvent adminEvent, boolean includeRepresentation) { + BasicDBObject e = new BasicDBObject(); + e.put("time", adminEvent.getTime()); + e.put("operationType", adminEvent.getOperationType().toString()); + setAuthDetails(e, adminEvent.getAuthDetails()); + e.put("resourcePath", adminEvent.getResourcePath()); + e.put("error", adminEvent.getError()); + + if(includeRepresentation) { + e.put("representation", adminEvent.getRepresentation()); } return e; } + + static AdminEvent convertAdminEvent(BasicDBObject o) { + AdminEvent adminEvent = new AdminEvent(); + adminEvent.setTime(o.getLong("time")); + adminEvent.setOperationType(OperationType.valueOf(o.getString("operationType"))); + setAuthDetails(adminEvent, o); + adminEvent.setResourcePath(o.getString("resourcePath")); + adminEvent.setError(o.getString("error")); + + if(o.getString("representation") != null) { + adminEvent.setRepresentation(o.getString("representation")); + } + return adminEvent; + } + + private static void setAuthDetails(BasicDBObject e, AuthDetails authDetails) { + e.put("realmId", authDetails.getRealmId()); + e.put("clientId", authDetails.getClientId()); + e.put("userId", authDetails.getUserId()); + e.put("ipAddress", authDetails.getIpAddress()); + } + + private static void setAuthDetails(AdminEvent adminEvent, BasicDBObject o) { + AuthDetails authDetails = new AuthDetails(); + authDetails.setRealmId(o.getString("realmId")); + authDetails.setClientId(o.getString("clientId")); + authDetails.setUserId(o.getString("userId")); + authDetails.setIpAddress(o.getString("ipAddress")); + adminEvent.setAuthDetails(authDetails); + } } diff --git a/events/mongo/src/main/java/org/keycloak/events/mongo/MongoEventStoreProviderFactory.java b/events/mongo/src/main/java/org/keycloak/events/mongo/MongoEventStoreProviderFactory.java index db4adeb859..cbf41ac31f 100755 --- a/events/mongo/src/main/java/org/keycloak/events/mongo/MongoEventStoreProviderFactory.java +++ b/events/mongo/src/main/java/org/keycloak/events/mongo/MongoEventStoreProviderFactory.java @@ -24,9 +24,12 @@ public class MongoEventStoreProviderFactory implements EventStoreProviderFactory MongoConnectionProvider connection = session.getProvider(MongoConnectionProvider.class); DBCollection collection = connection.getDB().getCollection("events"); + DBCollection adminCollection = connection.getDB().getCollection("adminEvents"); + collection.setWriteConcern(WriteConcern.UNACKNOWLEDGED); + adminCollection.setWriteConcern(WriteConcern.UNACKNOWLEDGED); - return new MongoEventStoreProvider(collection); + return new MongoEventStoreProvider(collection, adminCollection); } @Override diff --git a/events/syslog/src/main/java/org/keycloak/events/log/SysLoggingEventListenerProvider.java b/events/syslog/src/main/java/org/keycloak/events/log/SysLoggingEventListenerProvider.java index 05362868e9..14188a8388 100755 --- a/events/syslog/src/main/java/org/keycloak/events/log/SysLoggingEventListenerProvider.java +++ b/events/syslog/src/main/java/org/keycloak/events/log/SysLoggingEventListenerProvider.java @@ -1,5 +1,6 @@ package org.keycloak.events.log; +import org.keycloak.events.admin.AdminEvent; import org.keycloak.events.Event; import org.keycloak.events.EventListenerProvider; import org.productivity.java.syslog4j.SyslogConstants; @@ -58,6 +59,33 @@ public class SysLoggingEventListenerProvider implements EventListenerProvider { syslogger.log(level, sb.toString()); } + @Override + public void onEvent(AdminEvent adminEvent, boolean includeRepresentation) { + int level = adminEvent.getError() != null ? SyslogConstants.LEVEL_ERROR : SyslogConstants.LEVEL_INFO; + + StringBuilder sb = new StringBuilder(); + + sb.append("operationType="); + sb.append(adminEvent.getOperationType()); + sb.append(", realmId="); + sb.append(adminEvent.getAuthDetails().getRealmId()); + sb.append(", clientId="); + sb.append(adminEvent.getAuthDetails().getClientId()); + sb.append(", userId="); + sb.append(adminEvent.getAuthDetails().getUserId()); + sb.append(", ipAddress="); + sb.append(adminEvent.getAuthDetails().getIpAddress()); + sb.append(", resourcePath="); + sb.append(adminEvent.getResourcePath()); + + if (adminEvent.getError() != null) { + sb.append(", error="); + sb.append(adminEvent.getError()); + } + + syslogger.log(level, sb.toString()); + } + @Override public void close() { } diff --git a/examples/providers/event-listener-sysout/src/main/java/org/keycloak/examples/providers/events/SysoutEventListenerProvider.java b/examples/providers/event-listener-sysout/src/main/java/org/keycloak/examples/providers/events/SysoutEventListenerProvider.java index 8bd001ffe9..d81288e343 100755 --- a/examples/providers/event-listener-sysout/src/main/java/org/keycloak/examples/providers/events/SysoutEventListenerProvider.java +++ b/examples/providers/event-listener-sysout/src/main/java/org/keycloak/examples/providers/events/SysoutEventListenerProvider.java @@ -1,5 +1,7 @@ package org.keycloak.examples.providers.events; +import org.keycloak.events.admin.AdminEvent; +import org.keycloak.events.admin.OperationType; import org.keycloak.events.Event; import org.keycloak.events.EventListenerProvider; import org.keycloak.events.EventType; @@ -13,9 +15,11 @@ import java.util.Set; public class SysoutEventListenerProvider implements EventListenerProvider { private Set excludedEvents; + private Set excludedAdminOperations; - public SysoutEventListenerProvider(Set excludedEvents) { + public SysoutEventListenerProvider(Set excludedEvents, Set excludedAdminOpearations) { this.excludedEvents = excludedEvents; + this.excludedAdminOperations = excludedAdminOpearations; } @Override @@ -28,6 +32,16 @@ public class SysoutEventListenerProvider implements EventListenerProvider { } } + @Override + public void onEvent(AdminEvent event, boolean includeRepresentation) { + // Ignore excluded operations + if (excludedAdminOperations != null && excludedAdminOperations.contains(event.getOperationType())) { + return; + } else { + System.out.println("EVENT: " + toString(event)); + } + } + private String toString(Event event) { StringBuilder sb = new StringBuilder(); @@ -64,7 +78,31 @@ public class SysoutEventListenerProvider implements EventListenerProvider { return sb.toString(); } + + private String toString(AdminEvent adminEvent) { + StringBuilder sb = new StringBuilder(); + sb.append("operationType="); + sb.append(adminEvent.getOperationType()); + sb.append(", realmId="); + sb.append(adminEvent.getAuthDetails().getRealmId()); + sb.append(", clientId="); + sb.append(adminEvent.getAuthDetails().getClientId()); + sb.append(", userId="); + sb.append(adminEvent.getAuthDetails().getUserId()); + sb.append(", ipAddress="); + sb.append(adminEvent.getAuthDetails().getIpAddress()); + sb.append(", resourcePath="); + sb.append(adminEvent.getResourcePath()); + + if (adminEvent.getError() != null) { + sb.append(", error="); + sb.append(adminEvent.getError()); + } + + return sb.toString(); + } + @Override public void close() { } diff --git a/examples/providers/event-listener-sysout/src/main/java/org/keycloak/examples/providers/events/SysoutEventListenerProviderFactory.java b/examples/providers/event-listener-sysout/src/main/java/org/keycloak/examples/providers/events/SysoutEventListenerProviderFactory.java index 3f87d81c45..e7eb8d527e 100755 --- a/examples/providers/event-listener-sysout/src/main/java/org/keycloak/examples/providers/events/SysoutEventListenerProviderFactory.java +++ b/examples/providers/event-listener-sysout/src/main/java/org/keycloak/examples/providers/events/SysoutEventListenerProviderFactory.java @@ -4,6 +4,7 @@ import org.keycloak.Config; import org.keycloak.events.EventListenerProvider; import org.keycloak.events.EventListenerProviderFactory; import org.keycloak.events.EventType; +import org.keycloak.events.admin.OperationType; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; @@ -16,10 +17,11 @@ import java.util.Set; public class SysoutEventListenerProviderFactory implements EventListenerProviderFactory { private Set excludedEvents; + private Set excludedAdminOperations; @Override public EventListenerProvider create(KeycloakSession session) { - return new SysoutEventListenerProvider(excludedEvents); + return new SysoutEventListenerProvider(excludedEvents, excludedAdminOperations); } @Override @@ -31,6 +33,14 @@ public class SysoutEventListenerProviderFactory implements EventListenerProvider excludedEvents.add(EventType.valueOf(e)); } } + + String[] excludesOperations = config.getArray("excludesOperations"); + if (excludesOperations != null) { + excludedAdminOperations = new HashSet<>(); + for (String e : excludesOperations) { + excludedAdminOperations.add(OperationType.valueOf(e)); + } + } } @Override diff --git a/examples/providers/event-store-mem/src/main/java/org/keycloak/examples/providers/events/MemAdminEventQuery.java b/examples/providers/event-store-mem/src/main/java/org/keycloak/examples/providers/events/MemAdminEventQuery.java new file mode 100644 index 0000000000..830b7b52d7 --- /dev/null +++ b/examples/providers/event-store-mem/src/main/java/org/keycloak/examples/providers/events/MemAdminEventQuery.java @@ -0,0 +1,162 @@ +package org.keycloak.examples.providers.events; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.regex.Pattern; + +import org.keycloak.events.admin.AdminEvent; +import org.keycloak.events.admin.AdminEventQuery; +import org.keycloak.events.admin.OperationType; + +/** + * @author Giriraj Sharma + */ +public class MemAdminEventQuery implements AdminEventQuery { + + private List adminEvents; + + private int first; + private int max; + + public MemAdminEventQuery(List events) { + this.adminEvents = events; + } + + @Override + public AdminEventQuery operation(OperationType... operations) { + Iterator itr = this.adminEvents.iterator(); + while (itr.hasNext()) { + AdminEvent next = itr.next(); + boolean include = false; + for (OperationType e : operations) { + if (next.getOperationType().equals(e)) { + include = true; + break; + } + } + if (!include) { + itr.remove(); + } + } + return this; + } + + @Override + public AdminEventQuery authRealm(String realmId) { + Iterator itr = adminEvents.iterator(); + while (itr.hasNext()) { + if (!itr.next().getAuthDetails().getRealmId().equals(realmId)) { + itr.remove(); + } + } + return this; + } + + @Override + public AdminEventQuery authClient(String clientId) { + Iterator itr = adminEvents.iterator(); + while (itr.hasNext()) { + if (!itr.next().getAuthDetails().getClientId().equals(clientId)) { + itr.remove(); + } + } + return this; + } + + @Override + public AdminEventQuery authUser(String userId) { + Iterator itr = adminEvents.iterator(); + while (itr.hasNext()) { + if (!itr.next().getAuthDetails().getUserId().equals(userId)) { + itr.remove(); + } + } + return this; + } + + @Override + public AdminEventQuery authIpAddress(String ipAddress) { + Iterator itr = adminEvents.iterator(); + while (itr.hasNext()) { + if (!itr.next().getAuthDetails().getIpAddress().equals(ipAddress)) { + itr.remove(); + } + } + return this; + } + + @Override + public AdminEventQuery resourcePath(String resourcePath) { + Iterator itr = this.adminEvents.iterator(); + while (itr.hasNext()) { + if(!Pattern.compile(resourcePath).matcher(itr.next().getResourcePath()).find()) { + itr.remove(); + } + } + return (AdminEventQuery) this; + } + + @Override + public AdminEventQuery fromTime(String fromTime) { + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + Long from = null; + try { + from = df.parse(fromTime).getTime(); + } catch (ParseException e) { + e.printStackTrace(); + } + + Iterator itr = this.adminEvents.iterator(); + while (itr.hasNext()) { + if (!(itr.next().getTime() >= from)) { + itr.remove(); + } + } + return this; + } + + @Override + public AdminEventQuery toTime(String toTime) { + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + Long to = null; + try { + to = df.parse(toTime).getTime(); + } catch (ParseException e) { + e.printStackTrace(); + } + + Iterator itr = this.adminEvents.iterator(); + while (itr.hasNext()) { + if (!(itr.next().getTime() <= to)) { + itr.remove(); + } + } + return this; + } + + @Override + public AdminEventQuery firstResult(int result) { + this.first = result; + return this; + } + + @Override + public AdminEventQuery maxResults(int results) { + this.max = results; + return this; + } + + @Override + public List getResultList() { + if (adminEvents.size() < first) { + return Collections.emptyList(); + } + int end = first + max <= adminEvents.size() ? first + max : adminEvents.size(); + + return adminEvents.subList(first, end); + } + +} diff --git a/examples/providers/event-store-mem/src/main/java/org/keycloak/examples/providers/events/MemEventStoreProvider.java b/examples/providers/event-store-mem/src/main/java/org/keycloak/examples/providers/events/MemEventStoreProvider.java index 592455c485..7dd46060e8 100755 --- a/examples/providers/event-store-mem/src/main/java/org/keycloak/examples/providers/events/MemEventStoreProvider.java +++ b/examples/providers/event-store-mem/src/main/java/org/keycloak/examples/providers/events/MemEventStoreProvider.java @@ -1,5 +1,8 @@ package org.keycloak.examples.providers.events; +import org.keycloak.events.admin.AdminEvent; +import org.keycloak.events.admin.AdminEventQuery; +import org.keycloak.events.admin.OperationType; import org.keycloak.events.Event; import org.keycloak.events.EventQuery; import org.keycloak.events.EventStoreProvider; @@ -16,10 +19,16 @@ import java.util.Set; public class MemEventStoreProvider implements EventStoreProvider { private final List events; private final Set excludedEvents; + private final List adminEvents; + private final Set excludedOperations; - public MemEventStoreProvider(List events, Set excludedEvents) { + public MemEventStoreProvider(List events, Set excludedEvents, + List adminEvents, Set excludedOperations) { this.events = events; this.excludedEvents = excludedEvents; + + this.adminEvents = adminEvents; + this.excludedOperations = excludedOperations; } @Override @@ -64,6 +73,48 @@ public class MemEventStoreProvider implements EventStoreProvider { } } + @Override + public AdminEventQuery createAdminQuery() { + return new MemAdminEventQuery(new LinkedList<>(adminEvents)); + } + + @Override + public void clearAdmin() { + + } + + @Override + public void clearAdmin(String realmId) { + synchronized(adminEvents) { + Iterator itr = adminEvents.iterator(); + while (itr.hasNext()) { + if (itr.next().getAuthDetails().getRealmId().equals(realmId)) { + itr.remove(); + } + } + } + } + + @Override + public void clearAdmin(String realmId, long olderThan) { + synchronized(adminEvents) { + Iterator itr = adminEvents.iterator(); + while (itr.hasNext()) { + AdminEvent e = itr.next(); + if (e.getAuthDetails().getRealmId().equals(realmId) && e.getTime() < olderThan) { + itr.remove(); + } + } + } + } + + @Override + public void onEvent(AdminEvent adminEvent, boolean includeRepresentation) { + if (excludedOperations == null || !excludedOperations.contains(adminEvent.getOperationType())) { + adminEvents.add(0, adminEvent); + } + } + @Override public void close() { } diff --git a/examples/providers/event-store-mem/src/main/java/org/keycloak/examples/providers/events/MemEventStoreProviderFactory.java b/examples/providers/event-store-mem/src/main/java/org/keycloak/examples/providers/events/MemEventStoreProviderFactory.java index 83fd80e1f8..d09b1a3178 100755 --- a/examples/providers/event-store-mem/src/main/java/org/keycloak/examples/providers/events/MemEventStoreProviderFactory.java +++ b/examples/providers/event-store-mem/src/main/java/org/keycloak/examples/providers/events/MemEventStoreProviderFactory.java @@ -5,6 +5,8 @@ import org.keycloak.events.Event; import org.keycloak.events.EventStoreProvider; import org.keycloak.events.EventStoreProviderFactory; import org.keycloak.events.EventType; +import org.keycloak.events.admin.AdminEvent; +import org.keycloak.events.admin.OperationType; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; @@ -20,12 +22,13 @@ import java.util.Set; public class MemEventStoreProviderFactory implements EventStoreProviderFactory { private List events; - private Set excludedEvents; + private List adminEvents; + private Set excludedOperations; @Override public EventStoreProvider create(KeycloakSession session) { - return new MemEventStoreProvider(events, excludedEvents); + return new MemEventStoreProvider(events, excludedEvents, adminEvents, excludedOperations); } @Override @@ -39,6 +42,14 @@ public class MemEventStoreProviderFactory implements EventStoreProviderFactory { excludedEvents.add(EventType.valueOf(e)); } } + + String excludesOperations = config.get("excludesOperations"); + if (excludesOperations != null) { + excludedOperations = new HashSet<>(); + for (String e : excludesOperations.split(",")) { + excludedOperations.add(OperationType.valueOf(e)); + } + } } @Override @@ -49,6 +60,8 @@ public class MemEventStoreProviderFactory implements EventStoreProviderFactory { public void close() { events = null; excludedEvents = null; + adminEvents = null; + excludedOperations = null; } @Override diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js index 3ee1fbee95..2a470e2854 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -303,6 +303,18 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'RealmEventsCtrl' }) + .when('/realms/:realm/admin-events', { + templateUrl : resourceUrl + '/partials/realm-events-admin.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + serverInfo : function(ServerInfoLoader) { + return ServerInfoLoader(); + } + }, + controller : 'RealmAdminEventsCtrl' + }) .when('/realms/:realm/events-settings', { templateUrl : resourceUrl + '/partials/realm-events-config.html', resolve : { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index c30a68e892..f784137cbb 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -1214,7 +1214,7 @@ module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, real } }); -module.controller('RealmEventsConfigCtrl', function($scope, eventsConfig, RealmEventsConfig, RealmEvents, realm, serverInfo, $location, Notifications, TimeUnit, Dialog) { +module.controller('RealmEventsConfigCtrl', function($scope, eventsConfig, RealmEventsConfig, RealmEvents, RealmAdminEvents, realm, serverInfo, $location, Notifications, TimeUnit, Dialog) { $scope.realm = realm; $scope.eventsConfig = eventsConfig; @@ -1232,7 +1232,13 @@ module.controller('RealmEventsConfigCtrl', function($scope, eventsConfig, RealmE $scope.eventSelectOptions = { 'multiple': true, 'simple_tags': true, - 'tags': serverInfo.eventTypes + 'tags': serverInfo.enums['eventType'] + }; + + $scope.adminEnabledEventOperationsOptions = { + 'multiple': true, + 'simple_tags': true, + 'tags': serverInfo.enums['operationType'] }; var oldCopy = angular.copy($scope.eventsConfig); @@ -1272,6 +1278,14 @@ module.controller('RealmEventsConfigCtrl', function($scope, eventsConfig, RealmE }); }); }; + + $scope.clearAdminEvents = function() { + Dialog.confirmDelete($scope.realm.realm, 'admin-events', function() { + RealmAdminEvents.remove({ id : $scope.realm.realm }, function() { + Notifications.success("The admin events has been cleared."); + }); + }); + }; }); module.controller('RealmEventsCtrl', function($scope, RealmEvents, realm, serverInfo) { @@ -1281,7 +1295,7 @@ module.controller('RealmEventsCtrl', function($scope, RealmEvents, realm, server $scope.eventSelectOptions = { 'multiple': true, 'simple_tags': true, - 'tags': serverInfo.eventTypes + 'tags': serverInfo.enums['eventType'] }; $scope.query = { @@ -1342,6 +1356,106 @@ module.controller('RealmEventsCtrl', function($scope, RealmEvents, realm, server $scope.update(); }); +module.controller('RealmAdminEventsCtrl', function($scope, RealmAdminEvents, realm, serverInfo, $modal, $filter) { + $scope.realm = realm; + $scope.page = 0; + + $scope.query = { + id : realm.realm, + max : 5, + first : 0 + } + $scope.query.authRealm = 'master'; + + $scope.adminEnabledEventOperationsOptions = { + 'multiple': true, + 'simple_tags': true, + 'tags': serverInfo.enums['operationType'] + }; + + $scope.update = function() { + $scope.query.first = 0; + for (var i in $scope.query) { + if ($scope.query[i] === '') { + delete $scope.query[i]; + } + } + $scope.events = RealmAdminEvents.query($scope.query); + } + + $scope.reset = function() { + $scope.query.first = 0; + $scope.query.max = 5; + $scope.query.operationTypes = ''; + $scope.query.resourcePath = ''; + $scope.query.authRealm = 'master'; + $scope.query.authClient = ''; + $scope.query.authUser = ''; + $scope.query.authIpAddress = ''; + $scope.query.dateFrom = ''; + $scope.query.dateTo = ''; + + $scope.update(); + } + + $scope.queryUpdate = function() { + for (var i in $scope.query) { + if ($scope.query[i] === '') { + delete $scope.query[i]; + } + } + $scope.events = RealmAdminEvents.query($scope.query); + } + + $scope.firstPage = function() { + $scope.query.first = 0; + $scope.queryUpdate(); + } + + $scope.previousPage = function() { + $scope.query.first -= parseInt($scope.query.max); + if ($scope.query.first < 0) { + $scope.query.first = 0; + } + $scope.queryUpdate(); + } + + $scope.nextPage = function() { + $scope.query.first += parseInt($scope.query.max); + $scope.queryUpdate(); + } + + $scope.update(); + + $scope.viewRepresentation = function(event) { + $modal.open({ + templateUrl: resourceUrl + '/partials/modal/realm-events-admin-representation.html', + controller: 'RealmAdminEventsModalCtrl', + resolve: { + event: function () { + return event; + } + } + }) + } + + $scope.viewAuth = function(event) { + $modal.open({ + templateUrl: resourceUrl + '/partials/modal/realm-events-admin-auth.html', + controller: 'RealmAdminEventsModalCtrl', + resolve: { + event: function () { + return event; + } + } + }) + } +}); + +module.controller('RealmAdminEventsModalCtrl', function($scope, $filter, event) { + $scope.event = event; +}); + module.controller('RealmBruteForceCtrl', function($scope, Realm, realm, $http, $location, Dialog, Notifications, TimeUnit) { console.log('RealmBruteForceCtrl'); diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js index 7708821a47..1b4302797a 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js @@ -180,6 +180,12 @@ module.factory('RealmEvents', function($resource) { }); }); +module.factory('RealmAdminEvents', function($resource) { + return $resource(authUrl + '/admin/realms/:id/admin-events', { + id : '@realm' + }); +}); + module.factory('RealmLDAPConnectionTester', function($resource) { return $resource(authUrl + '/admin/realms/:realm/testLDAPConnection'); }); diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/modal/realm-events-admin-auth.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/modal/realm-events-admin-auth.html new file mode 100644 index 0000000000..8f765f073a --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/modal/realm-events-admin-auth.html @@ -0,0 +1,8 @@ +
+ + + + + +
Realm{{event.authDetails.realmId}}
Client{{event.authDetails.clientId}}
User{{event.authDetails.userId}}
IP Address{{event.authDetails.ipAddress}}
+
\ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/modal/realm-events-admin-representation.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/modal/realm-events-admin-representation.html new file mode 100644 index 0000000000..837a164ec3 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/modal/realm-events-admin-representation.html @@ -0,0 +1,3 @@ +
+

+
\ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-events-admin.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-events-admin.html new file mode 100755 index 0000000000..7168aab6af --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-events-admin.html @@ -0,0 +1,128 @@ +
+

+ Admin Events {{realm.realm|capitalize}} + Displays saved admin events for the realm. Events are related to admin account, for example a realm creation. To enable persisted events go to config. +

+ + +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + +
+
+
+ +
+ +
+
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+ Authentication Details + +
+ +
+ +
+ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
TimeOperation TypeResource PathDetails
+ + + +
{{event.time|date:'shortDate'}}
{{event.time|date:'mediumTime'}}
{{event.operationType}}{{event.resourcePath}} + + +
+
+
+ \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-events-config.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-events-config.html index 1b3438558b..7d05980b2c 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-events-config.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-events-config.html @@ -2,69 +2,120 @@

Events {{realm.realm|capitalize}} Events

+
+

{{realm.realm}} Events Config

-
-
-
- -
- + + +
+
+ + +
+ +
+ +
- If enabled events are saved to the database which makes events available to the admin and account management consoles. -
+
-
- +
+ Login Events Settings -
- +
+ +
+ +
+
- Configure what event types are saved. By default events related to login and users modifying their accounts are persisted. -
+
+ -
- -
- -
- Deletes all events in the database. -
-
- -
- - -
-
- Sets the expiration for events. Expired events are periodically deleted from the database. -
+
+ +
-
- - -
- +
- Configure what listeners receive events for the realm. -
-
+
+ +
+ +
+ +
+
+ +
+ +
+
+ + +
+
+ -
- - -
- + +
+ Admin Events Settings + +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ + +
+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+ +
- - + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-events.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-events.html index 7c450148e6..8df1629d68 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-events.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-events.html @@ -5,7 +5,8 @@ @@ -121,4 +122,4 @@ - + \ No newline at end of file diff --git a/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/beans/AdminEventBean.java b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/beans/AdminEventBean.java new file mode 100644 index 0000000000..ca225fc96d --- /dev/null +++ b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/beans/AdminEventBean.java @@ -0,0 +1,37 @@ +package org.keycloak.email.freemarker.beans; + +import java.util.Date; + +import org.keycloak.events.admin.AdminEvent; + +/** + * @author Giriraj Sharma + */ +public class AdminEventBean { + + private AdminEvent adminEvent; + + public AdminEventBean(AdminEvent adminEvent) { + this.adminEvent = adminEvent; + } + + public Date getDate() { + return new Date(adminEvent.getTime()); + } + + public String getOperationType() { + return adminEvent.getOperationType().toString().toLowerCase(); + } + + public String getClient() { + return adminEvent.getAuthDetails().getClientId(); + } + + public String getIpAddress() { + return adminEvent.getAuthDetails().getIpAddress(); + } + + public String getResourcePath() { + return adminEvent.getResourcePath(); + } +} diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java index 9c2960424b..8ac3b5ae2e 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmModel.java +++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java @@ -232,7 +232,19 @@ public interface RealmModel extends RoleContainerModel { Set getEnabledEventTypes(); void setEnabledEventTypes(Set enabledEventTypes); + + boolean isAdminEventsEnabled(); + void setAdminEventsEnabled(boolean enabled); + + Set getAdminEnabledEventOperations(); + + void setAdminEnabledEventOperations(Set adminEnabledEventOperations); + + boolean isAdminEventsDetailsEnabled(); + + void setAdminEventsDetailsEnabled(boolean enabled); + ClientModel getMasterAdminClient(); void setMasterAdminClient(ClientModel client); diff --git a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java index 3c978f90fb..cbb4c6d20d 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java @@ -63,7 +63,11 @@ public class RealmEntity extends AbstractIdentifiableEntity { private long eventsExpiration; private List eventsListeners = new ArrayList(); private List enabledEventTypes = new ArrayList(); - + + protected boolean adminEventsEnabled; + protected List adminEnabledEventOperations = new ArrayList();; + protected boolean adminEventsDetailsEnabled; + private String masterAdminClient; private boolean internationalizationEnabled; @@ -391,6 +395,30 @@ public class RealmEntity extends AbstractIdentifiableEntity { this.enabledEventTypes = enabledEventTypes; } + public boolean isAdminEventsEnabled() { + return adminEventsEnabled; + } + + public void setAdminEventsEnabled(boolean adminEventsEnabled) { + this.adminEventsEnabled = adminEventsEnabled; + } + + public List getAdminEnabledEventOperations() { + return adminEnabledEventOperations; + } + + public void setAdminEnabledEventOperations(List adminEnabledEventOperations) { + this.adminEnabledEventOperations = adminEnabledEventOperations; + } + + public boolean isAdminEventsDetailsEnabled() { + return adminEventsDetailsEnabled; + } + + public void setAdminEventsDetailsEnabled(boolean adminEventsDetailsEnabled) { + this.adminEventsDetailsEnabled = adminEventsDetailsEnabled; + } + public String getMasterAdminClient() { return masterAdminClient; } diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 32f4e8942b..144c140bc9 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -14,6 +14,7 @@ import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; + import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.FederatedIdentityRepresentation; @@ -192,6 +193,14 @@ public class ModelToRepresentation { rep.setEnabledEventTypes(new LinkedList(realm.getEnabledEventTypes())); } + rep.setAdminEventsEnabled(realm.isAdminEventsEnabled()); + + if(realm.getAdminEnabledEventOperations() != null) { + rep.setAdminEnabledEventOperations(new LinkedList(realm.getAdminEnabledEventOperations())); + } + + rep.setAdminEventsDetailsEnabled(realm.isAdminEventsDetailsEnabled()); + return rep; } diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index a80e582d9c..2154c13e8e 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -410,10 +410,16 @@ public class RepresentationToModel { if (rep.getAccountTheme() != null) realm.setAccountTheme(rep.getAccountTheme()); if (rep.getAdminTheme() != null) realm.setAdminTheme(rep.getAdminTheme()); if (rep.getEmailTheme() != null) realm.setEmailTheme(rep.getEmailTheme()); + if (rep.isEventsEnabled() != null) realm.setEventsEnabled(rep.isEventsEnabled()); if (rep.getEventsExpiration() != null) realm.setEventsExpiration(rep.getEventsExpiration()); if (rep.getEventsListeners() != null) realm.setEventsListeners(new HashSet<>(rep.getEventsListeners())); if (rep.getEnabledEventTypes() != null) realm.setEnabledEventTypes(new HashSet<>(rep.getEnabledEventTypes())); + + if (rep.isAdminEventsEnabled() != null) realm.setAdminEventsEnabled(rep.isAdminEventsEnabled()); + if (rep.getAdminEnabledEventOperations() != null) realm.setAdminEnabledEventOperations(new HashSet<>(rep.getAdminEnabledEventOperations())); + if (rep.isAdminEventsDetailsEnabled() != null) realm.setAdminEventsDetailsEnabled(rep.isAdminEventsDetailsEnabled()); + if (rep.getPasswordPolicy() != null) realm.setPasswordPolicy(new PasswordPolicy(rep.getPasswordPolicy())); diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java index cdd658a39e..65019819d4 100755 --- a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java +++ b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java @@ -958,6 +958,40 @@ public class RealmAdapter implements RealmModel { } } + @Override + public boolean isAdminEventsEnabled() { + return realm.isAdminEventsEnabled(); + } + + @Override + public void setAdminEventsEnabled(boolean enabled) { + realm.setAdminEventsEnabled(enabled); + } + + @Override + public Set getAdminEnabledEventOperations() { + return new HashSet(realm.getAdminEnabledEventOperations()); + } + + @Override + public void setAdminEnabledEventOperations(Set adminEnabledEventOperations) { + if (adminEnabledEventOperations != null) { + realm.setAdminEnabledEventOperations(new ArrayList(adminEnabledEventOperations)); + } else { + realm.setAdminEnabledEventOperations(Collections.EMPTY_LIST); + } + } + + @Override + public boolean isAdminEventsDetailsEnabled() { + return realm.isAdminEventsDetailsEnabled(); + } + + @Override + public void setAdminEventsDetailsEnabled(boolean enabled) { + realm.setAdminEventsDetailsEnabled(enabled); + } + @Override public ClientModel getMasterAdminClient() { return this.masterAdminApp; diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java index 68a65ac2ff..dfa657f1b8 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java @@ -750,6 +750,42 @@ public class RealmAdapter implements RealmModel { updated.setEnabledEventTypes(enabledEventTypes); } + @Override + public boolean isAdminEventsEnabled() { + if (updated != null) return updated.isAdminEventsEnabled(); + return cached.isAdminEventsEnabled(); + } + + @Override + public void setAdminEventsEnabled(boolean enabled) { + getDelegateForUpdate(); + updated.setAdminEventsEnabled(enabled); + } + + @Override + public Set getAdminEnabledEventOperations() { + if (updated != null) return updated.getAdminEnabledEventOperations(); + return cached.getAdminEnabledEventOperations(); + } + + @Override + public void setAdminEnabledEventOperations(Set adminEnabledEventOperations) { + getDelegateForUpdate(); + updated.setAdminEnabledEventOperations(adminEnabledEventOperations); + } + + @Override + public boolean isAdminEventsDetailsEnabled() { + if (updated != null) return updated.isAdminEventsDetailsEnabled(); + return cached.isAdminEventsDetailsEnabled(); + } + + @Override + public void setAdminEventsDetailsEnabled(boolean enabled) { + getDelegateForUpdate(); + updated.setAdminEventsDetailsEnabled(enabled); + } + @Override public ClientModel getMasterAdminClient() { return cacheSession.getRealm(Config.getAdminRealm()).getClientById(cached.getMasterAdminClient()); @@ -923,4 +959,5 @@ public class RealmAdapter implements RealmModel { } return null; } + } diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java index e4908140f1..97f2667bb8 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java @@ -79,6 +79,9 @@ public class CachedRealm { private long eventsExpiration; private Set eventsListeners = new HashSet(); private Set enabledEventTypes = new HashSet(); + protected boolean adminEventsEnabled; + protected Set adminEnabledEventOperations = new HashSet(); + protected boolean adminEventsDetailsEnabled; private List defaultRoles = new LinkedList(); private Map realmRoles = new HashMap(); private Map clients = new HashMap(); @@ -153,6 +156,11 @@ public class CachedRealm { eventsExpiration = model.getEventsExpiration(); eventsListeners.addAll(model.getEventsListeners()); enabledEventTypes.addAll(model.getEnabledEventTypes()); + + adminEventsEnabled = model.isAdminEventsEnabled(); + adminEnabledEventOperations.addAll(model.getAdminEnabledEventOperations()); + adminEventsDetailsEnabled = model.isAdminEventsDetailsEnabled(); + defaultRoles.addAll(model.getDefaultRoles()); masterAdminClient = model.getMasterAdminClient().getId(); @@ -350,6 +358,18 @@ public class CachedRealm { return enabledEventTypes; } + public boolean isAdminEventsEnabled() { + return adminEventsEnabled; + } + + public Set getAdminEnabledEventOperations() { + return adminEnabledEventOperations; + } + + public boolean isAdminEventsDetailsEnabled() { + return adminEventsDetailsEnabled; + } + public List getUserFederationProviders() { return userFederationProviders; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 8618ddb7a1..bcad0bb718 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -22,6 +22,7 @@ import org.keycloak.models.utils.KeycloakModelUtils; import javax.persistence.EntityManager; import javax.persistence.TypedQuery; + import java.security.Key; import java.security.PrivateKey; import java.security.PublicKey; @@ -1065,7 +1066,41 @@ public class RealmAdapter implements RealmModel { realm.setEnabledEventTypes(enabledEventTypes); em.flush(); } + + @Override + public boolean isAdminEventsEnabled() { + return realm.isAdminEventsEnabled(); + } + @Override + public void setAdminEventsEnabled(boolean enabled) { + realm.setAdminEventsEnabled(enabled); + em.flush(); + } + + @Override + public Set getAdminEnabledEventOperations() { + return realm.getAdminEnabledEventOperations(); + } + + @Override + public void setAdminEnabledEventOperations(Set adminEnabledEventOperations) { + realm.setAdminEnabledEventOperations(adminEnabledEventOperations); + em.flush(); + + } + + @Override + public boolean isAdminEventsDetailsEnabled() { + return realm.isAdminEventsDetailsEnabled(); + } + + @Override + public void setAdminEventsDetailsEnabled(boolean enabled) { + realm.setAdminEventsDetailsEnabled(enabled); + em.flush(); + } + @Override public ClientModel getMasterAdminClient() { return new ClientAdapter(this, em, session, realm.getMasterAdminClient()); @@ -1327,4 +1362,5 @@ public class RealmAdapter implements RealmModel { mapping.setConfig(config); return mapping; } + } \ No newline at end of file diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java index efdb4a8cdb..9c69d1db83 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java @@ -134,7 +134,18 @@ public class RealmEntity { @Column(name="VALUE") @CollectionTable(name="REALM_ENABLED_EVENT_TYPES", joinColumns={ @JoinColumn(name="REALM_ID") }) protected Set enabledEventTypes = new HashSet(); - + + @Column(name="ADMIN_EVENTS_ENABLED") + protected boolean adminEventsEnabled; + + @ElementCollection + @Column(name="VALUE") + @CollectionTable(name="REALM_ENABLED_ADMIN_EVENT_OPERATIONS", joinColumns={ @JoinColumn(name="REALM_ID") }) + protected Set adminEnabledEventOperations = new HashSet(); + + @Column(name="ADMIN_EVENTS_DETAILS_ENABLED") + protected boolean adminEventsDetailsEnabled; + @OneToOne @JoinColumn(name="MASTER_ADMIN_CLIENT") protected ClientEntity masterAdminClient; @@ -437,6 +448,30 @@ public class RealmEntity { this.enabledEventTypes = enabledEventTypes; } + public boolean isAdminEventsEnabled() { + return adminEventsEnabled; + } + + public void setAdminEventsEnabled(boolean adminEventsEnabled) { + this.adminEventsEnabled = adminEventsEnabled; + } + + public Set getAdminEnabledEventOperations() { + return adminEnabledEventOperations; + } + + public void setAdminEnabledEventOperations(Set adminEnabledEventOperations) { + this.adminEnabledEventOperations = adminEnabledEventOperations; + } + + public boolean isAdminEventsDetailsEnabled() { + return adminEventsDetailsEnabled; + } + + public void setAdminEventsDetailsEnabled(boolean adminEventsDetailsEnabled) { + this.adminEventsDetailsEnabled = adminEventsDetailsEnabled; + } + public ClientEntity getMasterAdminClient() { return masterAdminClient; } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java index fb08290832..c5b999ef22 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java @@ -2,6 +2,7 @@ package org.keycloak.models.mongo.keycloak.adapters; import com.mongodb.DBObject; import com.mongodb.QueryBuilder; + import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.enums.SslRequired; import org.keycloak.models.ClientModel; @@ -985,7 +986,45 @@ public class RealmAdapter extends AbstractMongoAdapter impleme } updateRealm(); } + + @Override + public boolean isAdminEventsEnabled() { + return realm.isAdminEventsEnabled(); + } + @Override + public void setAdminEventsEnabled(boolean enabled) { + realm.setAdminEventsEnabled(enabled); + updateRealm(); + + } + + @Override + public Set getAdminEnabledEventOperations() { + return new HashSet(realm.getAdminEnabledEventOperations()); + } + + @Override + public void setAdminEnabledEventOperations(Set adminEnabledEventOperations) { + if (adminEnabledEventOperations != null) { + realm.setAdminEnabledEventOperations(new ArrayList(adminEnabledEventOperations)); + } else { + realm.setAdminEnabledEventOperations(Collections.EMPTY_LIST); + } + updateRealm(); + } + + @Override + public boolean isAdminEventsDetailsEnabled() { + return realm.isAdminEventsDetailsEnabled(); + } + + @Override + public void setAdminEventsDetailsEnabled(boolean enabled) { + realm.setAdminEventsDetailsEnabled(enabled); + updateRealm(); + } + @Override public ClientModel getMasterAdminClient() { MongoClientEntity appData = getMongoStore().loadEntity(MongoClientEntity.class, realm.getMasterAdminClient(), invocationContext); diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java index 88d9eb53d4..e3a36517e9 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -168,6 +168,12 @@ public class RealmManager { if(rep.getEnabledEventTypes() != null) { realm.setEnabledEventTypes(new HashSet(rep.getEnabledEventTypes())); } + + realm.setAdminEventsEnabled(rep.isAdminEventsEnabled()); + if(rep.getAdminEnabledEventOperations() != null) { + realm.setAdminEnabledEventOperations(new HashSet(rep.getAdminEnabledEventOperations())); + } + realm.setAdminEventsDetailsEnabled(rep.isAdminEventsDetailsEnabled()); } // Should be RealmManager moved to model/api instead of referencing methods this way? diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java index 34e8272cd5..8b8253fd6c 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java @@ -8,6 +8,7 @@ import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.UnauthorizedException; import org.keycloak.ClientConnection; +import org.keycloak.events.AdminEventBuilder; import org.keycloak.jose.jws.JWSInput; import org.keycloak.models.AdminRoles; import org.keycloak.models.ClientModel; @@ -29,6 +30,7 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; + import java.io.IOException; /** @@ -185,8 +187,11 @@ public class AdminRoot { } Cors.add(request).allowedOrigins(auth.getToken()).allowedMethods("GET", "PUT", "POST", "DELETE").auth().build(response); - - RealmsAdminResource adminResource = new RealmsAdminResource(auth, tokenManager); + + AdminEventBuilder adminEvent = new AdminEventBuilder(auth.getRealm(), session, clientConnection); + adminEvent.user(auth.getUser()).client(auth.getClient()); + + RealmsAdminResource adminResource = new RealmsAdminResource(auth, tokenManager, adminEvent); ResteasyProviderFactory.getInstance().injectProperties(adminResource); return adminResource; } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java index 16a0fbe18d..4e75efb60b 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java @@ -6,6 +6,8 @@ import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; import org.jboss.resteasy.spi.BadRequestException; import org.jboss.resteasy.spi.NotAcceptableException; import org.jboss.resteasy.spi.NotFoundException; +import org.keycloak.events.AdminEventBuilder; +import org.keycloak.events.admin.OperationType; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -21,6 +23,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.UriInfo; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -47,11 +50,12 @@ public class ClientAttributeCertificateResource { private RealmAuth auth; protected ClientModel client; protected KeycloakSession session; + protected AdminEventBuilder adminEvent; protected String attributePrefix; protected String privateAttribute; protected String certificateAttribute; - public ClientAttributeCertificateResource(RealmModel realm, RealmAuth auth, ClientModel client, KeycloakSession session, String attributePrefix) { + public ClientAttributeCertificateResource(RealmModel realm, RealmAuth auth, ClientModel client, KeycloakSession session, String attributePrefix, AdminEventBuilder adminEvent) { this.realm = realm; this.auth = auth; this.client = client; @@ -59,6 +63,7 @@ public class ClientAttributeCertificateResource { this.attributePrefix = attributePrefix; this.privateAttribute = attributePrefix + "." + PRIVATE_KEY; this.certificateAttribute = attributePrefix + "." + X509CERTIFICATE; + this.adminEvent = adminEvent; } public static class ClientKeyPairInfo { @@ -94,6 +99,7 @@ public class ClientAttributeCertificateResource { ClientKeyPairInfo info = new ClientKeyPairInfo(); info.setCertificate(client.getAttribute(certificateAttribute)); info.setPrivateKey(client.getAttribute(privateAttribute)); + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); return info; } @@ -134,6 +140,7 @@ public class ClientAttributeCertificateResource { ClientKeyPairInfo info = new ClientKeyPairInfo(); info.setCertificate(client.getAttribute(certificateAttribute)); info.setPrivateKey(client.getAttribute(privateAttribute)); + adminEvent.operation(OperationType.CREATE).resourcePath(session.getContext().getUri().getPath()).representation(info).success(); return info; } @@ -190,7 +197,8 @@ public class ClientAttributeCertificateResource { client.setAttribute(certificateAttribute, certPem); info.setCertificate(certPem); } - + + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).representation(info).success(); return info; } @@ -316,6 +324,9 @@ public class ClientAttributeCertificateResource { stream.flush(); stream.close(); byte[] rtn = stream.toByteArray(); + + adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri().getPath()).success(); + return rtn; } catch (Exception e) { throw new RuntimeException(e); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java index f769101903..3f0d0ea2e4 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java @@ -5,6 +5,8 @@ import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.BadRequestException; import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.events.AdminEventBuilder; +import org.keycloak.events.admin.OperationType; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; @@ -40,6 +42,7 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; + import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; @@ -57,6 +60,7 @@ public class ClientResource { protected static final Logger logger = Logger.getLogger(ClientResource.class); protected RealmModel realm; private RealmAuth auth; + private AdminEventBuilder adminEvent; protected ClientModel client; protected KeycloakSession session; @@ -70,19 +74,21 @@ public class ClientResource { return keycloak; } - public ClientResource(RealmModel realm, RealmAuth auth, ClientModel clientModel, KeycloakSession session) { + public ClientResource(RealmModel realm, RealmAuth auth, ClientModel clientModel, KeycloakSession session, AdminEventBuilder adminEvent) { this.realm = realm; this.auth = auth; this.client = clientModel; this.session = session; + this.adminEvent = adminEvent; auth.init(RealmAuth.Resource.CLIENT); } @Path("protocol-mappers") public ProtocolMappersResource getProtocolMappers() { - ProtocolMappersResource mappers = new ProtocolMappersResource(client, auth); + ProtocolMappersResource mappers = new ProtocolMappersResource(client, auth, adminEvent); ResteasyProviderFactory.getInstance().injectProperties(mappers); + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return mappers; } @@ -98,6 +104,7 @@ public class ClientResource { try { RepresentationToModel.updateClient(rep, client); + adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo.getPath()).representation(rep).success(); return Response.noContent().build(); } catch (ModelDuplicateException e) { return ErrorResponse.exists("Client " + rep.getClientId() + " already exists"); @@ -115,7 +122,7 @@ public class ClientResource { @Produces(MediaType.APPLICATION_JSON) public ClientRepresentation getClient() { auth.requireView(); - + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return ModelToRepresentation.toRepresentation(client); } @@ -126,7 +133,7 @@ public class ClientResource { */ @Path("certificates/{attr}") public ClientAttributeCertificateResource getCertficateResource(@PathParam("attr") String attributePrefix) { - return new ClientAttributeCertificateResource(realm, auth, client, session, attributePrefix); + return new ClientAttributeCertificateResource(realm, auth, client, session, attributePrefix, adminEvent); } @@ -145,6 +152,8 @@ public class ClientResource { ClientManager clientManager = new ClientManager(new RealmManager(session)); Object rep = clientManager.toInstallationRepresentation(realm, client, getKeycloakApplication().getBaseUri(uriInfo)); + + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).success(); // TODO Temporary solution to pretty-print return JsonSerialization.mapper.writerWithDefaultPrettyPrinter().writeValueAsString(rep); @@ -164,6 +173,9 @@ public class ClientResource { auth.requireView(); ClientManager clientManager = new ClientManager(new RealmManager(session)); + + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).success(); + return clientManager.toJBossSubsystemConfig(realm, client, getKeycloakApplication().getBaseUri(uriInfo)); } @@ -176,6 +188,7 @@ public class ClientResource { public void deleteClient() { auth.requireManage(); new ClientManager(new RealmManager(session)).removeClient(realm, client); + adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo.getPath()).success(); } @@ -194,6 +207,7 @@ public class ClientResource { logger.debug("regenerateSecret"); UserCredentialModel cred = KeycloakModelUtils.generateSecret(client); CredentialRepresentation rep = ModelToRepresentation.toRepresentation(cred); + adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo.getPath()).representation(rep).success(); return rep; } @@ -212,6 +226,7 @@ public class ClientResource { logger.debug("getClientSecret"); UserCredentialModel model = UserCredentialModel.secret(client.getSecret()); if (model == null) throw new NotFoundException("Client does not have a secret"); + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return ModelToRepresentation.toRepresentation(model); } @@ -222,12 +237,12 @@ public class ClientResource { */ @Path("scope-mappings") public ScopeMappedResource getScopeMappedResource() { - return new ScopeMappedResource(realm, auth, client, session); + return new ScopeMappedResource(realm, auth, client, session, adminEvent); } @Path("roles") public RoleContainerResource getRoleContainerResource() { - return new RoleContainerResource(realm, auth, client); + return new RoleContainerResource(realm, auth, client, adminEvent); } /** @@ -243,7 +258,7 @@ public class ClientResource { public Set getAllowedOrigins() { auth.requireView(); - + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return client.getWebOrigins(); } @@ -261,6 +276,7 @@ public class ClientResource { auth.requireManage(); client.setWebOrigins(allowedOrigins); + adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo.getPath()).representation(client).success(); } /** @@ -279,6 +295,7 @@ public class ClientResource { for (String origin : allowedOrigins) { client.removeWebOrigin(origin); } + adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo.getPath()).success(); } /** @@ -289,9 +306,11 @@ public class ClientResource { @POST public GlobalRequestResult pushRevocation() { auth.requireManage(); - return new ResourceAdminManager(session).pushClientRevocationPolicy(uriInfo.getRequestUri(), realm, client); + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).success(); + return new ResourceAdminManager(session).pushClientRevocationPolicy(uriInfo.getRequestUri(), realm, client); + } - + /** * Number of user sessions associated with this client * @@ -309,6 +328,7 @@ public class ClientResource { auth.requireView(); Map map = new HashMap(); map.put("count", session.sessions().getActiveUserSessions(client.getRealm(), client)); + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return map; } @@ -330,6 +350,7 @@ public class ClientResource { UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(userSession); sessions.add(rep); } + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return sessions; } @@ -341,7 +362,9 @@ public class ClientResource { @POST public GlobalRequestResult logoutAll() { auth.requireManage(); + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).success(); return new ResourceAdminManager(session).logoutClient(uriInfo.getRequestUri(), realm, client); + } /** @@ -356,7 +379,9 @@ public class ClientResource { if (user == null) { throw new NotFoundException("User not found"); } - new ResourceAdminManager(session).logoutUserFromClient(uriInfo.getRequestUri(), realm, client, user); + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).success(); + new ResourceAdminManager(session).logoutUserFromClient(uriInfo.getRequestUri(), realm, client, user); + } /** @@ -376,6 +401,7 @@ public class ClientResource { } if (logger.isDebugEnabled()) logger.debug("Register node: " + node); client.registerNode(node, Time.currentTime()); + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).success(); } /** @@ -394,8 +420,8 @@ public class ClientResource { if (time == null) { throw new NotFoundException("Client does not have a node " + node); } - client.unregisterNode(node); + adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo.getPath()).success(); } /** @@ -408,9 +434,10 @@ public class ClientResource { @NoCache public GlobalRequestResult testNodesAvailable() { auth.requireManage(); - logger.debug("Test availability of cluster nodes"); - + logger.debug("Test availability of cluster nodes"); + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).success(); return new ResourceAdminManager(session).testNodesAvailability(uriInfo.getRequestUri(), realm, client); + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientsByIdResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientsByIdResource.java index 46f2f8774d..bb9ae8dfa3 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientsByIdResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientsByIdResource.java @@ -1,5 +1,6 @@ package org.keycloak.services.resources.admin; +import org.keycloak.events.AdminEventBuilder; import org.keycloak.models.ClientModel; import org.keycloak.models.RealmModel; @@ -8,8 +9,8 @@ import org.keycloak.models.RealmModel; * @version $Revision: 1 $ */ public class ClientsByIdResource extends ClientsResource { - public ClientsByIdResource(RealmModel realm, RealmAuth auth) { - super(realm, auth); + public ClientsByIdResource(RealmModel realm, RealmAuth auth, AdminEventBuilder adminEvent) { + super(realm, auth, adminEvent); } @Override diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java index b68bfc79f2..db635cb2b1 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java @@ -4,6 +4,8 @@ import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.events.AdminEventBuilder; +import org.keycloak.events.admin.OperationType; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; @@ -23,6 +25,7 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; + import java.util.ArrayList; import java.util.List; @@ -36,13 +39,15 @@ public class ClientsResource { protected static final Logger logger = Logger.getLogger(RealmAdminResource.class); protected RealmModel realm; private RealmAuth auth; - + private AdminEventBuilder adminEvent; + @Context protected KeycloakSession session; - public ClientsResource(RealmModel realm, RealmAuth auth) { + public ClientsResource(RealmModel realm, RealmAuth auth, AdminEventBuilder adminEvent) { this.realm = realm; this.auth = auth; + this.adminEvent = adminEvent; auth.init(RealmAuth.Resource.CLIENT); } @@ -71,7 +76,7 @@ public class ClientsResource { rep.add(client); } } - + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); return rep; } @@ -89,6 +94,9 @@ public class ClientsResource { try { ClientModel clientModel = RepresentationToModel.createClient(session, realm, rep, true); + adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo.getAbsolutePathBuilder() + .path(getClientPath(clientModel)).build().toString().substring(uriInfo.getBaseUri().toString().length())) + .representation(rep).success(); return Response.created(uriInfo.getAbsolutePathBuilder().path(getClientPath(clientModel)).build()).build(); } catch (ModelDuplicateException e) { return ErrorResponse.exists("Client " + rep.getClientId() + " already exists"); @@ -111,8 +119,9 @@ public class ClientsResource { if (clientModel == null) { throw new NotFoundException("Could not find client: " + name); } - ClientResource clientResource = new ClientResource(realm, auth, clientModel, session); + ClientResource clientResource = new ClientResource(realm, auth, clientModel, session, adminEvent); ResteasyProviderFactory.getInstance().injectProperties(clientResource); + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); return clientResource; } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java index 539c41094e..d70cfd9c4a 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java @@ -6,6 +6,8 @@ import org.jboss.resteasy.spi.NotFoundException; import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProviderFactory; import org.keycloak.broker.provider.IdentityProviderMapper; +import org.keycloak.events.AdminEventBuilder; +import org.keycloak.events.admin.OperationType; import org.keycloak.models.ClientModel; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.IdentityProviderMapperModel; @@ -13,7 +15,6 @@ import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.ModelDuplicateException; -import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.ModelToRepresentation; @@ -25,7 +26,6 @@ import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; import org.keycloak.representations.idm.IdentityProviderMapperTypeRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.services.ErrorResponse; -import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.social.SocialIdentityProvider; import javax.ws.rs.Consumes; @@ -41,6 +41,7 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; + import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; @@ -58,14 +59,16 @@ public class IdentityProviderResource { private final RealmModel realm; private final KeycloakSession session; private final IdentityProviderModel identityProviderModel; - + private final AdminEventBuilder adminEvent; + @Context private UriInfo uriInfo; - public IdentityProviderResource(RealmAuth auth, RealmModel realm, KeycloakSession session, IdentityProviderModel identityProviderModel) { + public IdentityProviderResource(RealmAuth auth, RealmModel realm, KeycloakSession session, IdentityProviderModel identityProviderModel, AdminEventBuilder adminEvent) { this.realm = realm; this.session = session; this.identityProviderModel = identityProviderModel; this.auth = auth; + this.adminEvent = adminEvent; } @GET @@ -74,7 +77,9 @@ public class IdentityProviderResource { public IdentityProviderRepresentation getIdentityProvider() { this.auth.requireView(); IdentityProviderRepresentation rep = ModelToRepresentation.toRepresentation(this.identityProviderModel); - + + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); + return rep; } @@ -84,7 +89,9 @@ public class IdentityProviderResource { this.auth.requireManage(); this.realm.removeIdentityProviderByAlias(this.identityProviderModel.getAlias()); - + + adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo.getPath()).success(); + return Response.noContent().build(); } @@ -108,7 +115,9 @@ public class IdentityProviderResource { updateUsersAfterProviderAliasChange(this.session.users().getUsers(this.realm), oldProviderId, newProviderId); } - + + adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo.getPath()).representation(providerRep).success(); + return Response.noContent().build(); } catch (ModelDuplicateException e) { return ErrorResponse.exists("Identity Provider " + providerRep.getAlias() + " already exists"); @@ -164,6 +173,7 @@ public class IdentityProviderResource { try { this.auth.requireView(); IdentityProviderFactory factory = getIdentityProviderFactory(); + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).success(); return factory.create(identityProviderModel).export(uriInfo, realm, format); } catch (Exception e) { return ErrorResponse.error("Could not export public broker configuration for identity provider [" + identityProviderModel.getProviderId() + "].", Response.Status.NOT_FOUND); @@ -202,6 +212,7 @@ public class IdentityProviderResource { } } } + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return types; } @@ -215,6 +226,7 @@ public class IdentityProviderResource { for (IdentityProviderMapperModel model : realm.getIdentityProviderMappersByAlias(identityProviderModel.getAlias())) { mappers.add(ModelToRepresentation.toRepresentation(model)); } + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return mappers; } @@ -225,6 +237,9 @@ public class IdentityProviderResource { auth.requireManage(); IdentityProviderMapperModel model = RepresentationToModel.toModel(mapper); model = realm.addIdentityProviderMapper(model); + adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo.getAbsolutePathBuilder() + .path(model.getId()).build().toString().substring(uriInfo.getBaseUri().toString().length())) + .representation(mapper).success(); return Response.created(uriInfo.getAbsolutePathBuilder().path(model.getId()).build()).build(); } @@ -237,6 +252,7 @@ public class IdentityProviderResource { auth.requireView(); IdentityProviderMapperModel model = realm.getIdentityProviderMapperById(id); if (model == null) throw new NotFoundException("Model not found"); + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return ModelToRepresentation.toRepresentation(model); } @@ -250,6 +266,8 @@ public class IdentityProviderResource { if (model == null) throw new NotFoundException("Model not found"); model = RepresentationToModel.toModel(rep); realm.updateIdentityProviderMapper(model); + adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo.getPath()).representation(rep).success(); + } @DELETE @@ -260,6 +278,8 @@ public class IdentityProviderResource { IdentityProviderMapperModel model = realm.getIdentityProviderMapperById(id); if (model == null) throw new NotFoundException("Model not found"); realm.removeIdentityProviderMapper(model); + adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo.getPath()).success(); + } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java index ec33725c22..cd3693ebe6 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java @@ -6,8 +6,10 @@ import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.broker.provider.IdentityProvider; -import org.keycloak.broker.provider.IdentityProviderFactory; -import org.keycloak.connections.httpclient.HttpClientProvider; +import org.keycloak.broker.provider.IdentityProviderFactory; +import org.keycloak.connections.httpclient.HttpClientProvider; +import org.keycloak.events.AdminEventBuilder; +import org.keycloak.events.admin.OperationType; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; @@ -29,6 +31,7 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; + import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -45,12 +48,14 @@ public class IdentityProvidersResource { private final RealmModel realm; private final KeycloakSession session; private RealmAuth auth; + private AdminEventBuilder adminEvent; - public IdentityProvidersResource(RealmModel realm, KeycloakSession session, RealmAuth auth) { + public IdentityProvidersResource(RealmModel realm, KeycloakSession session, RealmAuth auth, AdminEventBuilder adminEvent) { this.realm = realm; this.session = session; this.auth = auth; this.auth.init(RealmAuth.Resource.IDENTITY_PROVIDER); + this.adminEvent = adminEvent; } @Path("/providers/{provider_id}") @@ -62,9 +67,9 @@ public class IdentityProvidersResource { IdentityProviderFactory providerFactory = getProviderFactorytById(providerId); if (providerFactory != null) { + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); return Response.ok(providerFactory).build(); } - return Response.status(BAD_REQUEST).build(); } @@ -80,6 +85,9 @@ public class IdentityProvidersResource { InputStream inputStream = file.getBody(InputStream.class, null); IdentityProviderFactory providerFactory = getProviderFactorytById(providerId); Map config = providerFactory.parseConfig(inputStream); + + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).representation(config).success(); + return config; } @@ -97,13 +105,14 @@ public class IdentityProvidersResource { IdentityProviderFactory providerFactory = getProviderFactorytById(providerId); Map config; config = providerFactory.parseConfig(inputStream); + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).representation(config).success(); return config; } finally { try { inputStream.close(); } catch (IOException e) { } - } + } } @GET @@ -118,7 +127,7 @@ public class IdentityProvidersResource { for (IdentityProviderModel identityProviderModel : realm.getIdentityProviders()) { representations.add(ModelToRepresentation.toRepresentation(identityProviderModel)); } - + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); return representations; } @@ -130,7 +139,11 @@ public class IdentityProvidersResource { try { this.realm.addIdentityProvider(RepresentationToModel.toModel(representation)); - + + adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo.getAbsolutePathBuilder() + .path(representation.getProviderId()).build().toString().substring(uriInfo.getBaseUri().toString().length())) + .representation(representation).success(); + return Response.created(uriInfo.getAbsolutePathBuilder().path(representation.getProviderId()).build()).build(); } catch (ModelDuplicateException e) { return ErrorResponse.exists("Identity Provider " + representation.getAlias() + " already exists"); @@ -153,9 +166,10 @@ public class IdentityProvidersResource { throw new NotFoundException("Could not find identity provider: " + alias); } - IdentityProviderResource identityProviderResource = new IdentityProviderResource(this.auth, realm, session, identityProviderModel); + IdentityProviderResource identityProviderResource = new IdentityProviderResource(this.auth, realm, session, identityProviderModel, adminEvent); ResteasyProviderFactory.getInstance().injectProperties(identityProviderResource); - + + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); return identityProviderResource; } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java index 36428b89fb..a211087cfe 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java @@ -3,6 +3,8 @@ package org.keycloak.services.resources.admin; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.NotFoundException; +import org.keycloak.events.AdminEventBuilder; +import org.keycloak.events.admin.OperationType; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; @@ -22,6 +24,7 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; + import java.util.LinkedList; import java.util.List; @@ -37,6 +40,8 @@ public class ProtocolMappersResource { protected ClientModel client; protected RealmAuth auth; + + protected AdminEventBuilder adminEvent; @Context protected UriInfo uriInfo; @@ -44,9 +49,10 @@ public class ProtocolMappersResource { @Context protected KeycloakSession session; - public ProtocolMappersResource(ClientModel client, RealmAuth auth) { + public ProtocolMappersResource(ClientModel client, RealmAuth auth, AdminEventBuilder adminEvent) { this.auth = auth; this.client = client; + this.adminEvent = adminEvent; auth.init(RealmAuth.Resource.USER); } @@ -67,6 +73,7 @@ public class ProtocolMappersResource { for (ProtocolMapperModel mapper : client.getProtocolMappers()) { if (mapper.getProtocol().equals(protocol)) mappers.add(ModelToRepresentation.toRepresentation(mapper)); } + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return mappers; } @@ -83,6 +90,9 @@ public class ProtocolMappersResource { auth.requireManage(); ProtocolMapperModel model = RepresentationToModel.toModel(rep); model = client.addProtocolMapper(model); + adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo.getAbsolutePathBuilder() + .path(model.getId()).build().toString().substring(uriInfo.getBaseUri().toString().length())) + .representation(rep).success(); return Response.created(uriInfo.getAbsolutePathBuilder().path(model.getId()).build()).build(); } /** @@ -95,10 +105,12 @@ public class ProtocolMappersResource { @Consumes(MediaType.APPLICATION_JSON) public void createMapper(List reps) { auth.requireManage(); + ProtocolMapperModel model = null; for (ProtocolMapperRepresentation rep : reps) { - ProtocolMapperModel model = RepresentationToModel.toModel(rep); + model = RepresentationToModel.toModel(rep); model = client.addProtocolMapper(model); } + adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo.getPath()).representation(reps).success(); } @GET @@ -111,6 +123,7 @@ public class ProtocolMappersResource { for (ProtocolMapperModel mapper : client.getProtocolMappers()) { mappers.add(ModelToRepresentation.toRepresentation(mapper)); } + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return mappers; } @@ -122,6 +135,7 @@ public class ProtocolMappersResource { auth.requireView(); ProtocolMapperModel model = client.getProtocolMapperById(id); if (model == null) throw new NotFoundException("Model not found"); + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return ModelToRepresentation.toRepresentation(model); } @@ -135,6 +149,7 @@ public class ProtocolMappersResource { if (model == null) throw new NotFoundException("Model not found"); model = RepresentationToModel.toModel(rep); client.updateProtocolMapper(model); + adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo.getPath()).representation(rep).success(); } @DELETE @@ -145,6 +160,8 @@ public class ProtocolMappersResource { ProtocolMapperModel model = client.getProtocolMapperById(id); if (model == null) throw new NotFoundException("Model not found"); client.removeProtocolMapper(model); + adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo.getPath()).success(); + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index b0d171b53c..11b83a06ee 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -5,10 +5,15 @@ import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.ClientConnection; +import org.keycloak.Config; +import org.keycloak.events.AdminEventBuilder; import org.keycloak.events.Event; import org.keycloak.events.EventQuery; import org.keycloak.events.EventStoreProvider; import org.keycloak.events.EventType; +import org.keycloak.events.admin.AdminEvent; +import org.keycloak.events.admin.AdminEventQuery; +import org.keycloak.events.admin.OperationType; import org.keycloak.exportimport.ClientImporter; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; @@ -46,6 +51,7 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; + import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -63,6 +69,7 @@ public class RealmAdminResource { protected RealmAuth auth; protected RealmModel realm; private TokenManager tokenManager; + private AdminEventBuilder adminEvent; @Context protected KeycloakSession session; @@ -76,10 +83,11 @@ public class RealmAdminResource { @Context protected HttpHeaders headers; - public RealmAdminResource(RealmAuth auth, RealmModel realm, TokenManager tokenManager) { + public RealmAdminResource(RealmAuth auth, RealmModel realm, TokenManager tokenManager, AdminEventBuilder adminEvent) { this.auth = auth; this.realm = realm; this.tokenManager = tokenManager; + this.adminEvent = adminEvent; auth.init(RealmAuth.Resource.REALM); } @@ -102,7 +110,7 @@ public class RealmAdminResource { */ @Path("clients") public ClientsResource getClients() { - ClientsResource clientsResource = new ClientsResource(realm, auth); + ClientsResource clientsResource = new ClientsResource(realm, auth, adminEvent); ResteasyProviderFactory.getInstance().injectProperties(clientsResource); return clientsResource; } @@ -114,7 +122,7 @@ public class RealmAdminResource { */ @Path("clients-by-id") public ClientsByIdResource getClientsById() { - ClientsByIdResource clientsResource = new ClientsByIdResource(realm, auth); + ClientsByIdResource clientsResource = new ClientsByIdResource(realm, auth, adminEvent); ResteasyProviderFactory.getInstance().injectProperties(clientsResource); return clientsResource; } @@ -126,7 +134,7 @@ public class RealmAdminResource { */ @Path("roles") public RoleContainerResource getRoleContainerResource() { - return new RoleContainerResource(realm, auth, realm); + return new RoleContainerResource(realm, auth, realm, adminEvent); } /** @@ -148,12 +156,14 @@ public class RealmAdminResource { CacheUserProvider cache = (CacheUserProvider)session.userStorage(); rep.setUserCacheEnabled(cache.isEnabled()); } + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return rep; } else { auth.requireAny(); RealmRepresentation rep = new RealmRepresentation(); rep.setRealm(realm.getName()); + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return rep; } } @@ -188,7 +198,8 @@ public class RealmAdminResource { for (final UserFederationProviderModel fedProvider : federationProviders) { usersSyncManager.refreshPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), fedProvider, realm.getId()); } - + + adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo.getPath()).representation(rep).success(); return Response.noContent().build(); } catch (PatternSyntaxException e) { return ErrorResponse.exists("Specified regex pattern(s) is invalid."); @@ -209,6 +220,8 @@ public class RealmAdminResource { if (!new RealmManager(session).removeRealm(realm)) { throw new NotFoundException("Realm doesn't exist"); + } else { + adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo.getPath()).success(); } } @@ -219,7 +232,7 @@ public class RealmAdminResource { */ @Path("users") public UsersResource users() { - UsersResource users = new UsersResource(realm, auth, tokenManager); + UsersResource users = new UsersResource(realm, auth, tokenManager, adminEvent); ResteasyProviderFactory.getInstance().injectProperties(users); //resourceContext.initResource(users); return users; @@ -227,7 +240,7 @@ public class RealmAdminResource { @Path("user-federation") public UserFederationResource userFederation() { - UserFederationResource fed = new UserFederationResource(realm, auth); + UserFederationResource fed = new UserFederationResource(realm, auth, adminEvent); ResteasyProviderFactory.getInstance().injectProperties(fed); //resourceContext.initResource(fed); return fed; @@ -240,7 +253,7 @@ public class RealmAdminResource { */ @Path("roles-by-id") public RoleByIdResource rolesById() { - RoleByIdResource resource = new RoleByIdResource(realm, auth); + RoleByIdResource resource = new RoleByIdResource(realm, auth, adminEvent); ResteasyProviderFactory.getInstance().injectProperties(resource); //resourceContext.initResource(resource); return resource; @@ -254,7 +267,8 @@ public class RealmAdminResource { @POST public GlobalRequestResult pushRevocation() { auth.requireManage(); - return new ResourceAdminManager(session).pushRealmRevocationPolicy(uriInfo.getRequestUri(), realm); + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).success(); + return new ResourceAdminManager(session).pushRealmRevocationPolicy(uriInfo.getRequestUri(), realm); } /** @@ -266,7 +280,8 @@ public class RealmAdminResource { @POST public GlobalRequestResult logoutAll() { session.sessions().removeUserSessions(realm); - return new ResourceAdminManager(session).logoutAll(uriInfo.getRequestUri(), realm); + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).success(); + return new ResourceAdminManager(session).logoutAll(uriInfo.getRequestUri(), realm); } /** @@ -279,8 +294,10 @@ public class RealmAdminResource { @DELETE public void deleteSession(@PathParam("session") String sessionId) { UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId); - if (userSession == null) throw new NotFoundException("Sesssion not found"); - AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, connection, headers, true); + if (userSession == null) throw new NotFoundException("Sesssion not found"); + AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, connection, headers, true); + adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo.getPath()).success(); + } /** @@ -302,6 +319,7 @@ public class RealmAdminResource { if (size == 0) continue; stats.put(client.getClientId(), size); } + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).representation(stats).success(); return stats; } @@ -327,6 +345,7 @@ public class RealmAdminResource { map.put("active", size + ""); data.add(map); } + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).representation(data).success(); return data; } @@ -366,6 +385,8 @@ public class RealmAdminResource { * @param client app or oauth client name * @param user user id * @param ipAddress + * @param dateTo + * @param dateFrom * @param firstResult * @param maxResults * @return @@ -419,6 +440,86 @@ public class RealmAdminResource { return query.getResultList(); } + + /** + * Query admin events. Returns all admin events, or will query based on URL query parameters listed here + * + * @param client app or oauth client name + * @param operationTypes operation type + * @param authUser user id + * @param authIpAddress + * @param resourcePath + * @param dateTo + * @param dateFrom + * @param resourcePath + * @param firstResult + * @param maxResults + * @return + */ + @Path("admin-events") + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public List getEvents(@QueryParam("authRealm") String authRealm, @QueryParam("authClient") String authClient, + @QueryParam("authUser") String authUser, @QueryParam("authIpAddress") String authIpAddress, + @QueryParam("resourcePath") String resourcePath, @QueryParam("dateFrom") String dateFrom, + @QueryParam("dateTo") String dateTo, @QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResults) { + auth.init(RealmAuth.Resource.EVENTS).requireView(); + + EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); + AdminEventQuery query = null; + + if(realm.getName().equals(Config.getAdminRealm())) { + query = eventStore.createAdminQuery(); + if(authRealm != null) { + query.authRealm(authRealm); + } + } else { + query = eventStore.createAdminQuery().authRealm(realm.getId()); + } + + if (authClient != null) { + query.authClient(authClient); + } + + if (authUser != null) { + query.authUser(authUser); + } + + if (authIpAddress != null) { + query.authIpAddress(authIpAddress); + } + + if (resourcePath != null) { + query.resourcePath(resourcePath); + } + + List operationTypes = uriInfo.getQueryParameters().get("operationTypes"); + if (operationTypes != null) { + OperationType[] t = new OperationType[operationTypes.size()]; + for (int i = 0; i < t.length; i++) { + t[i] = OperationType.valueOf(operationTypes.get(i)); + } + query.operation(t); + } + + if(dateFrom != null) { + query.fromTime(dateFrom); + } + if(dateTo != null) { + query.toTime(dateTo); + } + + if (firstResult != null) { + query.firstResult(firstResult); + } + if (maxResults != null) { + query.maxResults(maxResults); + } + + return query.getResultList(); + } /** * Delete all events. @@ -432,6 +533,19 @@ public class RealmAdminResource { EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); eventStore.clear(realm.getId()); } + + /** + * Delete all admin events. + * + */ + @Path("admin-events") + @DELETE + public void clearAdminEvents() { + auth.init(RealmAuth.Resource.EVENTS).requireManage(); + + EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); + eventStore.clearAdmin(realm.getId()); + } @Path("testLDAPConnection") @GET @@ -446,6 +560,6 @@ public class RealmAdminResource { @Path("identity-provider") public IdentityProvidersResource getIdentityProviderResource() { - return new IdentityProvidersResource(realm, session, this.auth); + return new IdentityProvidersResource(realm, session, this.auth, adminEvent); } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java index c9fea3d09d..e50c25c834 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java @@ -6,6 +6,8 @@ import org.jboss.resteasy.plugins.providers.multipart.InputPart; import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.events.AdminEventBuilder; +import org.keycloak.events.admin.OperationType; import org.keycloak.models.AdminRoles; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; @@ -33,6 +35,7 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; + import java.io.IOException; import java.net.URI; import java.util.ArrayList; @@ -49,16 +52,18 @@ public class RealmsAdminResource { protected static final Logger logger = Logger.getLogger(RealmsAdminResource.class); protected AdminAuth auth; protected TokenManager tokenManager; + protected AdminEventBuilder adminEvent; @Context protected KeycloakSession session; - + @Context protected KeycloakApplication keycloak; - public RealmsAdminResource(AdminAuth auth, TokenManager tokenManager) { + public RealmsAdminResource(AdminAuth auth, TokenManager tokenManager, AdminEventBuilder adminEvent) { this.auth = auth; this.tokenManager = tokenManager; + this.adminEvent = adminEvent; } public static final CacheControl noCache = new CacheControl(); @@ -87,6 +92,7 @@ public class RealmsAdminResource { ClientModel adminApp = auth.getRealm().getClientByClientId(realmManager.getRealmAdminClientId(auth.getRealm())); addRealmRep(reps, auth.getRealm(), adminApp); } + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); logger.debug(("getRealms()")); return reps; } @@ -128,6 +134,8 @@ public class RealmsAdminResource { URI location = AdminRoot.realmsUrl(uriInfo).path(realm.getName()).build(); logger.debugv("imported realm success, sending back: {0}", location.toString()); + + adminEvent.operation(OperationType.CREATE).resourcePath(location.toString()).representation(rep).success(); return Response.created(location).build(); } catch (ModelDuplicateException e) { @@ -158,10 +166,11 @@ public class RealmsAdminResource { Map> uploadForm = input.getFormDataMap(); List inputParts = uploadForm.get("file"); - + RealmRepresentation rep = null; + for (InputPart inputPart : inputParts) { // inputPart.getBody doesn't work as content-type is wrong, and inputPart.setMediaType is not supported on AS7 (RestEasy 2.3.2.Final) - RealmRepresentation rep = JsonSerialization.readValue(inputPart.getBodyAsString(), RealmRepresentation.class); + rep = JsonSerialization.readValue(inputPart.getBodyAsString(), RealmRepresentation.class); RealmModel realm; try { realm = realmManager.importRealm(rep); @@ -170,13 +179,15 @@ public class RealmsAdminResource { } grantPermissionsToRealmCreator(realm); - + + URI location = null; if (inputParts.size() == 1) { - URI location = AdminRoot.realmsUrl(uriInfo).path(realm.getName()).build(); + location = AdminRoot.realmsUrl(uriInfo).path(realm.getName()).build(); + adminEvent.operation(OperationType.CREATE).resourcePath(location.toString()).representation(rep).success(); return Response.created(location).build(); } } - + return Response.noContent().build(); } @@ -219,7 +230,7 @@ public class RealmsAdminResource { realmAuth = new RealmAuth(auth, realm.getClientByClientId(realmManager.getRealmAdminClientId(auth.getRealm()))); } - RealmAdminResource adminResource = new RealmAdminResource(realmAuth, realm, tokenManager); + RealmAdminResource adminResource = new RealmAdminResource(realmAuth, realm, tokenManager, adminEvent); ResteasyProviderFactory.getInstance().injectProperties(adminResource); //resourceContext.initResource(adminResource); return adminResource; diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RoleByIdResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RoleByIdResource.java index 67d8c12e26..b54d44f198 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RoleByIdResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RoleByIdResource.java @@ -3,6 +3,8 @@ package org.keycloak.services.resources.admin; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.NotFoundException; +import org.keycloak.events.AdminEventBuilder; +import org.keycloak.events.admin.OperationType; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -20,6 +22,8 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; + import java.util.List; import java.util.Set; @@ -33,15 +37,17 @@ public class RoleByIdResource extends RoleResource { protected static final Logger logger = Logger.getLogger(RoleByIdResource.class); private final RealmModel realm; private final RealmAuth auth; + private AdminEventBuilder adminEvent; @Context protected KeycloakSession session; - public RoleByIdResource(RealmModel realm, RealmAuth auth) { + public RoleByIdResource(RealmModel realm, RealmAuth auth, AdminEventBuilder adminEvent) { super(realm); this.realm = realm; this.auth = auth; + this.adminEvent = adminEvent; } /** @@ -57,6 +63,8 @@ public class RoleByIdResource extends RoleResource { public RoleRepresentation getRole(final @PathParam("role-id") String id) { RoleModel roleModel = getRoleModel(id); auth.requireView(); + + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); return getRole(roleModel); } @@ -76,6 +84,8 @@ public class RoleByIdResource extends RoleResource { r = RealmAuth.Resource.USER; } auth.init(r); + + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); return roleModel; } @@ -92,6 +102,7 @@ public class RoleByIdResource extends RoleResource { RoleModel role = getRoleModel(id); auth.requireManage(); deleteRole(role); + adminEvent.operation(OperationType.DELETE).resourcePath(session.getContext().getUri().getPath()).success(); } /** @@ -107,6 +118,7 @@ public class RoleByIdResource extends RoleResource { RoleModel role = getRoleModel(id); auth.requireManage(); updateRole(rep, role); + adminEvent.operation(OperationType.UPDATE).resourcePath(session.getContext().getUri().getPath()).representation(rep).success(); } /** @@ -122,6 +134,8 @@ public class RoleByIdResource extends RoleResource { RoleModel role = getRoleModel(id); auth.requireManage(); addComposites(roles, role); + adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri().getPath()).representation(roles).success(); + } /** @@ -139,6 +153,7 @@ public class RoleByIdResource extends RoleResource { if (logger.isDebugEnabled()) logger.debug("*** getRoleComposites: '" + id + "'"); RoleModel role = getRoleModel(id); auth.requireView(); + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); return getRoleComposites(role); } @@ -155,6 +170,7 @@ public class RoleByIdResource extends RoleResource { public Set getRealmRoleComposites(final @PathParam("role-id") String id) { RoleModel role = getRoleModel(id); auth.requireView(); + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); return getRealmRoleComposites(role); } @@ -178,6 +194,7 @@ public class RoleByIdResource extends RoleResource { throw new NotFoundException("Could not find client: " + appName); } + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); return getClientRoleComposites(app, role); } @@ -201,6 +218,7 @@ public class RoleByIdResource extends RoleResource { throw new NotFoundException("Could not find client: " + appId); } + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); return getClientRoleComposites(app, role); } @@ -217,6 +235,7 @@ public class RoleByIdResource extends RoleResource { RoleModel role = getRoleModel(id); auth.requireManage(); deleteComposites(roles, role); + adminEvent.operation(OperationType.DELETE).resourcePath(session.getContext().getUri().getPath()).success(); } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java index fa0064fca7..43a41f6421 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java @@ -2,7 +2,10 @@ package org.keycloak.services.resources.admin; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.NotFoundException; +import org.keycloak.events.AdminEventBuilder; +import org.keycloak.events.admin.OperationType; import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleContainerModel; @@ -23,6 +26,7 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; + import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -35,12 +39,14 @@ public class RoleContainerResource extends RoleResource { private final RealmModel realm; private final RealmAuth auth; protected RoleContainerModel roleContainer; + private AdminEventBuilder adminEvent; - public RoleContainerResource(RealmModel realm, RealmAuth auth, RoleContainerModel roleContainer) { + public RoleContainerResource(RealmModel realm, RealmAuth auth, RoleContainerModel roleContainer, AdminEventBuilder adminEvent) { super(realm); this.realm = realm; this.auth = auth; this.roleContainer = roleContainer; + this.adminEvent = adminEvent; } /** @@ -51,7 +57,7 @@ public class RoleContainerResource extends RoleResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public List getRoles() { + public List getRoles(@Context final UriInfo uriInfo) { auth.requireAny(); Set roleModels = roleContainer.getRoles(); @@ -59,6 +65,7 @@ public class RoleContainerResource extends RoleResource { for (RoleModel roleModel : roleModels) { roles.add(ModelToRepresentation.toRepresentation(roleModel)); } + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return roles; } @@ -77,6 +84,11 @@ public class RoleContainerResource extends RoleResource { try { RoleModel role = roleContainer.addRole(rep.getName()); role.setDescription(rep.getDescription()); + + adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo.getAbsolutePathBuilder() + .path(role.getName()).build().toString().substring(uriInfo.getBaseUri().toString().length())) + .representation(rep).success(); + return Response.created(uriInfo.getAbsolutePathBuilder().path(role.getName()).build()).build(); } catch (ModelDuplicateException e) { return ErrorResponse.exists("Role with name " + rep.getName() + " already exists"); @@ -93,7 +105,7 @@ public class RoleContainerResource extends RoleResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public RoleRepresentation getRole(final @PathParam("role-name") String roleName) { + public RoleRepresentation getRole(@Context final UriInfo uriInfo, final @PathParam("role-name") String roleName) { auth.requireView(); RoleModel roleModel = roleContainer.getRole(roleName); @@ -101,6 +113,8 @@ public class RoleContainerResource extends RoleResource { throw new NotFoundException("Could not find role: " + roleName); } + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); + return getRole(roleModel); } @@ -112,15 +126,18 @@ public class RoleContainerResource extends RoleResource { @Path("{role-name}") @DELETE @NoCache - public void deleteRole(final @PathParam("role-name") String roleName) { + public void deleteRole(@Context final UriInfo uriInfo, final @PathParam("role-name") String roleName) { auth.requireManage(); - RoleRepresentation rep = getRole(roleName); + RoleRepresentation rep = getRole(uriInfo, roleName); RoleModel role = roleContainer.getRole(roleName); if (role == null) { throw new NotFoundException("Could not find role: " + roleName); } deleteRole(role); + + adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo.getPath()).success(); + } /** @@ -133,7 +150,7 @@ public class RoleContainerResource extends RoleResource { @Path("{role-name}") @PUT @Consumes(MediaType.APPLICATION_JSON) - public Response updateRole(final @PathParam("role-name") String roleName, final RoleRepresentation rep) { + public Response updateRole(@Context final UriInfo uriInfo, final @PathParam("role-name") String roleName, final RoleRepresentation rep) { auth.requireManage(); RoleModel role = roleContainer.getRole(roleName); @@ -142,6 +159,9 @@ public class RoleContainerResource extends RoleResource { } try { updateRole(rep, role); + + adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo.getPath()).representation(rep).success(); + return Response.noContent().build(); } catch (ModelDuplicateException e) { return ErrorResponse.exists("Role with name " + rep.getName() + " already exists"); @@ -157,7 +177,7 @@ public class RoleContainerResource extends RoleResource { @Path("{role-name}/composites") @POST @Consumes(MediaType.APPLICATION_JSON) - public void addComposites(final @PathParam("role-name") String roleName, List roles) { + public void addComposites(@Context final UriInfo uriInfo, final @PathParam("role-name") String roleName, List roles) { auth.requireManage(); RoleModel role = roleContainer.getRole(roleName); @@ -165,6 +185,8 @@ public class RoleContainerResource extends RoleResource { throw new NotFoundException("Could not find role: " + roleName); } addComposites(roles, role); + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).representation(roles).success(); + } /** @@ -177,13 +199,14 @@ public class RoleContainerResource extends RoleResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public Set getRoleComposites(final @PathParam("role-name") String roleName) { + public Set getRoleComposites(@Context final UriInfo uriInfo, final @PathParam("role-name") String roleName) { auth.requireManage(); RoleModel role = roleContainer.getRole(roleName); if (role == null) { throw new NotFoundException("Could not find role: " + roleName); } + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return getRoleComposites(role); } @@ -197,13 +220,14 @@ public class RoleContainerResource extends RoleResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public Set getRealmRoleComposites(final @PathParam("role-name") String roleName) { + public Set getRealmRoleComposites(@Context final UriInfo uriInfo, final @PathParam("role-name") String roleName) { auth.requireManage(); RoleModel role = roleContainer.getRole(roleName); if (role == null) { throw new NotFoundException("Could not find role: " + roleName); } + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return getRealmRoleComposites(role); } @@ -218,7 +242,8 @@ public class RoleContainerResource extends RoleResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public Set getClientRoleComposites(final @PathParam("role-name") String roleName, + public Set getClientRoleComposites(@Context final UriInfo uriInfo, + final @PathParam("role-name") String roleName, final @PathParam("clientId") String clientId) { auth.requireManage(); @@ -231,6 +256,7 @@ public class RoleContainerResource extends RoleResource { throw new NotFoundException("Could not find client: " + clientId); } + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return getClientRoleComposites(app, role); } @@ -246,7 +272,8 @@ public class RoleContainerResource extends RoleResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public Set getClientByIdRoleComposites(final @PathParam("role-name") String roleName, + public Set getClientByIdRoleComposites(@Context final UriInfo uriInfo, + final @PathParam("role-name") String roleName, final @PathParam("id") String id) { auth.requireManage(); @@ -259,6 +286,7 @@ public class RoleContainerResource extends RoleResource { throw new NotFoundException("Could not find client: " + id); } + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return getClientRoleComposites(client, role); } @@ -272,7 +300,9 @@ public class RoleContainerResource extends RoleResource { @Path("{role-name}/composites") @DELETE @Consumes(MediaType.APPLICATION_JSON) - public void deleteComposites(final @PathParam("role-name") String roleName, List roles) { + public void deleteComposites(@Context final UriInfo uriInfo, + final @PathParam("role-name") String roleName, + List roles) { auth.requireManage(); RoleModel role = roleContainer.getRole(roleName); @@ -280,6 +310,7 @@ public class RoleContainerResource extends RoleResource { throw new NotFoundException("Could not find role: " + roleName); } deleteComposites(roles, role); + adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo.getPath()).success(); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedClientResource.java index 01fe1d67ec..2825dac99d 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedClientResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedClientResource.java @@ -2,6 +2,8 @@ package org.keycloak.services.resources.admin; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.NotFoundException; +import org.keycloak.events.AdminEventBuilder; +import org.keycloak.events.admin.OperationType; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -15,7 +17,10 @@ import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; + import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -30,13 +35,15 @@ public class ScopeMappedClientResource { protected ClientModel client; protected KeycloakSession session; protected ClientModel scopedClient; - - public ScopeMappedClientResource(RealmModel realm, RealmAuth auth, ClientModel client, KeycloakSession session, ClientModel scopedClient) { + protected AdminEventBuilder adminEvent; + + public ScopeMappedClientResource(RealmModel realm, RealmAuth auth, ClientModel client, KeycloakSession session, ClientModel scopedClient, AdminEventBuilder adminEvent) { this.realm = realm; this.auth = auth; this.client = client; this.session = session; this.scopedClient = scopedClient; + this.adminEvent = adminEvent; } /** @@ -55,6 +62,7 @@ public class ScopeMappedClientResource { for (RoleModel roleModel : mappings) { mapRep.add(ModelToRepresentation.toRepresentation(roleModel)); } + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); return mapRep; } @@ -71,6 +79,7 @@ public class ScopeMappedClientResource { auth.requireView(); Set roles = scopedClient.getRoles(); + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); return ScopeMappedResource.getAvailable(client, roles); } @@ -87,6 +96,7 @@ public class ScopeMappedClientResource { auth.requireView(); Set roles = scopedClient.getRoles(); + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); return ScopeMappedResource.getComposite(client, roles); } @@ -107,6 +117,7 @@ public class ScopeMappedClientResource { } client.addScopeMapping(roleModel); } + adminEvent.operation(OperationType.CREATE).resourcePath(session.getContext().getUri().getPath()).representation(roles).success(); } @@ -135,5 +146,6 @@ public class ScopeMappedClientResource { client.deleteScopeMapping(roleModel); } } + adminEvent.operation(OperationType.DELETE).resourcePath(session.getContext().getUri().getPath()).success(); } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedResource.java index 8d4e005710..16b87440b5 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedResource.java @@ -2,6 +2,8 @@ package org.keycloak.services.resources.admin; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.NotFoundException; +import org.keycloak.events.AdminEventBuilder; +import org.keycloak.events.admin.OperationType; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -18,7 +20,10 @@ import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -36,12 +41,14 @@ public class ScopeMappedResource { private RealmAuth auth; protected ClientModel client; protected KeycloakSession session; + protected AdminEventBuilder adminEvent; - public ScopeMappedResource(RealmModel realm, RealmAuth auth, ClientModel client, KeycloakSession session) { + public ScopeMappedResource(RealmModel realm, RealmAuth auth, ClientModel client, KeycloakSession session, AdminEventBuilder adminEvent) { this.realm = realm; this.auth = auth; this.client = client; this.session = session; + this.adminEvent = adminEvent; } /** @@ -84,6 +91,7 @@ public class ScopeMappedResource { } } } + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); return all; } @@ -104,6 +112,7 @@ public class ScopeMappedResource { for (RoleModel roleModel : realmMappings) { realmMappingsRep.add(ModelToRepresentation.toRepresentation(roleModel)); } + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); return realmMappingsRep; } @@ -120,6 +129,7 @@ public class ScopeMappedResource { auth.requireView(); Set roles = realm.getRoles(); + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); return getAvailable(client, roles); } @@ -147,6 +157,7 @@ public class ScopeMappedResource { auth.requireView(); Set roles = realm.getRoles(); + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); return getComposite(client, roles); } @@ -176,7 +187,7 @@ public class ScopeMappedResource { } client.addScopeMapping(roleModel); } - + adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri().getPath()).representation(roles).success(); } @@ -206,6 +217,8 @@ public class ScopeMappedResource { client.deleteScopeMapping(roleModel); } } + adminEvent.operation(OperationType.DELETE).resourcePath(session.getContext().getUri().getPath()).success(); + } @Path("clients/{clientId}") @@ -215,8 +228,8 @@ public class ScopeMappedResource { if (app == null) { throw new NotFoundException("Role not found"); } - - return new ScopeMappedClientResource(realm, auth, client, session, app); + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); + return new ScopeMappedClientResource(realm, auth, client, session, app, adminEvent); } @Path("clients-by-id/{id}") @@ -226,7 +239,7 @@ public class ScopeMappedResource { if (app == null) { throw new NotFoundException("Client not found"); } - - return new ScopeMappedClientResource(realm, auth, client, session, app); + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); + return new ScopeMappedClientResource(realm, auth, client, session, app, adminEvent); } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java index f535b191ba..4e0d8f9078 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java @@ -5,6 +5,7 @@ import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProviderFactory; import org.keycloak.events.EventListenerProvider; import org.keycloak.events.EventType; +import org.keycloak.events.admin.OperationType; import org.keycloak.exportimport.ClientImporter; import org.keycloak.exportimport.ClientImporterFactory; import org.keycloak.freemarker.Theme; @@ -39,6 +40,8 @@ import java.util.Set; */ public class ServerInfoAdminResource { + private static final Map> ENUMS = createEnumsMap(EventType.class, OperationType.class); + @Context private KeycloakSession session; @@ -61,7 +64,7 @@ public class ServerInfoAdminResource { setProviders(info); setProtocolMapperTypes(info); setBuiltinProtocolMappers(info); - setEventTypes(info); + info.setEnums(ENUMS); return info; } @@ -181,15 +184,6 @@ public class ServerInfoAdminResource { } } - private void setEventTypes(ServerInfoRepresentation info) { - List eventTypes = new LinkedList<>(); - for (EventType t : EventType.values()) { - eventTypes.add(t.name()); - } - Collections.sort(eventTypes); - info.setEventTypes(eventTypes); - } - public static class ServerInfoRepresentation { private String version; @@ -209,7 +203,7 @@ public class ServerInfoAdminResource { private Map> protocolMapperTypes; private Map> builtinProtocolMappers; - private List eventTypes; + private Map> enums; public ServerInfoRepresentation() { } @@ -262,13 +256,30 @@ public class ServerInfoAdminResource { this.builtinProtocolMappers = builtinProtocolMappers; } - public List getEventTypes() { - return eventTypes; + public Map> getEnums() { + return enums; } - public void setEventTypes(List eventTypes) { - this.eventTypes = eventTypes; + public void setEnums(Map> enums) { + this.enums = enums; } } + private static Map> createEnumsMap(Class... enums) { + Map> m = new HashMap<>(); + for (Class e : enums) { + String n = e.getSimpleName(); + n = Character.toLowerCase(n.charAt(0)) + n.substring(1); + + List l = new LinkedList<>(); + for (Object c : e.getEnumConstants()) { + l.add(c.toString()); + } + Collections.sort(l); + + m.put(n, l); + } + return m; + } + } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserClientRoleMappingsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserClientRoleMappingsResource.java index e838333c61..39e180a68d 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserClientRoleMappingsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserClientRoleMappingsResource.java @@ -3,7 +3,10 @@ package org.keycloak.services.resources.admin; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.NotFoundException; +import org.keycloak.events.AdminEventBuilder; +import org.keycloak.events.admin.OperationType; import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; @@ -16,7 +19,10 @@ import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; + import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -33,12 +39,18 @@ public class UserClientRoleMappingsResource { protected RealmAuth auth; protected UserModel user; protected ClientModel client; + protected AdminEventBuilder adminEvent; + + @Context + protected KeycloakSession session; + - public UserClientRoleMappingsResource(RealmModel realm, RealmAuth auth, UserModel user, ClientModel client) { + public UserClientRoleMappingsResource(RealmModel realm, RealmAuth auth, UserModel user, ClientModel client, AdminEventBuilder adminEvent) { this.realm = realm; this.auth = auth; this.user = user; this.client = client; + this.adminEvent = adminEvent; } /** @@ -57,6 +69,7 @@ public class UserClientRoleMappingsResource { for (RoleModel roleModel : mappings) { mapRep.add(ModelToRepresentation.toRepresentation(roleModel)); } + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); return mapRep; } @@ -77,6 +90,7 @@ public class UserClientRoleMappingsResource { for (RoleModel roleModel : roles) { if (user.hasRole(roleModel)) mapRep.add(ModelToRepresentation.toRepresentation(roleModel)); } + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); return mapRep; } @@ -93,6 +107,7 @@ public class UserClientRoleMappingsResource { auth.requireView(); Set available = client.getRoles(); + adminEvent.operation(OperationType.VIEW).resourcePath(session.getContext().getUri().getPath()).success(); return getAvailableRoles(user, available); } @@ -127,6 +142,7 @@ public class UserClientRoleMappingsResource { } user.grantRole(roleModel); } + adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri().getPath()).representation(roles).success(); } @@ -159,5 +175,6 @@ public class UserClientRoleMappingsResource { user.deleteRoleMapping(roleModel); } } + adminEvent.operation(OperationType.DELETE).resourcePath(session.getContext().getUri().getPath()).success(); } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationResource.java index 188cb30ae3..a1d6851a9f 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationResource.java @@ -4,6 +4,8 @@ import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.NotFoundException; import org.keycloak.constants.KerberosConstants; +import org.keycloak.events.AdminEventBuilder; +import org.keycloak.events.admin.OperationType; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredCredentialModel; @@ -31,6 +33,7 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; + import java.util.LinkedList; import java.util.List; @@ -46,6 +49,8 @@ public class UserFederationResource { protected RealmModel realm; protected RealmAuth auth; + + protected AdminEventBuilder adminEvent; @Context protected UriInfo uriInfo; @@ -53,10 +58,11 @@ public class UserFederationResource { @Context protected KeycloakSession session; - public UserFederationResource(RealmModel realm, RealmAuth auth) { + public UserFederationResource(RealmModel realm, RealmAuth auth, AdminEventBuilder adminEvent) { this.auth = auth; this.realm = realm; - + this.adminEvent = adminEvent; + auth.init(RealmAuth.Resource.USER); } @@ -78,6 +84,7 @@ public class UserFederationResource { rep.setOptions(((UserFederationProviderFactory)factory).getConfigurationOptions()); providers.add(rep); } + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return providers; } @@ -99,6 +106,9 @@ public class UserFederationResource { UserFederationProviderFactoryRepresentation rep = new UserFederationProviderFactoryRepresentation(); rep.setId(factory.getId()); rep.setOptions(((UserFederationProviderFactory)factory).getConfigurationOptions()); + + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); + return rep; } throw new NotFoundException("Could not find provider"); @@ -123,6 +133,11 @@ public class UserFederationResource { rep.getFullSyncPeriod(), rep.getChangedSyncPeriod(), rep.getLastSync()); new UsersSyncManager().refreshPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), model, realm.getId()); checkKerberosCredential(model); + + adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo.getAbsolutePathBuilder() + .path(model.getId()).build().toString().substring(uriInfo.getBaseUri().toString().length())) + .representation(rep).success(); + return Response.created(uriInfo.getAbsolutePathBuilder().path(model.getId()).build()).build(); } @@ -146,6 +161,9 @@ public class UserFederationResource { realm.updateUserFederationProvider(model); new UsersSyncManager().refreshPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), model, realm.getId()); checkKerberosCredential(model); + + adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo.getPath()).representation(rep).success(); + } /** @@ -161,10 +179,10 @@ public class UserFederationResource { auth.requireView(); for (UserFederationProviderModel model : realm.getUserFederationProviders()) { if (model.getId().equals(id)) { + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return ModelToRepresentation.toRepresentation(model); } } - throw new NotFoundException("could not find provider"); } @@ -182,6 +200,9 @@ public class UserFederationResource { UserFederationProviderModel model = new UserFederationProviderModel(id, null, null, -1, null, -1, -1, 0); realm.removeUserFederationProvider(model); new UsersSyncManager().removePeriodicSyncForProvider(session.getProvider(TimerProvider.class), model); + + adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo.getPath()).success(); + } @@ -201,6 +222,7 @@ public class UserFederationResource { UserFederationProviderRepresentation rep = ModelToRepresentation.toRepresentation(model); reps.add(rep); } + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return reps; } @@ -224,6 +246,7 @@ public class UserFederationResource { } else if ("triggerChangedUsersSync".equals(action)) { syncManager.syncChangedUsers(session.getKeycloakSessionFactory(), realm.getId(), model); } + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).success(); return Response.noContent().build(); } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index ee9dc90dc0..291387e4b7 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -7,6 +7,8 @@ import org.jboss.resteasy.spi.NotFoundException; import org.keycloak.ClientConnection; import org.keycloak.email.EmailException; import org.keycloak.email.EmailProvider; +import org.keycloak.events.AdminEventBuilder; +import org.keycloak.events.admin.OperationType; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.Constants; @@ -56,6 +58,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; + import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -78,6 +81,8 @@ public class UsersResource { private RealmAuth auth; + private AdminEventBuilder adminEvent; + @Context protected ClientConnection clientConnection; @@ -90,9 +95,10 @@ public class UsersResource { @Context protected HttpHeaders headers; - public UsersResource(RealmModel realm, RealmAuth auth, TokenManager tokenManager) { + public UsersResource(RealmModel realm, RealmAuth auth, TokenManager tokenManager, AdminEventBuilder adminEvent) { this.auth = auth; this.realm = realm; + this.adminEvent = adminEvent; auth.init(RealmAuth.Resource.USER); } @@ -116,11 +122,11 @@ public class UsersResource { throw new NotFoundException("User not found"); } updateUserFromRep(user, rep); + adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo.getPath()).representation(rep).success(); if (session.getTransaction().isActive()) { session.getTransaction().commit(); } - return Response.noContent().build(); } catch (ModelDuplicateException e) { return ErrorResponse.exists("User exists with same username or email"); @@ -152,11 +158,15 @@ public class UsersResource { try { UserModel user = session.users().addUser(realm, rep.getUsername()); updateUserFromRep(user, rep); - + + adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo.getAbsolutePathBuilder() + .path(user.getUsername()).build().toString().substring(uriInfo.getBaseUri().toString().length())) + .representation(rep).success(); + if (session.getTransaction().isActive()) { session.getTransaction().commit(); } - + return Response.created(uriInfo.getAbsolutePathBuilder().path(user.getUsername()).build()).build(); } catch (ModelDuplicateException e) { if (session.getTransaction().isActive()) { @@ -217,6 +227,8 @@ public class UsersResource { if (user == null) { throw new NotFoundException("User not found"); } + + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); UserRepresentation rep = ModelToRepresentation.toRepresentation(user); @@ -256,6 +268,7 @@ public class UsersResource { UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(session); reps.add(rep); } + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return reps; } @@ -287,6 +300,7 @@ public class UsersResource { } } } + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return result; } @@ -305,7 +319,7 @@ public class UsersResource { FederatedIdentityModel socialLink = new FederatedIdentityModel(provider, rep.getUserId(), rep.getUserName()); session.users().addFederatedIdentity(realm, user, socialLink); - + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).representation(rep).success(); return Response.noContent().build(); } @@ -321,6 +335,7 @@ public class UsersResource { if (!session.users().removeFederatedIdentity(realm, user, provider)) { throw new NotFoundException("Link not found"); } + adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo.getPath()).success(); } /** @@ -347,6 +362,7 @@ public class UsersResource { UserConsentRepresentation rep = ModelToRepresentation.toRepresentation(consent); result.add(rep); } + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return result; } @@ -374,6 +390,7 @@ public class UsersResource { } else { throw new NotFoundException("Consent not found for user " + username + " and client " + clientId); } + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).success(); } /** @@ -395,6 +412,7 @@ public class UsersResource { for (UserSessionModel userSession : userSessions) { AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, true); } + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).success(); } /** @@ -416,6 +434,7 @@ public class UsersResource { boolean removed = new UserManager(session).removeUser(realm, user); if (removed) { + adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo.getPath()).success(); return Response.noContent().build(); } else { return ErrorResponse.error("User couldn't be deleted", Response.Status.BAD_REQUEST); @@ -524,6 +543,7 @@ public class UsersResource { } } } + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return all; } @@ -550,6 +570,7 @@ public class UsersResource { for (RoleModel roleModel : realmMappings) { realmMappingsRep.add(ModelToRepresentation.toRepresentation(roleModel)); } + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return realmMappingsRep; } @@ -578,6 +599,7 @@ public class UsersResource { realmMappingsRep.add(ModelToRepresentation.toRepresentation(roleModel)); } } + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return realmMappingsRep; } @@ -600,6 +622,7 @@ public class UsersResource { } Set available = realm.getRoles(); + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); return UserClientRoleMappingsResource.getAvailableRoles(user, available); } @@ -628,7 +651,8 @@ public class UsersResource { } user.grantRole(roleModel); } - + + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).representation(roles).success(); } @@ -665,6 +689,8 @@ public class UsersResource { user.deleteRoleMapping(roleModel); } } + + adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo.getPath()).success(); } @Path("{username}/role-mappings/clients/{clientId}") @@ -679,8 +705,8 @@ public class UsersResource { if (client == null) { throw new NotFoundException("Client not found"); } - - return new UserClientRoleMappingsResource(realm, auth, user, client); + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); + return new UserClientRoleMappingsResource(realm, auth, user, client, adminEvent); } @Path("{username}/role-mappings/clients-by-id/{id}") @@ -695,8 +721,9 @@ public class UsersResource { if (client == null) { throw new NotFoundException("Client not found"); } - - return new UserClientRoleMappingsResource(realm, auth, user, client); + + adminEvent.operation(OperationType.VIEW).resourcePath(uriInfo.getPath()).success(); + return new UserClientRoleMappingsResource(realm, auth, user, client, adminEvent); } /** @@ -729,6 +756,8 @@ public class UsersResource { throw new BadRequestException("Can't reset password as account is read only"); } if (pass.isTemporary()) user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); + + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).success(); } /** @@ -748,6 +777,7 @@ public class UsersResource { } user.setTotp(false); + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).success(); } /** @@ -823,6 +853,9 @@ public class UsersResource { this.session.getProvider(EmailProvider.class).setRealm(realm).setUser(user).sendPasswordReset(link, expiration); //audit.user(user).detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, accessCode.getCodeId()).success(); + + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo.getPath()).success(); + return Response.ok().build(); } catch (EmailException e) { logger.error("Failed to send password reset email", e); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java index 13c181bfac..ce805271c6 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java @@ -8,6 +8,7 @@ import org.junit.Assert; import org.junit.rules.TestRule; import org.junit.runners.model.Statement; import org.keycloak.Config; +import org.keycloak.events.admin.AdminEvent; import org.keycloak.events.Details; import org.keycloak.events.Event; import org.keycloak.events.EventListenerProvider; @@ -195,6 +196,12 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory { @Override public void close() { } + + @Override + public void onEvent(AdminEvent event, boolean includeRepresentation) { + // TODO Auto-generated method stub + + } }; } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/events/AdminEventStoreProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/events/AdminEventStoreProviderTest.java new file mode 100644 index 0000000000..dacf7c5e3d --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/events/AdminEventStoreProviderTest.java @@ -0,0 +1,226 @@ +package org.keycloak.testsuite.events; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.events.EventStoreProvider; +import org.keycloak.events.admin.AdminEvent; +import org.keycloak.events.admin.AuthDetails; +import org.keycloak.events.admin.OperationType; +import org.keycloak.models.KeycloakSession; +import org.keycloak.testsuite.rule.KeycloakRule; + +/** + * @author Giriraj Sharma + */ +public class AdminEventStoreProviderTest { + + @ClassRule + public static KeycloakRule kc = new KeycloakRule(); + + private KeycloakSession session; + + private EventStoreProvider eventStore; + + @Before + public void before() { + session = kc.startSession(); + eventStore = session.getProvider(EventStoreProvider.class); + } + + @After + public void after() { + eventStore.clearAdmin(); + kc.stopSession(session, true); + } + + @Test + public void save() { + eventStore.onEvent(create(OperationType.VIEW, "realmId", "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + } + + @Test + public void query() { + long oldest = System.currentTimeMillis() - 30000; + long newest = System.currentTimeMillis() + 30000; + + eventStore.onEvent(create(OperationType.VIEW, "realmId", "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(newest, OperationType.ACTION, "realmId", "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(newest, OperationType.ACTION, "realmId", "clientId", "userId2", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(OperationType.VIEW, "realmId2", "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(oldest, OperationType.VIEW, "realmId", "clientId2", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(OperationType.VIEW, "realmId", "clientId", "userId2", "127.0.0.1", "/admin/realms/master", "error"), false); + + resetSession(); + + Assert.assertEquals(5, eventStore.createAdminQuery().authClient("clientId").getResultList().size()); + Assert.assertEquals(5, eventStore.createAdminQuery().authRealm("realmId").getResultList().size()); + Assert.assertEquals(4, eventStore.createAdminQuery().operation(OperationType.VIEW).getResultList().size()); + Assert.assertEquals(6, eventStore.createAdminQuery().operation(OperationType.VIEW, OperationType.ACTION).getResultList().size()); + Assert.assertEquals(4, eventStore.createAdminQuery().authUser("userId").getResultList().size()); + + Assert.assertEquals(1, eventStore.createAdminQuery().authUser("userId").operation(OperationType.ACTION).getResultList().size()); + + Assert.assertEquals(2, eventStore.createAdminQuery().maxResults(2).getResultList().size()); + Assert.assertEquals(1, eventStore.createAdminQuery().firstResult(5).getResultList().size()); + + Assert.assertEquals(newest, eventStore.createAdminQuery().maxResults(1).getResultList().get(0).getTime()); + Assert.assertEquals(oldest, eventStore.createAdminQuery().firstResult(5).maxResults(1).getResultList().get(0).getTime()); + + eventStore.clearAdmin("realmId"); + eventStore.clearAdmin("realmId2"); + + Assert.assertEquals(0, eventStore.createAdminQuery().getResultList().size()); + + String d1 = new String("2015-03-04"); + String d2 = new String("2015-03-05"); + String d3 = new String("2015-03-06"); + String d4 = new String("2015-03-07"); + + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); + Date date1 = null, date2 = null, date3 = null, date4 = null; + + try { + date1 = formatter.parse(d1); + date2 = formatter.parse(d2); + date3 = formatter.parse(d3); + date4 = formatter.parse(d4); + } catch (ParseException e) { + e.printStackTrace(); + } + + eventStore.onEvent(create(date1, OperationType.VIEW, "realmId", "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(date1, OperationType.VIEW, "realmId", "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(date2, OperationType.ACTION, "realmId", "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(date2, OperationType.ACTION, "realmId", "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(date3, OperationType.UPDATE, "realmId", "clientId", "userId2", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(date3, OperationType.DELETE, "realmId", "clientId", "userId2", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(date4, OperationType.CREATE, "realmId2", "clientId2", "userId2", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(date4, OperationType.CREATE, "realmId2", "clientId2", "userId2", "127.0.0.1", "/admin/realms/master", "error"), false); + + resetSession(); + + Assert.assertEquals(6, eventStore.createAdminQuery().authClient("clientId").getResultList().size()); + Assert.assertEquals(2, eventStore.createAdminQuery().authClient("clientId2").getResultList().size()); + + Assert.assertEquals(6, eventStore.createAdminQuery().authRealm("realmId").getResultList().size()); + Assert.assertEquals(2, eventStore.createAdminQuery().authRealm("realmId2").getResultList().size()); + + Assert.assertEquals(4, eventStore.createAdminQuery().authUser("userId").getResultList().size()); + Assert.assertEquals(4, eventStore.createAdminQuery().authUser("userId2").getResultList().size()); + + Assert.assertEquals(2, eventStore.createAdminQuery().operation(OperationType.VIEW).getResultList().size()); + Assert.assertEquals(2, eventStore.createAdminQuery().operation(OperationType.ACTION).getResultList().size()); + Assert.assertEquals(4, eventStore.createAdminQuery().operation(OperationType.VIEW, OperationType.ACTION).getResultList().size()); + Assert.assertEquals(1, eventStore.createAdminQuery().operation(OperationType.UPDATE).getResultList().size()); + Assert.assertEquals(1, eventStore.createAdminQuery().operation(OperationType.DELETE).getResultList().size()); + Assert.assertEquals(2, eventStore.createAdminQuery().operation(OperationType.CREATE).getResultList().size()); + + Assert.assertEquals(8, eventStore.createAdminQuery().fromTime("2015-03-04").getResultList().size()); + Assert.assertEquals(8, eventStore.createAdminQuery().toTime("2015-03-07").getResultList().size()); + + Assert.assertEquals(4, eventStore.createAdminQuery().fromTime("2015-03-06").getResultList().size()); + Assert.assertEquals(4, eventStore.createAdminQuery().toTime("2015-03-05").getResultList().size()); + + Assert.assertEquals(0, eventStore.createAdminQuery().fromTime("2015-03-08").getResultList().size()); + Assert.assertEquals(0, eventStore.createAdminQuery().toTime("2015-03-03").getResultList().size()); + + Assert.assertEquals(8, eventStore.createAdminQuery().fromTime("2015-03-04").toTime("2015-03-07").getResultList().size()); + Assert.assertEquals(6, eventStore.createAdminQuery().fromTime("2015-03-05").toTime("2015-03-07").getResultList().size()); + Assert.assertEquals(4, eventStore.createAdminQuery().fromTime("2015-03-04").toTime("2015-03-05").getResultList().size()); + Assert.assertEquals(4, eventStore.createAdminQuery().fromTime("2015-03-06").toTime("2015-03-07").getResultList().size()); + + Assert.assertEquals(0, eventStore.createAdminQuery().fromTime("2015-03-01").toTime("2015-03-03").getResultList().size()); + Assert.assertEquals(0, eventStore.createAdminQuery().fromTime("2015-03-08").toTime("2015-03-10").getResultList().size()); + + } + + @Test + public void queryResourcePath() { + long oldest = System.currentTimeMillis() - 30000; + long newest = System.currentTimeMillis() + 30000; + + eventStore.onEvent(create(OperationType.VIEW, "realmId", "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(newest, OperationType.ACTION, "realmId", "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(newest, OperationType.ACTION, "realmId", "clientId", "userId2", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(OperationType.VIEW, "realmId2", "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(oldest, OperationType.VIEW, "realmId", "clientId2", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(OperationType.VIEW, "realmId", "clientId", "userId2", "127.0.0.1", "/admin/realms/master", "error"), false); + + resetSession(); + + Assert.assertEquals(6, eventStore.createAdminQuery().resourcePath("/admin").getResultList().size()); + Assert.assertEquals(6, eventStore.createAdminQuery().resourcePath("/realms").getResultList().size()); + Assert.assertEquals(6, eventStore.createAdminQuery().resourcePath("/master").getResultList().size()); + Assert.assertEquals(6, eventStore.createAdminQuery().resourcePath("/admin/realms").getResultList().size()); + Assert.assertEquals(6, eventStore.createAdminQuery().resourcePath("/realms/master").getResultList().size()); + Assert.assertEquals(6, eventStore.createAdminQuery().resourcePath("/admin/realms/master").getResultList().size()); + } + + @Test + public void clear() { + eventStore.onEvent(create(System.currentTimeMillis() - 30000, OperationType.VIEW, "realmId", "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(System.currentTimeMillis() - 20000, OperationType.VIEW, "realmId", "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(System.currentTimeMillis(), OperationType.VIEW, "realmId", "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(System.currentTimeMillis(), OperationType.VIEW, "realmId", "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(System.currentTimeMillis() - 30000, OperationType.VIEW, "realmId2", "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + + resetSession(); + + eventStore.clearAdmin("realmId"); + + Assert.assertEquals(1, eventStore.createAdminQuery().getResultList().size()); + } + + @Test + public void clearOld() { + eventStore.onEvent(create(System.currentTimeMillis() - 30000, OperationType.VIEW, "realmId", "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(System.currentTimeMillis() - 20000, OperationType.VIEW, "realmId", "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(System.currentTimeMillis(), OperationType.VIEW, "realmId", "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(System.currentTimeMillis(), OperationType.VIEW, "realmId", "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + eventStore.onEvent(create(System.currentTimeMillis() - 30000, OperationType.VIEW, "realmId2", "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); + + resetSession(); + + eventStore.clearAdmin("realmId", System.currentTimeMillis() - 10000); + + Assert.assertEquals(3, eventStore.createAdminQuery().getResultList().size()); + } + + private AdminEvent create(OperationType operation, String realmId, String clientId, String userId, String ipAddress, String resourcePath, String error) { + return create(System.currentTimeMillis(), operation, realmId, clientId, userId, ipAddress, resourcePath, error); + } + + private AdminEvent create(Date date, OperationType operation, String realmId, String clientId, String userId, String ipAddress, String resourcePath, String error) { + return create(date.getTime(), operation, realmId, clientId, userId, ipAddress, resourcePath, error); + } + + private AdminEvent create(long time, OperationType operation, String realmId, String clientId, String userId, String ipAddress, String resourcePath, String error) { + AdminEvent e = new AdminEvent(); + e.setTime(time); + e.setOperationType(operation); + AuthDetails authDetails = new AuthDetails(); + authDetails.setRealmId(realmId); + authDetails.setClientId(clientId); + authDetails.setUserId(userId); + authDetails.setIpAddress(ipAddress); + e.setAuthDetails(authDetails); + e.setResourcePath(resourcePath); + e.setError(error); + + return e; + } + + private void resetSession() { + kc.stopSession(session, true); + session = kc.startSession(); + eventStore = session.getProvider(EventStoreProvider.class); + } + +}