keycloak-memoizeit

Merge pull request #326 from stianst/master Clearing audit

4/8/2014 8:43:52 AM

Details

diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js
index dc54399..9bbea17 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js
@@ -1028,10 +1028,6 @@ module.controller('RealmAuditCtrl', function($scope, auditConfig, RealmAudit, Re
     $scope.auditConfig = auditConfig;
 
     $scope.auditConfig.expirationUnit = TimeUnit.autoUnit(auditConfig.auditExpiration);
-    if ($scope.auditConfig.expirationUnit) {
-        $scope.auditConfig.expirationUnit = 'Hours';
-    }
-
     $scope.auditConfig.auditExpiration = TimeUnit.toUnit(auditConfig.auditExpiration, $scope.auditConfig.expirationUnit);
     $scope.$watch('auditConfig.expirationUnit', function(to, from) {
         if ($scope.auditConfig.auditExpiration) {
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/services.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/services.js
index 81af19b..b3d5964 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/js/services.js
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/services.js
@@ -696,6 +696,10 @@ module.factory('TimeUnit', function() {
     var t = {};
 
     t.autoUnit = function(time) {
+        if (!time) {
+            return 'Hours';
+        }
+
         var unit = 'Seconds';
         if (time % 60 == 0) {
             unit = 'Minutes';
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-audit-config.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-audit-config.html
index befe2dd..3697e7e 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-audit-config.html
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-audit-config.html
@@ -42,6 +42,7 @@
                             </div>
                             <div class="col-sm-2 select-kc">
                                 <select name="expirationUnit" data-ng-model="auditConfig.expirationUnit" >
+                                    <option>Minutes</option>
                                     <option>Hours</option>
                                     <option>Days</option>
                                 </select>
diff --git a/model/api/src/main/java/org/keycloak/models/Config.java b/model/api/src/main/java/org/keycloak/models/Config.java
index 7851ea8..17ff238 100644
--- a/model/api/src/main/java/org/keycloak/models/Config.java
+++ b/model/api/src/main/java/org/keycloak/models/Config.java
@@ -1,6 +1,7 @@
 package org.keycloak.models;
 
 import java.io.File;
+import java.util.concurrent.TimeUnit;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -12,7 +13,10 @@ public class Config {
 
     public static final String MODEL_PROVIDER_KEY = "keycloak.model";
 
-    public static final String MODEL_AUDIT_KEY = "keycloak.audit";
+    public static final String AUDIT_PROVIDER_KEY = "keycloak.audit";
+    public static final String AUDIT_PROVIDER_DEFAULT = "jpa";
+    public static final String AUDIT_EXPIRATION_SCHEDULE_KEY = "keycloak.audit.expirationSchedule";
+    public static final String AUDIT_EXPIRATION_SCHEDULE_DEFAULT = String.valueOf(TimeUnit.MINUTES.toMillis(15));
 
     public static final String THEME_BASE_KEY = "keycloak.theme.base";
     public static final String THEME_BASE_DEFAULT = "base";
@@ -21,6 +25,9 @@ public class Config {
     public static final String THEME_DIR_KEY = "keycloak.theme.dir";
     public static final String JBOSS_SERVER_CONFIG_DIR_KEY = "jboss.server.config.dir";
 
+    public static final String TIMER_PROVIDER_KEY = "keycloak.timer";
+    public static final String TIMER_PROVIDER_DEFAULT = "basic";
+
     public static String getAdminRealm() {
         return System.getProperty(ADMIN_REALM_KEY, ADMIN_REALM_DEFAULT);
     }
@@ -30,13 +37,21 @@ public class Config {
     }
 
     public static String getAuditProvider() {
-        return System.getProperty(MODEL_PROVIDER_KEY, "jpa");
+        return System.getProperty(AUDIT_PROVIDER_KEY, AUDIT_PROVIDER_DEFAULT);
     }
 
     public static void setAuditProvider(String provider) {
         System.setProperty(MODEL_PROVIDER_KEY, provider);
     }
 
+    public static String getAuditExpirationSchedule() {
+        return System.getProperty(AUDIT_EXPIRATION_SCHEDULE_KEY, AUDIT_EXPIRATION_SCHEDULE_DEFAULT);
+    }
+
+    public static void setAuditExpirationSchedule(String schedule) {
+        System.setProperty(AUDIT_EXPIRATION_SCHEDULE_KEY, schedule);
+    }
+
     public static String getModelProvider() {
         return System.getProperty(MODEL_PROVIDER_KEY);
     }
@@ -45,6 +60,14 @@ public class Config {
         System.setProperty(MODEL_PROVIDER_KEY, provider);
     }
 
+    public static String getTimerProvider() {
+        return System.getProperty(TIMER_PROVIDER_KEY, TIMER_PROVIDER_DEFAULT);
+    }
+
+    public static void setTimerProvider(String provider) {
+        System.setProperty(TIMER_PROVIDER_KEY, provider);
+    }
+
     public static String getThemeDir() {
         String themeDir = System.getProperty(THEME_DIR_KEY);
         if (themeDir == null && System.getProperties().containsKey(JBOSS_SERVER_CONFIG_DIR_KEY)) {

pom.xml 1(+1 -0)

diff --git a/pom.xml b/pom.xml
index f6a7e08..9d07a30 100755
--- a/pom.xml
+++ b/pom.xml
@@ -98,6 +98,7 @@
         <module>testsuite</module>
         <module>server</module>
         <module>spi</module>
+        <module>timer</module>    
     </modules>
 
     <dependencyManagement>

server/pom.xml 12(+12 -0)

diff --git a/server/pom.xml b/server/pom.xml
index 9b33349..44c063e 100755
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -143,6 +143,18 @@
             <version>${project.version}</version>
         </dependency>
         <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-timer-api</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-timer-basic</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
             <version>4.1</version>

services/pom.xml 12(+12 -0)

diff --git a/services/pom.xml b/services/pom.xml
index 1c68b71..40be81e 100755
--- a/services/pom.xml
+++ b/services/pom.xml
@@ -71,6 +71,18 @@
         </dependency>
         <dependency>
             <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-timer-api</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-timer-basic</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
             <artifactId>keycloak-authentication-spi</artifactId>
             <version>${project.version}</version>
             <scope>provided</scope>
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 bed8867..1e5bb84 100755
--- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
@@ -170,7 +170,7 @@ public class RealmManager {
 
     public void updateRealmAudit(RealmAuditRepresentation rep, RealmModel realm) {
         realm.setAuditEnabled(rep.isAuditEnabled());
-        realm.setAuditExpiration(rep.getAuditExpiration());
+        realm.setAuditExpiration(rep.getAuditExpiration() != null ? rep.getAuditExpiration() : 0);
         if (rep.getAuditListeners() != null) {
             realm.setAuditListeners(new HashSet<String>(rep.getAuditListeners()));
         }
diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
index da644ac..865be1d 100755
--- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
+++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
@@ -7,11 +7,16 @@ import org.keycloak.audit.AuditListenerFactory;
 import org.keycloak.audit.AuditProvider;
 import org.keycloak.audit.AuditProviderFactory;
 import org.keycloak.models.Config;
+import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.models.ModelProvider;
+import org.keycloak.models.RealmModel;
+import org.keycloak.provider.ProviderFactory;
 import org.keycloak.provider.ProviderFactoryLoader;
 import org.keycloak.services.DefaultProviderSessionFactory;
 import org.keycloak.services.ProviderSessionFactory;
+import org.keycloak.timer.TimerProvider;
+import org.keycloak.timer.TimerProviderFactory;
 import org.keycloak.util.KeycloakRegistry;
 import org.keycloak.services.managers.ApplianceBootstrap;
 import org.keycloak.services.managers.SocialRequestManager;
@@ -24,6 +29,7 @@ import javax.ws.rs.core.Application;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.UriInfo;
 import java.net.URI;
+import java.util.Date;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -49,7 +55,9 @@ public class KeycloakApplication extends Application {
         context.setAttribute(KeycloakRegistry.class.getName(), registry);
         //classes.add(KeycloakSessionCleanupFilter.class);
 
-        context.setAttribute(ProviderSessionFactory.class.getName(), createProviderSessionFactory());
+        DefaultProviderSessionFactory providerSessionFactory = createProviderSessionFactory();
+
+        context.setAttribute(ProviderSessionFactory.class.getName(), providerSessionFactory);
 
         TokenManager tokenManager = new TokenManager();
         SocialRequestManager socialRequestManager = new SocialRequestManager();
@@ -62,6 +70,8 @@ public class KeycloakApplication extends Application {
         classes.add(ThemeResource.class);
 
         setupDefaultRealm(context.getContextPath());
+
+        setupScheduledTasks(providerSessionFactory, factory);
     }
 
     public String getContextPath() {
@@ -99,10 +109,45 @@ public class KeycloakApplication extends Application {
 
         factory.registerLoader(AuditProvider.class, ProviderFactoryLoader.create(AuditProviderFactory.class), Config.getAuditProvider());
         factory.registerLoader(AuditListener.class, ProviderFactoryLoader.create(AuditListenerFactory.class));
+        factory.registerLoader(TimerProvider.class, ProviderFactoryLoader.create(TimerProviderFactory.class), Config.getTimerProvider());
 
         return factory;
     }
 
+    public static void setupScheduledTasks(final ProviderSessionFactory providerSessionFactory, final KeycloakSessionFactory keycloakSessionFactory) {
+        ProviderFactory<TimerProvider> timerFactory = providerSessionFactory.getProviderFactory(TimerProvider.class);
+        if (timerFactory == null) {
+            log.error("Can't setup schedule tasks, no timer provider found");
+            return;
+        }
+        TimerProvider timer = timerFactory.create();
+
+        final ProviderFactory<AuditProvider> auditFactory = providerSessionFactory.getProviderFactory(AuditProvider.class);
+        if (auditFactory != null) {
+            timer.schedule(new Runnable() {
+                @Override
+                public void run() {
+                    KeycloakSession keycloakSession = keycloakSessionFactory.createSession();
+                    AuditProvider audit = providerSessionFactory.getProviderFactory(AuditProvider.class).create();
+                    try {
+                        for (RealmModel realm : keycloakSession.getRealms()) {
+                            if (realm.isAuditEnabled() && realm.getAuditExpiration() > 0) {
+                                long olderThan = System.currentTimeMillis() - realm.getAuditExpiration() * 1000;
+                                log.info("Expiring audit events for " + realm.getName() + " older than " + new Date(olderThan));
+                                audit.clear(realm.getId(), olderThan);
+                            }
+                        }
+                    } finally {
+                        keycloakSession.close();
+                        audit.close();
+                    }
+                }
+            }, Config.getAuditExpirationSchedule());
+        } else {
+            log.info("Not scheduling audit expiration, no audit provider found");
+        }
+    }
+
     public KeycloakSessionFactory getFactory() {
         return factory;
     }
diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml
index d4ef010..b5fd3c8 100755
--- a/testsuite/integration/pom.xml
+++ b/testsuite/integration/pom.xml
@@ -74,6 +74,16 @@
         </dependency>
         <dependency>
             <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-timer-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-timer-basic</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
             <artifactId>keycloak-js-adapter</artifactId>
             <version>${project.version}</version>
         </dependency>
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
index 5300cfa..663eb6a 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
@@ -22,8 +22,10 @@
 package org.keycloak.testsuite.account;
 
 import org.junit.After;
+import org.junit.AfterClass;
 import org.junit.Assert;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
@@ -31,6 +33,7 @@ import org.keycloak.audit.Details;
 import org.keycloak.audit.Event;
 import org.keycloak.audit.jpa.JpaAuditProviderFactory;
 import org.keycloak.models.ApplicationModel;
+import org.keycloak.models.Config;
 import org.keycloak.models.PasswordPolicy;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserCredentialModel;
@@ -40,6 +43,7 @@ import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.services.managers.RealmManager;
 import org.keycloak.testsuite.AssertEvents;
 import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.Retry;
 import org.keycloak.testsuite.pages.AccountLogPage;
 import org.keycloak.testsuite.pages.AccountPasswordPage;
 import org.keycloak.testsuite.pages.AccountTotpPage;
@@ -145,8 +149,6 @@ public class AccountTest {
                 appRealm.updateCredential(user, cred);
             }
         });
-
-        System.out.println(JpaAuditProviderFactory.class);
     }
 
     @Test
@@ -365,17 +367,23 @@ public class AccountTest {
 
             Assert.assertTrue(logPage.isCurrent());
 
-            List<List<String>> actual = logPage.getEvents();
-
-            Assert.assertEquals(e.size(), actual.size());
+            final int expectedEvents = e.size();
+            Retry.execute(new Runnable() {
+                @Override
+                public void run() {
+                    Assert.assertEquals(expectedEvents, logPage.getEvents().size());
+                }
+            }, 10, 500);
 
-            Iterator<List<String>> itr = actual.iterator();
+            Iterator<List<String>> itr = logPage.getEvents().iterator();
             for (Event event : e) {
                 List<String> a = itr.next();
                 Assert.assertEquals(event.getEvent().replace('_', ' '), a.get(1));
                 Assert.assertEquals(event.getIpAddress(), a.get(2));
                 Assert.assertEquals(event.getClientId(), a.get(3));
             }
+        } catch (InterruptedException e) {
+            e.printStackTrace();
         } finally {
             keycloakRule.configure(new KeycloakSetup() {
                 @Override
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/Retry.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/Retry.java
new file mode 100644
index 0000000..4e585ad
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/Retry.java
@@ -0,0 +1,24 @@
+package org.keycloak.testsuite;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class Retry {
+
+    public static void execute(Runnable runnable, int retry, long interval) throws InterruptedException {
+        while (true) {
+            try {
+                runnable.run();
+                return;
+            } catch (RuntimeException e) {
+                retry--;
+                if (retry > 0) {
+                   Thread.sleep(interval);
+                } else {
+                    throw e;
+                }
+            }
+        }
+    }
+
+}

timer/api/pom.xml 22(+22 -0)

diff --git a/timer/api/pom.xml b/timer/api/pom.xml
new file mode 100755
index 0000000..030f089
--- /dev/null
+++ b/timer/api/pom.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<project>
+    <parent>
+        <artifactId>keycloak-timer-parent</artifactId>
+        <groupId>org.keycloak</groupId>
+        <version>1.0-beta-1-SNAPSHOT</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>keycloak-timer-api</artifactId>
+    <name>Keycloak Timer API</name>
+    <description/>
+    <dependencies>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/timer/api/src/main/java/org/keycloak/timer/TimerProvider.java b/timer/api/src/main/java/org/keycloak/timer/TimerProvider.java
new file mode 100644
index 0000000..e701b5f
--- /dev/null
+++ b/timer/api/src/main/java/org/keycloak/timer/TimerProvider.java
@@ -0,0 +1,12 @@
+package org.keycloak.timer;
+
+import org.keycloak.provider.Provider;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface TimerProvider extends Provider {
+
+    public void schedule(Runnable runnable, String config);
+
+}
diff --git a/timer/api/src/main/java/org/keycloak/timer/TimerProviderFactory.java b/timer/api/src/main/java/org/keycloak/timer/TimerProviderFactory.java
new file mode 100644
index 0000000..5c73b8e
--- /dev/null
+++ b/timer/api/src/main/java/org/keycloak/timer/TimerProviderFactory.java
@@ -0,0 +1,9 @@
+package org.keycloak.timer;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface TimerProviderFactory extends ProviderFactory<TimerProvider> {
+}

timer/basic/pom.xml 23(+23 -0)

diff --git a/timer/basic/pom.xml b/timer/basic/pom.xml
new file mode 100755
index 0000000..8d5c5bf
--- /dev/null
+++ b/timer/basic/pom.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<project>
+    <parent>
+        <artifactId>keycloak-timer-parent</artifactId>
+        <groupId>org.keycloak</groupId>
+        <version>1.0-beta-1-SNAPSHOT</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>keycloak-timer-basic</artifactId>
+    <name>Keycloak Timer Basic Provider</name>
+    <description/>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-timer-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/timer/basic/src/main/java/org/keycloak/timer/basic/BasicTimerProvider.java b/timer/basic/src/main/java/org/keycloak/timer/basic/BasicTimerProvider.java
new file mode 100644
index 0000000..bda150b
--- /dev/null
+++ b/timer/basic/src/main/java/org/keycloak/timer/basic/BasicTimerProvider.java
@@ -0,0 +1,39 @@
+package org.keycloak.timer.basic;
+
+import org.keycloak.timer.TimerProvider;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class BasicTimerProvider implements TimerProvider {
+
+    private Timer timer;
+
+    public BasicTimerProvider(Timer timer) {
+
+        this.timer = timer;
+    }
+
+    @Override
+    public void schedule(final Runnable runnable, String config) {
+        long interval = Long.parseLong(config);
+
+        TimerTask task = new TimerTask() {
+            @Override
+            public void run() {
+                runnable.run();
+            }
+        };
+
+        timer.schedule(task, interval, interval);
+    }
+
+    @Override
+    public void close() {
+        // do nothing
+    }
+
+}
diff --git a/timer/basic/src/main/java/org/keycloak/timer/basic/BasicTimerProviderFactory.java b/timer/basic/src/main/java/org/keycloak/timer/basic/BasicTimerProviderFactory.java
new file mode 100644
index 0000000..accb42b
--- /dev/null
+++ b/timer/basic/src/main/java/org/keycloak/timer/basic/BasicTimerProviderFactory.java
@@ -0,0 +1,40 @@
+package org.keycloak.timer.basic;
+
+import org.keycloak.timer.TimerProvider;
+import org.keycloak.timer.TimerProviderFactory;
+
+import java.util.Timer;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class BasicTimerProviderFactory implements TimerProviderFactory {
+
+    private Timer timer;
+
+    @Override
+    public TimerProvider create() {
+        return new BasicTimerProvider(timer);
+    }
+
+    @Override
+    public void init() {
+        timer = new Timer();
+    }
+
+    @Override
+    public void close() {
+        timer.cancel();
+        timer = null;
+    }
+
+    @Override
+    public String getId() {
+        return "basic";
+    }
+
+    @Override
+    public boolean lazyLoad() {
+        return true;
+    }
+}
diff --git a/timer/basic/src/main/resources/META-INF/services/org.keycloak.timer.TimerProviderFactory b/timer/basic/src/main/resources/META-INF/services/org.keycloak.timer.TimerProviderFactory
new file mode 100644
index 0000000..94a4b8d
--- /dev/null
+++ b/timer/basic/src/main/resources/META-INF/services/org.keycloak.timer.TimerProviderFactory
@@ -0,0 +1 @@
+org.keycloak.timer.basic.BasicTimerProviderFactory
\ No newline at end of file

timer/pom.xml 22(+22 -0)

diff --git a/timer/pom.xml b/timer/pom.xml
new file mode 100755
index 0000000..dc65cad
--- /dev/null
+++ b/timer/pom.xml
@@ -0,0 +1,22 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <parent>
+        <artifactId>keycloak-parent</artifactId>
+        <groupId>org.keycloak</groupId>
+        <version>1.0-beta-1-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <name>Keycloak Timer Parent</name>
+    <description/>
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>org.keycloak</groupId>
+    <artifactId>keycloak-timer-parent</artifactId>
+    <packaging>pom</packaging>
+
+    <modules>
+        <module>api</module>
+        <module>basic</module>
+    </modules>
+</project>