Merge pull request #6 from myrle-krantz/develop

Fixing up authentication.  Not finished integration testing yet though.
diff --git a/component-test/src/main/java/io/mifos/rhythm/AbstractRhythmTest.java b/component-test/src/main/java/io/mifos/rhythm/AbstractRhythmTest.java
index 17862e5..dad54b1 100644
--- a/component-test/src/main/java/io/mifos/rhythm/AbstractRhythmTest.java
+++ b/component-test/src/main/java/io/mifos/rhythm/AbstractRhythmTest.java
@@ -40,7 +40,7 @@
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.SpyBean;
+import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.cloud.netflix.feign.EnableFeignClients;
 import org.springframework.cloud.netflix.ribbon.RibbonClient;
 import org.springframework.context.annotation.Bean;
@@ -52,8 +52,7 @@
 import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.time.temporal.ChronoUnit;
-
-import static org.mockito.Matchers.*;
+import java.util.Optional;
 
 /**
  * @author Myrle Krantz
@@ -109,7 +108,7 @@
   @Autowired
   EventRecorder eventRecorder;
 
-  @SpyBean
+  @MockBean
   BeatPublisherService beatPublisherServiceSpy;
 
   @Before
@@ -132,6 +131,7 @@
   }
 
   Beat createBeat(final String applicationName, final String beatIdentifier) throws InterruptedException {
+    final String tenantIdentifier = tenantDataStoreContext.getTenantName();
     final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
 
     final Beat beat = new Beat();
@@ -139,13 +139,15 @@
     beat.setAlignmentHour(now.getHour());
 
     final LocalDateTime expectedBeatTimestamp = getExpectedBeatTimestamp(now, beat.getAlignmentHour());
-    Mockito.doReturn(true).when(beatPublisherServiceSpy).publishBeat(Matchers.eq(beatIdentifier), Matchers.eq(tenantDataStoreContext.getTenantName()), Matchers.eq(applicationName),
-                    AdditionalMatchers.or(Matchers.eq(expectedBeatTimestamp), Matchers.eq(getNextTimeStamp(expectedBeatTimestamp))));
+    Mockito.doReturn(Optional.of("boop")).when(beatPublisherServiceSpy).requestPermissionForBeats(Matchers.eq(tenantIdentifier), Matchers.eq(applicationName));
+    Mockito.doReturn(true).when(beatPublisherServiceSpy).publishBeat(Matchers.eq(beatIdentifier), Matchers.eq(tenantIdentifier), Matchers.eq(applicationName),
+            AdditionalMatchers.or(Matchers.eq(expectedBeatTimestamp), Matchers.eq(getNextTimeStamp(expectedBeatTimestamp))));
 
     this.testSubject.createBeat(applicationName, beat);
 
     Assert.assertTrue(this.eventRecorder.wait(EventConstants.POST_BEAT, new BeatEvent(applicationName, beat.getIdentifier())));
 
+    Mockito.verify(beatPublisherServiceSpy, Mockito.timeout(2_000).times(1)).requestPermissionForBeats(tenantIdentifier, applicationName);
     Mockito.verify(beatPublisherServiceSpy, Mockito.timeout(2_000).times(1)).publishBeat(beatIdentifier, tenantDataStoreContext.getTenantName(), applicationName, expectedBeatTimestamp);
 
     return beat;
diff --git a/component-test/src/main/java/io/mifos/rhythm/TestBeats.java b/component-test/src/main/java/io/mifos/rhythm/TestBeats.java
index 352005b..9008f53 100644
--- a/component-test/src/main/java/io/mifos/rhythm/TestBeats.java
+++ b/component-test/src/main/java/io/mifos/rhythm/TestBeats.java
@@ -21,13 +21,13 @@
 import io.mifos.rhythm.api.v1.events.EventConstants;
 import org.junit.Assert;
 import org.junit.Test;
+import org.mockito.Matchers;
 import org.mockito.Mockito;
 
 import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.util.List;
-
-import static org.mockito.Matchers.*;
+import java.util.Optional;
 
 /**
  * @author Myrle Krantz
@@ -79,6 +79,7 @@
 
   @Test
   public void shouldRetryBeatPublishIfFirstAttemptFails() throws InterruptedException {
+    final String tenantIdentifier = tenantDataStoreContext.getTenantName();
     final String appName = "funnybusiness-v4";
     final String beatId = "bebopthedowop";
 
@@ -90,12 +91,13 @@
 
     final LocalDateTime expectedBeatTimestamp = getExpectedBeatTimestamp(now, beat.getAlignmentHour());
 
-    Mockito.when(beatPublisherServiceSpy.publishBeat(beatId, tenantDataStoreContext.getTenantName(), appName, expectedBeatTimestamp)).thenReturn(false, false, true);
+    Mockito.doReturn(Optional.of("boop")).when(beatPublisherServiceSpy).requestPermissionForBeats(Matchers.eq(tenantIdentifier), Matchers.eq(appName));
+    Mockito.when(beatPublisherServiceSpy.publishBeat(beatId, tenantIdentifier, appName, expectedBeatTimestamp)).thenReturn(false, false, true);
 
     this.testSubject.createBeat(appName, beat);
 
     Assert.assertTrue(this.eventRecorder.wait(EventConstants.POST_BEAT, new BeatEvent(appName, beat.getIdentifier())));
 
-    Mockito.verify(beatPublisherServiceSpy, Mockito.timeout(10_000).times(3)).publishBeat(beatId, tenantDataStoreContext.getTenantName(), appName, expectedBeatTimestamp);
+    Mockito.verify(beatPublisherServiceSpy, Mockito.timeout(10_000).times(3)).publishBeat(beatId, tenantIdentifier, appName, expectedBeatTimestamp);
   }
 }
\ No newline at end of file
diff --git a/service/build.gradle b/service/build.gradle
index 26015c3..98c8036 100644
--- a/service/build.gradle
+++ b/service/build.gradle
@@ -33,6 +33,7 @@
             [group: 'io.mifos.rhythm', name: 'api', version: project.version],
             [group: 'io.mifos.rhythm', name: 'spi', version: project.version],
             [group: 'io.mifos.anubis', name: 'library', version: versions.frameworkanubis],
+            [group: 'io.mifos.identity', name: 'api', version: versions.identity],
             [group: 'io.mifos.permitted-feign-client', name: 'library', version: versions.frameworkpermittedfeignclient],
             [group: 'com.google.code.gson', name: 'gson'],
             [group: 'io.mifos.core', name: 'lang', version: versions.frameworklang],
diff --git a/service/src/main/java/io/mifos/rhythm/service/RhythmApplication.java b/service/src/main/java/io/mifos/rhythm/service/RhythmApplication.java
new file mode 100644
index 0000000..fa6f5ec
--- /dev/null
+++ b/service/src/main/java/io/mifos/rhythm/service/RhythmApplication.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.rhythm.service;
+
+import org.springframework.boot.SpringApplication;
+
+/**
+ * @author Myrle Krantz
+ */
+public class RhythmApplication {
+
+  public RhythmApplication() {
+    super();
+  }
+
+  public static void main(String[] args) {
+    SpringApplication.run(RhythmConfiguration.class, args);
+  }
+}
diff --git a/service/src/main/java/io/mifos/rhythm/service/RhythmConfiguration.java b/service/src/main/java/io/mifos/rhythm/service/RhythmConfiguration.java
index 1cf17a9..469df5c 100644
--- a/service/src/main/java/io/mifos/rhythm/service/RhythmConfiguration.java
+++ b/service/src/main/java/io/mifos/rhythm/service/RhythmConfiguration.java
@@ -20,14 +20,18 @@
 import io.mifos.core.async.config.EnableAsync;
 import io.mifos.core.cassandra.config.EnableCassandra;
 import io.mifos.core.command.config.EnableCommandProcessing;
+import io.mifos.core.lang.config.EnableApplicationName;
 import io.mifos.core.lang.config.EnableServiceException;
 import io.mifos.core.lang.config.EnableTenantContext;
 import io.mifos.core.mariadb.config.EnableMariaDB;
 import io.mifos.permittedfeignclient.config.EnablePermissionRequestingFeignClient;
+import io.mifos.rhythm.service.internal.identity.ApplicationPermissionRequestCreator;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.cloud.netflix.feign.EnableFeignClients;
+import org.springframework.cloud.netflix.ribbon.RibbonClient;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
@@ -52,13 +56,15 @@
 @EnableServiceException
 @EnableScheduling
 @EnableTenantContext
-@EnablePermissionRequestingFeignClient
+@EnablePermissionRequestingFeignClient(feignClasses = {ApplicationPermissionRequestCreator.class})
+@RibbonClient(name = "rhythm-v1")
+@EnableApplicationName
+@EnableFeignClients(clients = {ApplicationPermissionRequestCreator.class})
 @ComponentScan({
     "io.mifos.rhythm.service.rest",
     "io.mifos.rhythm.service.config",
     "io.mifos.rhythm.service.internal.service",
     "io.mifos.rhythm.service.internal.repository",
-    "io.mifos.rhythm.service.internal.scheduler",
     "io.mifos.rhythm.service.internal.command.handler"
 })
 @EnableJpaRepositories({
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/command/handler/ApplicationCommandHandler.java b/service/src/main/java/io/mifos/rhythm/service/internal/command/handler/ApplicationCommandHandler.java
index 7b896fa..79476f0 100644
--- a/service/src/main/java/io/mifos/rhythm/service/internal/command/handler/ApplicationCommandHandler.java
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/command/handler/ApplicationCommandHandler.java
@@ -19,6 +19,7 @@
 import io.mifos.core.command.annotation.CommandHandler;
 import io.mifos.rhythm.api.v1.events.EventConstants;
 import io.mifos.rhythm.service.internal.command.DeleteApplicationCommand;
+import io.mifos.rhythm.service.internal.repository.ApplicationRepository;
 import io.mifos.rhythm.service.internal.repository.BeatRepository;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.transaction.annotation.Transactional;
@@ -29,12 +30,17 @@
 @SuppressWarnings("unused")
 @Aggregate
 public class ApplicationCommandHandler {
+  private final ApplicationRepository applicationRepository;
   private final BeatRepository beatRepository;
   private final EventHelper eventHelper;
 
   @Autowired
-  public ApplicationCommandHandler(final BeatRepository beatRepository, final EventHelper eventHelper) {
+  public ApplicationCommandHandler(
+          final ApplicationRepository applicationRepository,
+          final BeatRepository beatRepository,
+          final EventHelper eventHelper) {
     super();
+    this.applicationRepository = applicationRepository;
     this.beatRepository = beatRepository;
     this.eventHelper = eventHelper;
   }
@@ -42,6 +48,7 @@
   @CommandHandler
   @Transactional
   public void process(final DeleteApplicationCommand deleteApplicationCommand) {
+    this.applicationRepository.deleteByTenantIdentifierAndApplicationName(deleteApplicationCommand.getTenantIdentifier(), deleteApplicationCommand.getApplicationName());
     this.beatRepository.deleteByTenantIdentifierAndApplicationName(deleteApplicationCommand.getTenantIdentifier(), deleteApplicationCommand.getApplicationName());
     eventHelper.sendEvent(EventConstants.DELETE_APPLICATION, deleteApplicationCommand.getTenantIdentifier(), deleteApplicationCommand.getApplicationName());
   }
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/command/handler/BeatCommandHandler.java b/service/src/main/java/io/mifos/rhythm/service/internal/command/handler/BeatCommandHandler.java
index f287f86..07480a3 100644
--- a/service/src/main/java/io/mifos/rhythm/service/internal/command/handler/BeatCommandHandler.java
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/command/handler/BeatCommandHandler.java
@@ -20,12 +20,16 @@
 import io.mifos.core.lang.ServiceException;
 import io.mifos.rhythm.api.v1.events.BeatEvent;
 import io.mifos.rhythm.api.v1.events.EventConstants;
+import io.mifos.rhythm.service.ServiceConstants;
 import io.mifos.rhythm.service.internal.command.CreateBeatCommand;
 import io.mifos.rhythm.service.internal.command.DeleteBeatCommand;
 import io.mifos.rhythm.service.internal.mapper.BeatMapper;
 import io.mifos.rhythm.service.internal.repository.BeatEntity;
 import io.mifos.rhythm.service.internal.repository.BeatRepository;
+import io.mifos.rhythm.service.internal.service.IdentityPermittableGroupService;
+import org.slf4j.Logger;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.util.Optional;
@@ -36,20 +40,33 @@
 @SuppressWarnings("unused")
 @Aggregate
 public class BeatCommandHandler {
-
+  private final IdentityPermittableGroupService identityPermittableGroupService;
   private final BeatRepository beatRepository;
   private final EventHelper eventHelper;
+  private final Logger logger;
 
   @Autowired
-  public BeatCommandHandler(final BeatRepository beatRepository, final EventHelper eventHelper) {
+  public BeatCommandHandler(
+          final IdentityPermittableGroupService identityPermittableGroupService,
+          final BeatRepository beatRepository,
+          final EventHelper eventHelper,
+          @Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger) {
     super();
+    this.identityPermittableGroupService = identityPermittableGroupService;
     this.beatRepository = beatRepository;
     this.eventHelper = eventHelper;
+    this.logger = logger;
   }
 
   @CommandHandler
   @Transactional
   public void process(final CreateBeatCommand createBeatCommand) {
+    final boolean applicationHasRequestForAccessPermission = identityPermittableGroupService.checkThatApplicationHasRequestForAccessPermission(
+            createBeatCommand.getTenantIdentifier(), createBeatCommand.getApplicationName());
+    if (!applicationHasRequestForAccessPermission) {
+      logger.warn("Rhythm needs permission to publish beats to application, but couldn't request that permission for tenant '{}' and application '{}'.",
+              createBeatCommand.getApplicationName(), createBeatCommand.getTenantIdentifier());
+    }
 
     final BeatEntity entity = BeatMapper.map(
             createBeatCommand.getTenantIdentifier(),
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/identity/ApplicationPermissionRequestCreator.java b/service/src/main/java/io/mifos/rhythm/service/internal/identity/ApplicationPermissionRequestCreator.java
new file mode 100644
index 0000000..498638b
--- /dev/null
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/identity/ApplicationPermissionRequestCreator.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.rhythm.service.internal.identity;
+
+import io.mifos.anubis.annotation.Permittable;
+import io.mifos.core.api.annotation.ThrowsException;
+import io.mifos.identity.api.v1.client.ApplicationPermissionAlreadyExistsException;
+import io.mifos.identity.api.v1.domain.Permission;
+import io.mifos.permittedfeignclient.annotation.EndpointSet;
+import io.mifos.permittedfeignclient.annotation.PermittedFeignClientsConfiguration;
+import org.springframework.cloud.netflix.feign.FeignClient;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+/**
+ * @author Myrle Krantz
+ */
+@EndpointSet(identifier = "rhythm__v1__identity__v1")
+@FeignClient(name="identity-v1", path="/identity/v1", configuration=PermittedFeignClientsConfiguration.class)
+public interface ApplicationPermissionRequestCreator {
+
+  @RequestMapping(value = "/applications/{applicationidentifier}/permissions", method = RequestMethod.POST,
+          consumes = {MediaType.APPLICATION_JSON_VALUE},
+          produces = {MediaType.ALL_VALUE})
+  @ThrowsException(status = HttpStatus.CONFLICT, exception = ApplicationPermissionAlreadyExistsException.class)
+  @Permittable(groupId = io.mifos.identity.api.v1.PermittableGroupIds.APPLICATION_SELF_MANAGEMENT)
+  void createApplicationPermission(@PathVariable("applicationidentifier") String applicationIdentifier, Permission permission);
+}
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/repository/ApplicationEntity.java b/service/src/main/java/io/mifos/rhythm/service/internal/repository/ApplicationEntity.java
new file mode 100644
index 0000000..5cb48dc
--- /dev/null
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/repository/ApplicationEntity.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.rhythm.service.internal.repository;
+
+import javax.persistence.*;
+import java.util.Objects;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings({"unused", "WeakerAccess"})
+@Entity
+@Table(name = "khepri_apps")
+public class ApplicationEntity {
+  @Id
+  @GeneratedValue(strategy = GenerationType.IDENTITY)
+  @Column(name = "id")
+  private Long id;
+
+  @Column(name = "tenant_identifier", nullable = false)
+  private String tenantIdentifier;
+
+  @Column(name = "application_name", nullable = false)
+  private String applicationName;
+
+  @Column(name = "permittable_identifier")
+  private String consumerPermittableGroupIdentifier;
+
+  public ApplicationEntity() {
+  }
+
+  public ApplicationEntity(String tenantIdentifier, String applicationName, String consumerPermittableGroupIdentifier) {
+    this.tenantIdentifier = tenantIdentifier;
+    this.applicationName = applicationName;
+    this.consumerPermittableGroupIdentifier = consumerPermittableGroupIdentifier;
+  }
+
+  public Long getId() {
+    return id;
+  }
+
+  public void setId(Long id) {
+    this.id = id;
+  }
+
+  public String getTenantIdentifier() {
+    return tenantIdentifier;
+  }
+
+  public void setTenantIdentifier(String tenantIdentifier) {
+    this.tenantIdentifier = tenantIdentifier;
+  }
+
+  public String getApplicationName() {
+    return applicationName;
+  }
+
+  public void setApplicationName(String applicationName) {
+    this.applicationName = applicationName;
+  }
+
+  public String getConsumerPermittableGroupIdentifier() {
+    return consumerPermittableGroupIdentifier;
+  }
+
+  public void setConsumerPermittableGroupIdentifier(String consumerPermittableGroupIdentifier) {
+    this.consumerPermittableGroupIdentifier = consumerPermittableGroupIdentifier;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    ApplicationEntity that = (ApplicationEntity) o;
+    return Objects.equals(tenantIdentifier, that.tenantIdentifier) &&
+            Objects.equals(applicationName, that.applicationName);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(tenantIdentifier, applicationName);
+  }
+
+  @Override
+  public String toString() {
+    return "ApplicationEntity{" +
+            "id=" + id +
+            ", tenantIdentifier='" + tenantIdentifier + '\'' +
+            ", applicationName='" + applicationName + '\'' +
+            ", consumerPermittableGroupIdentifier='" + consumerPermittableGroupIdentifier + '\'' +
+            '}';
+  }
+}
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/repository/ApplicationRepository.java b/service/src/main/java/io/mifos/rhythm/service/internal/repository/ApplicationRepository.java
new file mode 100644
index 0000000..da3e821
--- /dev/null
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/repository/ApplicationRepository.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.rhythm.service.internal.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+/**
+ * @author Myrle Krantz
+ */
+@Repository
+public interface ApplicationRepository extends JpaRepository<ApplicationEntity, Long> {
+  void deleteByTenantIdentifierAndApplicationName(String tenantIdentifier, String applicationName);
+  Optional<ApplicationEntity> findByTenantIdentifierAndApplicationName(String tenantIdentifier, String applicationName);
+}
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/scheduler/Drummer.java b/service/src/main/java/io/mifos/rhythm/service/internal/scheduler/Drummer.java
deleted file mode 100644
index 4f84037..0000000
--- a/service/src/main/java/io/mifos/rhythm/service/internal/scheduler/Drummer.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright 2017 The Mifos Initiative.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package io.mifos.rhythm.service.internal.scheduler;
-
-import io.mifos.rhythm.service.ServiceConstants;
-import io.mifos.rhythm.service.internal.repository.BeatEntity;
-import io.mifos.rhythm.service.internal.repository.BeatRepository;
-import io.mifos.rhythm.service.internal.service.BeatPublisherService;
-import org.slf4j.Logger;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.dao.InvalidDataAccessResourceUsageException;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-import org.springframework.transaction.annotation.Transactional;
-
-import java.time.LocalDateTime;
-import java.time.ZoneId;
-import java.util.Optional;
-import java.util.stream.Stream;
-
-/**
- * @author Myrle Krantz
- */
-@SuppressWarnings({"unused", "WeakerAccess"})
-@Component
-public class Drummer {
-  private final BeatPublisherService beatPublisherService;
-  private final BeatRepository beatRepository;
-  private final Logger logger;
-
-  @Autowired
-  public Drummer(
-          final BeatPublisherService beatPublisherService,
-          final BeatRepository beatRepository,
-          @Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger) {
-    this.beatPublisherService = beatPublisherService;
-    this.beatRepository = beatRepository;
-    this.logger = logger;
-  }
-
-  @Scheduled(initialDelayString = "${rhythm.beatCheckRate}", fixedRateString = "${rhythm.beatCheckRate}")
-  @Transactional
-  public void checkForBeatsNeeded() {
-    try {
-      final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
-      //Get beats from the last two hours in case restart/start happens close to hour begin.
-      final Stream<BeatEntity> beats = beatRepository.findByNextBeatBefore(now);
-      beats.forEach((beat) -> {
-        logger.info("Checking if beat {} needs publishing.", beat);
-        final Optional<LocalDateTime> nextBeat = beatPublisherService.checkBeatForPublish(now, beat.getBeatIdentifier(), beat.getTenantIdentifier(), beat.getApplicationName(), beat.getAlignmentHour(), beat.getNextBeat());
-        nextBeat.ifPresent(x -> {
-          beat.setNextBeat(x);
-          beatRepository.save(beat);
-        });
-      });
-
-    }
-    catch (final InvalidDataAccessResourceUsageException e) {
-      logger.info("InvalidDataAccessResourceUsageException in check for scheduled beats, probably " +
-              "because initialize hasn't been called yet. {}", e);
-    }
-  }
-}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/service/BeatPublisherService.java b/service/src/main/java/io/mifos/rhythm/service/internal/service/BeatPublisherService.java
index 3d57c9a..6495db2 100644
--- a/service/src/main/java/io/mifos/rhythm/service/internal/service/BeatPublisherService.java
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/service/BeatPublisherService.java
@@ -15,12 +15,18 @@
  */
 package io.mifos.rhythm.service.internal.service;
 
+import io.mifos.anubis.api.v1.domain.AllowedOperation;
 import io.mifos.core.api.context.AutoUserContext;
 import io.mifos.core.api.util.ApiFactory;
+import io.mifos.core.lang.ApplicationName;
 import io.mifos.core.lang.AutoTenantContext;
 import io.mifos.core.lang.DateConverter;
+import io.mifos.identity.api.v1.client.ApplicationPermissionAlreadyExistsException;
+import io.mifos.identity.api.v1.domain.Permission;
 import io.mifos.permittedfeignclient.service.ApplicationAccessTokenService;
 import io.mifos.rhythm.service.config.RhythmProperties;
+import io.mifos.rhythm.service.internal.identity.ApplicationPermissionRequestCreator;
+import io.mifos.rhythm.spi.v1.PermittableGroupIds;
 import io.mifos.rhythm.spi.v1.client.BeatListener;
 import io.mifos.rhythm.spi.v1.domain.BeatPublish;
 import org.slf4j.Logger;
@@ -30,23 +36,23 @@
 import org.springframework.cloud.client.discovery.DiscoveryClient;
 import org.springframework.stereotype.Service;
 
-import javax.annotation.Nonnull;
 import java.time.LocalDateTime;
-import java.time.temporal.ChronoUnit;
+import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
-import java.util.function.Predicate;
-import java.util.stream.Stream;
 
 import static io.mifos.rhythm.service.ServiceConstants.LOGGER_NAME;
 
 /**
  * @author Myrle Krantz
  */
+@SuppressWarnings("WeakerAccess")
 @Service
 public class BeatPublisherService {
   private final DiscoveryClient discoveryClient;
+  private final ApplicationPermissionRequestCreator applicationPermissionRequestCreator;
   private final ApplicationAccessTokenService applicationAccessTokenService;
+  private final ApplicationName rhythmApplicationName;
   private final ApiFactory apiFactory;
   private final RhythmProperties properties;
   private final Logger logger;
@@ -54,19 +60,54 @@
   @Autowired
   public BeatPublisherService(
           @SuppressWarnings("SpringJavaAutowiringInspection") final DiscoveryClient discoveryClient,
+          @SuppressWarnings("SpringJavaAutowiringInspection") final ApplicationPermissionRequestCreator applicationPermissionRequestCreator,
           @SuppressWarnings("SpringJavaAutowiringInspection") final ApplicationAccessTokenService applicationAccessTokenService,
+          final ApplicationName rhythmApplicationName,
           final ApiFactory apiFactory,
           final RhythmProperties properties,
           @Qualifier(LOGGER_NAME) final Logger logger) {
     this.discoveryClient = discoveryClient;
+    this.applicationPermissionRequestCreator = applicationPermissionRequestCreator;
     this.applicationAccessTokenService = applicationAccessTokenService;
+    this.rhythmApplicationName = rhythmApplicationName;
     this.apiFactory = apiFactory;
     this.properties = properties;
     this.logger = logger;
   }
 
   /**
-   * Authenticate with identity and publish the beat to the application.  This function performs all the internal
+   * Register a request to access an endpoint with identity.  This only creates the request.  The request must be
+   * accepted by the user named in rhythm's configuration before beats can actually be sent.  This function makes
+   * calls to identity, and therefore most be mocked in unit and component tests.
+   *
+   * @param tenantIdentifier The tenant identifier as provided via the tenant header when the beat was created.
+   * @param applicationName The name of the application the beat should be sent to.
+   *
+   * @return true if the beat was published.  false if the beat was not published, or we just don't know.
+   */
+  @SuppressWarnings("WeakerAccess") //Access is public for mocking in component test.
+  public Optional<String> requestPermissionForBeats(final String tenantIdentifier, final String applicationName) {
+    try (final AutoTenantContext ignored = new AutoTenantContext(tenantIdentifier)) {
+      try (final AutoUserContext ignored2 = new AutoUserContext(properties.getUser(), "")) {
+        final String consumerPermittableGroupIdentifier = PermittableGroupIds.forApplication(applicationName);
+        final Permission publishBeatPermission = new Permission();
+        publishBeatPermission.setAllowedOperations(Collections.singleton(AllowedOperation.CHANGE));
+        publishBeatPermission.setPermittableEndpointGroupIdentifier(consumerPermittableGroupIdentifier);
+        try {
+          applicationPermissionRequestCreator.createApplicationPermission(rhythmApplicationName.toString(), publishBeatPermission);
+        }
+        catch (final ApplicationPermissionAlreadyExistsException ignored3) { }
+
+        return Optional.of(consumerPermittableGroupIdentifier);
+      }
+    }
+    catch (final Throwable e) {
+      return Optional.empty();
+    }
+  }
+
+  /**
+   * Authenticate with identity and publish the beat to the application.  This function performs all the scheduled
    * interprocess communication in rhythm, and therefore most be mocked in unit and component tests.
    *
    * @param beatIdentifier The identifier of the beat as provided when the beat was created.
@@ -76,7 +117,7 @@
    *
    * @return true if the beat was published.  false if the beat was not published, or we just don't know.
    */
-  @SuppressWarnings("WeakerAccess") //Access is public for spying in component test.
+  @SuppressWarnings("WeakerAccess") //Access is public for mocking in component test.
   public boolean publishBeat(
           final String beatIdentifier,
           final String tenantIdentifier,
@@ -92,64 +133,27 @@
     final ServiceInstance beatListenerService = applicationsByName.get(0);
     final BeatListener beatListener = apiFactory.create(BeatListener.class, beatListenerService.getUri().toString());
 
-    final String accessToken = applicationAccessTokenService.getAccessToken(
-            properties.getUser(), getEndointSetIdentifier(applicationName));
-    try (final AutoUserContext ignored2 = new AutoUserContext(properties.getUser(), accessToken)) {
-      try (final AutoTenantContext ignored = new AutoTenantContext(tenantIdentifier)) {
+    try (final AutoTenantContext ignored = new AutoTenantContext(tenantIdentifier)) {
+      final String accessToken;
+      try {
+        accessToken = applicationAccessTokenService.getAccessToken(
+                properties.getUser(), tenantIdentifier);
+      }
+      catch (final Exception e) {
+        logger.warn("Unable to publish beat '{}' to application '{}' for tenant '{}', " +
+                "because access token could not be acquired from identity. Exception was {}.",
+                beatIdentifier, applicationName, tenantIdentifier, e);
+        return false;
+      }
+      try (final AutoUserContext ignored2 = new AutoUserContext(properties.getUser(), accessToken)) {
         beatListener.publishBeat(beatPublish);
         return true;
       }
     }
     catch (final Throwable e) {
+      logger.warn("Unable to publish beat '{}' to application '{}' for tenant '{}', " +
+              "because exception was thrown in publish {}.", beatIdentifier, applicationName, tenantIdentifier, e);
       return false;
     }
   }
-
-  private static String getEndointSetIdentifier(final String applicationName) {
-    return applicationName.replace("-", "__") + "__khepri";
-  }
-
-  public Optional<LocalDateTime> checkBeatForPublish(
-          final LocalDateTime now,
-          final String beatIdentifier,
-          final String tenantIdentifier,
-          final String applicationName,
-          final Integer alignmentHour,
-          final LocalDateTime nextBeat) {
-    return checkBeatForPublishHelper(now, alignmentHour, nextBeat, x -> publishBeat(beatIdentifier, tenantIdentifier, applicationName, x));
-  }
-
-  //Helper is separated from original function so that it can be unit-tested separately from publishBeat.
-  static Optional<LocalDateTime> checkBeatForPublishHelper(
-          final LocalDateTime now,
-          final Integer alignmentHour,
-          final LocalDateTime nextBeat,
-          final Predicate<LocalDateTime> publishSucceeded) {
-    final long numberOfBeatPublishesNeeded = getNumberOfBeatPublishesNeeded(now, nextBeat);
-    if (numberOfBeatPublishesNeeded == 0)
-      return Optional.empty();
-
-    final Optional<LocalDateTime> firstFailedBeat = Stream.iterate(nextBeat,
-            x -> incrementToAlignment(x, alignmentHour))
-            .limit(numberOfBeatPublishesNeeded)
-            .filter(x -> !publishSucceeded.test(x))
-            .findFirst();
-
-    if (firstFailedBeat.isPresent())
-      return firstFailedBeat;
-    else
-      return Optional.of(incrementToAlignment(now, alignmentHour));
-  }
-
-  static long getNumberOfBeatPublishesNeeded(final LocalDateTime now, final @Nonnull LocalDateTime nextBeat) {
-    if (nextBeat.isAfter(now))
-      return 0;
-
-    return Math.max(1, nextBeat.until(now, ChronoUnit.DAYS));
-  }
-
-  static LocalDateTime incrementToAlignment(final LocalDateTime toIncrement, final Integer alignmentHour)
-  {
-    return toIncrement.plusDays(1).truncatedTo(ChronoUnit.DAYS).plusHours(alignmentHour);
-  }
 }
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/service/Drummer.java b/service/src/main/java/io/mifos/rhythm/service/internal/service/Drummer.java
new file mode 100644
index 0000000..e8cda57
--- /dev/null
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/service/Drummer.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.rhythm.service.internal.service;
+
+import io.mifos.rhythm.service.ServiceConstants;
+import io.mifos.rhythm.service.internal.repository.BeatEntity;
+import io.mifos.rhythm.service.internal.repository.BeatRepository;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.dao.InvalidDataAccessResourceUsageException;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Nonnull;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.temporal.ChronoUnit;
+import java.util.Optional;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings({"unused", "WeakerAccess"})
+@Component
+public class Drummer {
+  private final IdentityPermittableGroupService identityPermittableGroupService;
+  private final BeatPublisherService beatPublisherService;
+  private final BeatRepository beatRepository;
+  private final Logger logger;
+
+  @Autowired
+  public Drummer(
+          final IdentityPermittableGroupService identityPermittableGroupService,
+          final BeatPublisherService beatPublisherService,
+          final BeatRepository beatRepository,
+          @Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger) {
+    this.identityPermittableGroupService = identityPermittableGroupService;
+    this.beatPublisherService = beatPublisherService;
+    this.beatRepository = beatRepository;
+    this.logger = logger;
+  }
+
+  @Scheduled(initialDelayString = "${rhythm.beatCheckRate}", fixedRateString = "${rhythm.beatCheckRate}")
+  @Transactional
+  public void checkForBeatsNeeded() {
+    try {
+      final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
+      //Get beats from the last two hours in case restart/start happens close to hour begin.
+      final Stream<BeatEntity> beats = beatRepository.findByNextBeatBefore(now);
+      beats.forEach((beat) -> {
+        final boolean applicationHasRequestForAccessPermission
+                = identityPermittableGroupService.checkThatApplicationHasRequestForAccessPermission(
+                beat.getTenantIdentifier(), beat.getApplicationName());
+        if (!applicationHasRequestForAccessPermission) {
+          logger.info("Not checking if beat {} needs publishing, because application access needed to publish is not available.", beat);
+        }
+        else {
+          logger.info("Checking if beat {} needs publishing.", beat);
+          final Optional<LocalDateTime> nextBeat = checkBeatForPublish(
+                  now,
+                  beat.getBeatIdentifier(),
+                  beat.getTenantIdentifier(),
+                  beat.getApplicationName(),
+                  beat.getAlignmentHour(),
+                  beat.getNextBeat());
+          nextBeat.ifPresent(y -> {
+            beat.setNextBeat(y);
+            beatRepository.save(beat);
+          });
+        }
+      });
+
+    }
+    catch (final InvalidDataAccessResourceUsageException e) {
+      logger.info("InvalidDataAccessResourceUsageException in check for scheduled beats, probably " +
+              "because initialize hasn't been called yet. {}", e);
+    }
+  }
+
+  public Optional<LocalDateTime> checkBeatForPublish(
+          final LocalDateTime now,
+          final String beatIdentifier,
+          final String tenantIdentifier,
+          final String applicationName,
+          final Integer alignmentHour,
+          final LocalDateTime nextBeat) {
+    return checkBeatForPublishHelper(now, alignmentHour, nextBeat,
+            x -> beatPublisherService.publishBeat(beatIdentifier, tenantIdentifier, applicationName, x));
+  }
+
+  //Helper is separated from original function so that it can be unit-tested separately from publishBeat.
+  static Optional<LocalDateTime> checkBeatForPublishHelper(
+          final LocalDateTime now,
+          final Integer alignmentHour,
+          final LocalDateTime nextBeat,
+          final Predicate<LocalDateTime> publishSucceeded) {
+    final long numberOfBeatPublishesNeeded = getNumberOfBeatPublishesNeeded(now, nextBeat);
+    if (numberOfBeatPublishesNeeded == 0)
+      return Optional.empty();
+
+    final Optional<LocalDateTime> firstFailedBeat = Stream.iterate(nextBeat,
+            x -> incrementToAlignment(x, alignmentHour))
+            .limit(numberOfBeatPublishesNeeded)
+            .filter(x -> !publishSucceeded.test(x))
+            .findFirst();
+
+    if (firstFailedBeat.isPresent())
+      return firstFailedBeat;
+    else
+      return Optional.of(incrementToAlignment(now, alignmentHour));
+  }
+
+  static long getNumberOfBeatPublishesNeeded(final LocalDateTime now, final @Nonnull LocalDateTime nextBeat) {
+    if (nextBeat.isAfter(now))
+      return 0;
+
+    return Math.max(1, nextBeat.until(now, ChronoUnit.DAYS));
+  }
+
+  static LocalDateTime incrementToAlignment(final LocalDateTime toIncrement, final Integer alignmentHour)
+  {
+    return toIncrement.plusDays(1).truncatedTo(ChronoUnit.DAYS).plusHours(alignmentHour);
+  }
+}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/service/IdentityPermittableGroupService.java b/service/src/main/java/io/mifos/rhythm/service/internal/service/IdentityPermittableGroupService.java
new file mode 100644
index 0000000..f8dab7f
--- /dev/null
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/service/IdentityPermittableGroupService.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.rhythm.service.internal.service;
+
+import io.mifos.rhythm.service.internal.repository.ApplicationEntity;
+import io.mifos.rhythm.service.internal.repository.ApplicationRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Optional;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class IdentityPermittableGroupService {
+  private final ApplicationRepository applicationRepository;
+  private final BeatPublisherService beatPublisherService;
+
+  @Autowired
+  public IdentityPermittableGroupService(
+          final ApplicationRepository applicationRepository,
+          final BeatPublisherService beatPublisherService) {
+    this.applicationRepository = applicationRepository;
+    this.beatPublisherService = beatPublisherService;
+  }
+
+  @Transactional
+  public boolean checkThatApplicationHasRequestForAccessPermission(
+          final String tenantIdentifier,
+          final String applicationName) {
+    final Optional<ApplicationEntity> findApplication = applicationRepository.findByTenantIdentifierAndApplicationName(
+            tenantIdentifier,
+            applicationName);
+    if (findApplication.isPresent())
+      return true;
+    else {
+      final Optional<String> ret = beatPublisherService.requestPermissionForBeats(tenantIdentifier, applicationName);
+
+      ret.ifPresent(x -> {
+        final ApplicationEntity saveApplication = new ApplicationEntity();
+        saveApplication.setTenantIdentifier(tenantIdentifier);
+        saveApplication.setApplicationName(applicationName);
+        saveApplication.setConsumerPermittableGroupIdentifier(x);
+        applicationRepository.save(saveApplication);
+      });
+
+      return ret.isPresent();
+    }
+  }
+}
diff --git a/service/src/main/resources/db/migrations/mariadb/V1__initial_setup.sql b/service/src/main/resources/db/migrations/mariadb/V1__initial_setup.sql
index bd253d9..8b6ebe2 100644
--- a/service/src/main/resources/db/migrations/mariadb/V1__initial_setup.sql
+++ b/service/src/main/resources/db/migrations/mariadb/V1__initial_setup.sql
@@ -19,7 +19,7 @@
 
 CREATE TABLE khepri_beats (
   id BIGINT NOT NULL AUTO_INCREMENT,
-  tenant_identifier        VARCHAR(64) NOT NULL,
+  tenant_identifier        VARCHAR(32) NOT NULL,
   application_name         VARCHAR(64) NOT NULL,
   beat_identifier          VARCHAR(32) NOT NULL,
   alignment_hour           INT         NOT NULL,
@@ -27,3 +27,12 @@
   CONSTRAINT khepri_beats_uq UNIQUE (tenant_identifier, application_name, beat_identifier),
   CONSTRAINT khepri_beats_pk PRIMARY KEY (id)
 );
+
+CREATE TABLE khepri_apps (
+  id BIGINT NOT NULL AUTO_INCREMENT,
+  tenant_identifier        VARCHAR(32) NOT NULL,
+  application_name         VARCHAR(32) NOT NULL,
+  permittable_identifier   VARCHAR(32) NOT NULL,
+  CONSTRAINT khepri_apps_uq UNIQUE (tenant_identifier, application_name),
+  CONSTRAINT khepri_apps_pk PRIMARY KEY (id)
+);
\ No newline at end of file
diff --git a/service/src/main/resources/logback.xml b/service/src/main/resources/logback.xml
new file mode 100644
index 0000000..16f5144
--- /dev/null
+++ b/service/src/main/resources/logback.xml
@@ -0,0 +1,55 @@
+<!--
+
+    Copyright 2017 The Mifos Initiative.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<configuration>
+    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>logs/rhythm.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>rhythm.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <maxHistory>7</maxHistory>
+            <totalSizeCap>2GB</totalSizeCap>
+        </rollingPolicy>
+        <encoder>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <logger name="com" level="INFO">
+        <appender-ref ref="STDOUT" />
+    </logger>
+
+    <logger name="org" level="INFO">
+        <appender-ref ref="STDOUT" />
+    </logger>
+
+    <logger name="io" level="INFO">
+        <appender-ref ref="STDOUT" />
+    </logger>
+
+    <logger name="net" level="INFO">
+        <appender-ref ref="STDOUT" />
+    </logger>
+
+    <root level="DEBUG">
+        <appender-ref ref="FILE"/>
+    </root>
+</configuration>
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/rhythm/service/internal/service/BeatPublisherServiceTest.java b/service/src/test/java/io/mifos/rhythm/service/internal/service/DrummerTest.java
similarity index 70%
rename from service/src/test/java/io/mifos/rhythm/service/internal/service/BeatPublisherServiceTest.java
rename to service/src/test/java/io/mifos/rhythm/service/internal/service/DrummerTest.java
index 3d82b75..3cb376d 100644
--- a/service/src/test/java/io/mifos/rhythm/service/internal/service/BeatPublisherServiceTest.java
+++ b/service/src/test/java/io/mifos/rhythm/service/internal/service/DrummerTest.java
@@ -28,12 +28,12 @@
 /**
  * @author Myrle Krantz
  */
-public class BeatPublisherServiceTest {
+public class DrummerTest {
 
   @Test
   public void incrementToAlignment() {
     final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
-    final LocalDateTime tomorrow = BeatPublisherService.incrementToAlignment(now, 3);
+    final LocalDateTime tomorrow = Drummer.incrementToAlignment(now, 3);
 
     Assert.assertEquals(tomorrow.minusDays(1).truncatedTo(ChronoUnit.DAYS), now.truncatedTo(ChronoUnit.DAYS));
     Assert.assertEquals(3, tomorrow.getHour());
@@ -42,21 +42,21 @@
   @Test
   public void getNumberOfBeatPublishesNeeded() {
     final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
-    final long eventsNeeded3 = BeatPublisherService.getNumberOfBeatPublishesNeeded(now, now.minus(3, ChronoUnit.DAYS));
+    final long eventsNeeded3 = Drummer.getNumberOfBeatPublishesNeeded(now, now.minus(3, ChronoUnit.DAYS));
     Assert.assertEquals(3, eventsNeeded3);
 
-    final long eventsNeededPast = BeatPublisherService.getNumberOfBeatPublishesNeeded(now, now.plus(1, ChronoUnit.DAYS));
+    final long eventsNeededPast = Drummer.getNumberOfBeatPublishesNeeded(now, now.plus(1, ChronoUnit.DAYS));
     Assert.assertEquals(0, eventsNeededPast);
 
-    final long eventsNeededNow = BeatPublisherService.getNumberOfBeatPublishesNeeded(now, now.minus(2, ChronoUnit.MINUTES));
+    final long eventsNeededNow = Drummer.getNumberOfBeatPublishesNeeded(now, now.minus(2, ChronoUnit.MINUTES));
     Assert.assertEquals(1, eventsNeededNow);
   }
 
   @Test
   public void checkBeatForPublishAllBeatsSucceed() {
     final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
-    final Optional<LocalDateTime> ret = BeatPublisherService.checkBeatForPublishHelper(now, 0, now.minus(3, ChronoUnit.DAYS), x -> true);
-    Assert.assertEquals(Optional.of(BeatPublisherService.incrementToAlignment(now, 0)), ret);
+    final Optional<LocalDateTime> ret = Drummer.checkBeatForPublishHelper(now, 0, now.minus(3, ChronoUnit.DAYS), x -> true);
+    Assert.assertEquals(Optional.of(Drummer.incrementToAlignment(now, 0)), ret);
   }
 
   @Test
@@ -65,7 +65,7 @@
     final LocalDateTime nextBeat = now.minus(3, ChronoUnit.DAYS);
     @SuppressWarnings("unchecked") final Predicate<LocalDateTime> produceBeatsMock = Mockito.mock(Predicate.class);
     Mockito.when(produceBeatsMock.test(nextBeat)).thenReturn(false);
-    final Optional<LocalDateTime> ret = BeatPublisherService.checkBeatForPublishHelper(now, 0, nextBeat, produceBeatsMock);
+    final Optional<LocalDateTime> ret = Drummer.checkBeatForPublishHelper(now, 0, nextBeat, produceBeatsMock);
     Assert.assertEquals(Optional.of(nextBeat), ret);
   }
 
@@ -73,18 +73,18 @@
   public void checkBeatForPublishSecondFails() {
     final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
     final LocalDateTime nextBeat = now.minus(3, ChronoUnit.DAYS);
-    final LocalDateTime secondBeat = BeatPublisherService.incrementToAlignment(nextBeat, 0);
+    final LocalDateTime secondBeat = Drummer.incrementToAlignment(nextBeat, 0);
     @SuppressWarnings("unchecked") final Predicate<LocalDateTime> produceBeatsMock = Mockito.mock(Predicate.class);
     Mockito.when(produceBeatsMock.test(nextBeat)).thenReturn(true);
     Mockito.when(produceBeatsMock.test(secondBeat)).thenReturn(false);
-    final Optional<LocalDateTime> ret = BeatPublisherService.checkBeatForPublishHelper(now, 0, nextBeat, produceBeatsMock);
+    final Optional<LocalDateTime> ret = Drummer.checkBeatForPublishHelper(now, 0, nextBeat, produceBeatsMock);
     Assert.assertEquals(Optional.of(secondBeat), ret);
   }
 
   @Test
   public void checkBeatForPublishNoneNeeded() {
     final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
-    final Optional<LocalDateTime> ret = BeatPublisherService.checkBeatForPublishHelper(now, 0, now.plus(1, ChronoUnit.DAYS),
+    final Optional<LocalDateTime> ret = Drummer.checkBeatForPublishHelper(now, 0, now.plus(1, ChronoUnit.DAYS),
             x -> { Assert.fail("Pubish shouldn't be called"); return true; });
     Assert.assertEquals(Optional.empty(), ret);
   }
diff --git a/shared.gradle b/shared.gradle
index 4403324..dc7dd71 100644
--- a/shared.gradle
+++ b/shared.gradle
@@ -1,5 +1,5 @@
 group 'io.mifos.rhythm'
-version '0.1.0.BUILD-SNAPSHOT'
+version '0.1.0-BUILD-SNAPSHOT'
 
 ext.versions = [
         frameworkapi : '0.1.0-BUILD-SNAPSHOT',
@@ -11,6 +11,7 @@
         frameworktest: '0.1.0-BUILD-SNAPSHOT',
         frameworkanubis: '0.1.0-BUILD-SNAPSHOT',
         frameworkpermittedfeignclient: '0.1.0-BUILD-SNAPSHOT',
+        identity:  '0.1.0-BUILD-SNAPSHOT',
         validator : '5.3.0.Final'
 ]
 
diff --git a/spi/build.gradle b/spi/build.gradle
index 17c6648..35ae7b7 100644
--- a/spi/build.gradle
+++ b/spi/build.gradle
@@ -18,6 +18,8 @@
     compile(
             [group: 'org.springframework.cloud', name: 'spring-cloud-starter-feign'],
             [group: 'io.mifos.core', name: 'api', version: versions.frameworkapi],
+            [group: 'io.mifos.core', name: 'cassandra', version: versions.frameworkcassandra],
+            [group: 'io.mifos.core', name: 'command', version: versions.frameworkcommand],
             [group: 'org.hibernate', name: 'hibernate-validator', version: versions.validator],
             [group: 'org.hibernate', name: 'hibernate-validator-annotation-processor', version: versions.validator]
     )
diff --git a/spi/src/main/java/io/mifos/rhythm/spi/v1/PermittableGroupIds.java b/spi/src/main/java/io/mifos/rhythm/spi/v1/PermittableGroupIds.java
new file mode 100644
index 0000000..cd2a35c
--- /dev/null
+++ b/spi/src/main/java/io/mifos/rhythm/spi/v1/PermittableGroupIds.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.rhythm.spi.v1;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+public interface PermittableGroupIds {
+  static String forApplication(final String beatConsumingApplicationName) {
+    return beatConsumingApplicationName.replace("-", "__") + "__khepri";
+  }
+}
diff --git a/spi/src/main/java/io/mifos/rhythm/spi/v1/client/BeatListener.java b/spi/src/main/java/io/mifos/rhythm/spi/v1/client/BeatListener.java
index c94a5d1..0e408e6 100644
--- a/spi/src/main/java/io/mifos/rhythm/spi/v1/client/BeatListener.java
+++ b/spi/src/main/java/io/mifos/rhythm/spi/v1/client/BeatListener.java
@@ -18,7 +18,6 @@
 import io.mifos.rhythm.spi.v1.domain.BeatPublish;
 import org.springframework.cloud.netflix.feign.FeignClient;
 import org.springframework.http.MediaType;
-import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMethod;
 
@@ -26,10 +25,12 @@
  * @author Myrle Krantz
  */
 @SuppressWarnings("unused")
-@FeignClient(path="/beatlistener/v1")
+@FeignClient
 public interface BeatListener {
+  String PUBLISH_BEAT_PATH = "/beatlistener/v1/publishedbeats";
+
   @RequestMapping(
-          value = "/publishedbeats",
+          value = PUBLISH_BEAT_PATH,
           method = RequestMethod.POST,
           produces = MediaType.APPLICATION_JSON_VALUE,
           consumes = MediaType.APPLICATION_JSON_VALUE
diff --git a/spi/src/main/java/io/mifos/rhythm/spi/v1/events/BeatPublishEvent.java b/spi/src/main/java/io/mifos/rhythm/spi/v1/events/BeatPublishEvent.java
new file mode 100644
index 0000000..c07a20d
--- /dev/null
+++ b/spi/src/main/java/io/mifos/rhythm/spi/v1/events/BeatPublishEvent.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.rhythm.spi.v1.events;
+
+import java.util.Objects;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings({"WeakerAccess", "unused"})
+public class BeatPublishEvent {
+  String applicationName;
+  String beatIdentifier;
+  String forTime;
+
+  public BeatPublishEvent() {
+  }
+
+  public BeatPublishEvent(String applicationName, String beatIdentifier, String forTime) {
+    this.applicationName = applicationName;
+    this.beatIdentifier = beatIdentifier;
+    this.forTime = forTime;
+  }
+
+  public String getApplicationName() {
+    return applicationName;
+  }
+
+  public void setApplicationName(String applicationName) {
+    this.applicationName = applicationName;
+  }
+
+  public String getBeatIdentifier() {
+    return beatIdentifier;
+  }
+
+  public void setBeatIdentifier(String beatIdentifier) {
+    this.beatIdentifier = beatIdentifier;
+  }
+
+  public String getForTime() {
+    return forTime;
+  }
+
+  public void setForTime(String forTime) {
+    this.forTime = forTime;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    BeatPublishEvent that = (BeatPublishEvent) o;
+    return Objects.equals(applicationName, that.applicationName) &&
+            Objects.equals(beatIdentifier, that.beatIdentifier) &&
+            Objects.equals(forTime, that.forTime);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(applicationName, beatIdentifier, forTime);
+  }
+
+  @Override
+  public String toString() {
+    return "BeatPublishEvent{" +
+            "applicationName='" + applicationName + '\'' +
+            ", beatIdentifier='" + beatIdentifier + '\'' +
+            ", forTime='" + forTime + '\'' +
+            '}';
+  }
+}
diff --git a/spi/src/main/java/io/mifos/rhythm/spi/v1/events/EventConstants.java b/spi/src/main/java/io/mifos/rhythm/spi/v1/events/EventConstants.java
new file mode 100644
index 0000000..25869ba
--- /dev/null
+++ b/spi/src/main/java/io/mifos/rhythm/spi/v1/events/EventConstants.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.rhythm.spi.v1.events;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+public interface EventConstants {
+  String DESTINATION = "beats-v1";
+  String SELECTOR_NAME = "action";
+  String POST_PUBLISHEDBEAT = "post-publishedbeat";
+  String SELECTOR_POST_PUBLISHEDBEAT = SELECTOR_NAME + " = '" + POST_PUBLISHEDBEAT + "'";
+}