diff --git a/backend/src/main/app-resources/default.yml b/backend/src/main/app-resources/default.yml index 949a8e4b5..5ba247176 100644 --- a/backend/src/main/app-resources/default.yml +++ b/backend/src/main/app-resources/default.yml @@ -19,6 +19,20 @@ ## The first time the scheduler executes on this node, write out the entity descriptors to file [true|false] (default is true) # entityDescriptor: # writeOnStartup: true +### BEACON SETTINGS +## Set enabled to false to disable sending beacon data +## Only set the installationID if you wish to define the ID used by the beacon. RECOMMENDED: ignore - a default random value will be used +## Only change the urls if instructed or if you wish to redirect for testing (if multiple, separate list with commas +## Only change the productName for testing purposes +## Set the cron time to send the beacon data at a specific time - if unset, the system will send at a random time between 12AM and 4AM once per day +# beacon: +# enabled: ture +# installationID: [user-defined value] +# urls: http://collector.testbed.tier.internet2.edu:5001 +# productName: ShibUI +# send: +# cron: 0 4 * * * * + # pac4j-enabled: true # pac4j: # keystorePath: "/etc/shibui/samlKeystore.jks" diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/beacon/BeaconDetail.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/beacon/BeaconDetail.java new file mode 100644 index 000000000..24c9ccbc4 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/beacon/BeaconDetail.java @@ -0,0 +1,21 @@ +package edu.internet2.tier.shibboleth.admin.ui.beacon; + +import lombok.Data; + +@Data +public class BeaconDetail { + private String msgType = "TIERBEACON"; + private String msgName = "TIER"; + private String msgVersion = "1.0"; + + private String tbProduct; + private String tbProductVersion; + private String tbTIERRelease; + private String tbMaintainer = "Unicon"; + + private ShibuiDetail shibui; +} + +// "tbProduct": "ShibUI (shibui.beacon.productName)", +// "tbProductVersion": "1.17.4 (can we get this from the app?)", +// "tbTIERRelease": "(if the TAP container: 1.17.4; if the Unicon package, INTERNAL_1.17.4)", \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/beacon/ShibuiDetail.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/beacon/ShibuiDetail.java new file mode 100644 index 000000000..fdf0abf6b --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/beacon/ShibuiDetail.java @@ -0,0 +1,21 @@ +package edu.internet2.tier.shibboleth.admin.ui.beacon; + +import lombok.Data; + +@Data +public class ShibuiDetail { + + + // "shibui": { + // "authMechanisms": [ + // "pac4j-saml" + // ], + // "numberOfMetadataSources": 25, + // "numberOfMetadataProviders": 3, + // "numberOfFilters": 0, + // "dailyLogins": 12, + // "numberOfGroups": 15, + // "numberOfRoles": 5, + // "installationID": "4813c762-4a90-40a2-9c01-81bf67647da4" + // } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java index d1c1e9a0b..f059b5fc6 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java @@ -1,7 +1,9 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration; import com.fasterxml.jackson.databind.Module; +import edu.internet2.tier.shibboleth.admin.ui.domain.BeaconConfiguration; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; +import edu.internet2.tier.shibboleth.admin.ui.repository.BeaconConfigurationRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolversPositionOrderContainerRepository; @@ -19,6 +21,7 @@ import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; +import edu.internet2.tier.shibboleth.admin.ui.service.BeaconDataServiceImpl; import edu.internet2.tier.shibboleth.admin.ui.service.DefaultMetadataResolversPositionOrderContainerService; import edu.internet2.tier.shibboleth.admin.ui.service.DirectoryService; import edu.internet2.tier.shibboleth.admin.ui.service.DirectoryServiceImpl; @@ -29,6 +32,7 @@ import edu.internet2.tier.shibboleth.admin.ui.service.FileCheckingFileWritingService; import edu.internet2.tier.shibboleth.admin.ui.service.FileWritingService; import edu.internet2.tier.shibboleth.admin.ui.service.FilterTargetService; +import edu.internet2.tier.shibboleth.admin.ui.service.IBeaconDataService; import edu.internet2.tier.shibboleth.admin.ui.service.JPADynamicRegistrationServiceImpl; import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl; import edu.internet2.tier.shibboleth.admin.ui.service.JPAFilterTargetServiceImpl; @@ -42,10 +46,12 @@ import jakarta.servlet.http.HttpServletRequest; import net.javacrumbs.shedlock.core.LockProvider; import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider; +import org.apache.commons.lang3.StringUtils; import org.apache.lucene.analysis.Analyzer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.actuate.info.InfoEndpoint; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -64,6 +70,7 @@ import org.springframework.web.util.UrlPathHelper; import javax.sql.DataSource; +import java.util.Optional; @SpringBootConfiguration @Import(SearchConfiguration.class) @@ -254,4 +261,17 @@ public DynamicRegistrationService dynamicRegistrationService(DynamicRegistration public LockProvider lockProvider(DataSource dataSource) { return new JdbcTemplateLockProvider(JdbcTemplateLockProvider.Configuration.builder().withJdbcTemplate(new JdbcTemplate(dataSource)).usingDbTime().build()); } + + @Bean + public String getBeaconCronValue(BeaconConfigurationRepository repo) + { + Optional bc = repo.findById(1); + return bc.isPresent() ? bc.get().getSendCron() : "0 3 * * * *"; + } + + @Bean + public IBeaconDataService getBeaconDataService(@Value("${shibui.beacon.productName:ShibUi}") String productName, InfoEndpoint info, @Value("#{environment.TIERVERSION}") String tierVersion) { + BeaconDataServiceImpl result = new BeaconDataServiceImpl(productName, info); + return result; + } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/postprocessors/BeaconConfigurationStartupProcessing.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/postprocessors/BeaconConfigurationStartupProcessing.java new file mode 100644 index 000000000..ee7e1a1e9 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/postprocessors/BeaconConfigurationStartupProcessing.java @@ -0,0 +1,68 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration.postprocessors; + +import edu.internet2.tier.shibboleth.admin.ui.domain.BeaconConfiguration; +import edu.internet2.tier.shibboleth.admin.ui.repository.BeaconConfigurationRepository; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import javax.persistence.EntityExistsException; +import javax.transaction.Transactional; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; + +/** + * This approach for running logic waits until after the Spring context has been initialized. + * We will always use an ID of 1 for the beaconConfiguration, so if the data doesn't exist, we can try to write the data. If the + * data exists, we don't need to do anything. + * + * Because multiple instances could be starting at the same time, we must account for a save "losing" to another entity - that's ok + * and if we get an exception because the entity with the id already exists, we can just bail out. + */ +@Component +public class BeaconConfigurationStartupProcessing { + @Autowired + BeaconConfigurationRepository beaconRepo; + + @EventListener + @Transactional + public void onApplicationEvent(ContextRefreshedEvent event) { + // If there is nothing in the db, create the entry to use going forward + if (beaconRepo.count() == 0) { + String appId = event.getApplicationContext().getEnvironment().getProperty("shibui.beacon.installationID"); + BeaconConfiguration bc = new BeaconConfiguration(); + bc.setInstallationId(StringUtils.isBlank(appId) ? UUID.randomUUID().toString() : appId); + + String cronDef = event.getApplicationContext().getEnvironment().getProperty("shibui.beacon.send.cron"); + + // If not set, set a random time between 12AM-4AM + if (StringUtils.isBlank(cronDef)) { + int randomMin = ThreadLocalRandom.current().nextInt(0, 60); + int randomHour = ThreadLocalRandom.current().nextInt(0, 5); + cronDef = "" + randomMin + " " + randomHour + " * * * *"; + } + + bc.setSendCron(cronDef); + + try { + beaconRepo.save(bc); + } + catch (EntityExistsException ignore) { + // Race between startup instances - as long as one of them won... + } + } + // if the entry exists, check that the cron timing hasn't changed + else { + String cronDef = event.getApplicationContext().getEnvironment().getProperty("shibui.beacon.send.cron"); + if (StringUtils.isNotBlank(cronDef)) { + BeaconConfiguration bc = beaconRepo.getReferenceById(1); + if (StringUtils.isNotBlank(cronDef) && !cronDef.equals(bc.getSendCron())) { + bc.setSendCron(cronDef); + beaconRepo.save(bc); + } + } + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/RootUiViewController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/RootUiViewController.java index 69fd53158..5883bc349 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/RootUiViewController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/RootUiViewController.java @@ -3,6 +3,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.info.InfoEndpoint; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -18,6 +19,7 @@ @Controller public class RootUiViewController { + @Autowired HealthEndpoint healthEndpoint; @Autowired InfoEndpoint infoEndpoint; @@ -46,4 +48,9 @@ public void indexHtml(HttpServletRequest request, HttpServletResponse response) writer.write(content.getBytes()); } } + + @GetMapping(value = "/health") + public ResponseEntity getHealth() { + return ResponseEntity.ok(healthEndpoint.health()); + } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/BeaconConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/BeaconConfiguration.java new file mode 100644 index 000000000..93d76fdc3 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/BeaconConfiguration.java @@ -0,0 +1,24 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import lombok.Data; + +import javax.persistence.Entity; +import javax.persistence.Id; + +@Data +@Entity +public class BeaconConfiguration { + @Id + private int id = 1; + + private String installationId; + + private String sendCron; + + /** + * enforce that id will only ever be 1 + */ + public void setId(int ignored) { + this.id = 1; + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/BeaconConfigurationRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/BeaconConfigurationRepository.java new file mode 100644 index 000000000..346a6566e --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/BeaconConfigurationRepository.java @@ -0,0 +1,8 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository; + +import edu.internet2.tier.shibboleth.admin.ui.domain.BeaconConfiguration; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BeaconConfigurationRepository extends JpaRepository { + +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/scheduled/BeaconReportingTask.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/scheduled/BeaconReportingTask.java new file mode 100644 index 000000000..c87d7017c --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/scheduled/BeaconReportingTask.java @@ -0,0 +1,25 @@ +package edu.internet2.tier.shibboleth.admin.ui.scheduled; + +import edu.internet2.tier.shibboleth.admin.ui.service.IBeaconDataService; +import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.transaction.annotation.Transactional; + +@Configuration +@ConditionalOnProperty(name = "shibui.beacon.enabled", matchIfMissing=true) +@EnableSchedulerLock(defaultLockAtMostFor = "${shibui.maxTask.lockTime:30m}") +public class BeaconReportingTask { + @Autowired + IBeaconDataService dataService; + + @Scheduled(cron="#{@getBeaconCronValue}") + @SchedulerLock(name = "generateEntityDescriptorFiles") + @Transactional(readOnly = true) + public void generateEntityDescriptorFiles() { + + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/BeaconDataServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/BeaconDataServiceImpl.java new file mode 100644 index 000000000..b9f97ea9a --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/BeaconDataServiceImpl.java @@ -0,0 +1,33 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.internet2.tier.shibboleth.admin.ui.beacon.BeaconDetail; +import lombok.SneakyThrows; +import org.springframework.boot.actuate.info.InfoEndpoint; + +import java.util.Map; + +public class BeaconDataServiceImpl implements IBeaconDataService { + private ObjectMapper mapper; + private String productName; + private String version; + + public BeaconDataServiceImpl(String productName, InfoEndpoint info) { + mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // skip any null values + + this.productName = productName; + version = info.info().get("build") == null ? "unknown" : ((Map)info.info().get("build")).get("version").toString(); + } + + @Override + @SneakyThrows + public String getBeaconData() throws JsonProcessingException { + BeaconDetail detail = new BeaconDetail(); + detail.setTbProduct(productName); + detail.setTbProductVersion(version); + return mapper.writeValueAsString(detail); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/IBeaconDataService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/IBeaconDataService.java new file mode 100644 index 000000000..4509b0584 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/IBeaconDataService.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import com.fasterxml.jackson.core.JsonProcessingException; + +public interface IBeaconDataService { + String getBeaconData() throws JsonProcessingException; +} \ No newline at end of file diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 1fd9b2b28..ac76ebb36 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -121,7 +121,9 @@ shibui.pac4j-enabled=false #This property must be set to true in order to enable posting stats to beacon endpoint. Furthermore, appropriate #environment variables must be set for beacon publisher to be used (the ones that are set when running shib-ui in #docker container -shibui.beacon-enabled=true +shibui.beacon.enabled=true +shibui.beacon.productName=ShibUi +shibui.beacon.installationID=UNICON-SHIBUI-TESTING ### Swagger/Springdoc patterns springdoc.use-management-port=true @@ -129,7 +131,7 @@ springdoc.swagger-ui.tagsSorter: alpha springdoc.writer-with-order-by-keys: true springdoc.pathsToMatch=/entities, /api/** # This property enables the openapi and swagger-ui endpoints to be exposed beneath the actuator base path. -management.endpoints.web.exposure.include=openapi, swagger-ui, info +management.endpoints.web.exposure.include=openapi, swagger-ui, info, health management.server.port=9090 management.endpoints.web.cors.allowed-origins=* management.endpoints.web.cors.allowed-headers=* diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy index 345b0e2b7..a04043631 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy @@ -24,6 +24,8 @@ import org.opensaml.saml.metadata.resolver.impl.ResourceBackedMetadataResolver import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.actuate.info.InfoContributor +import org.springframework.boot.actuate.info.InfoEndpoint import org.springframework.boot.SpringBootConfiguration import org.springframework.boot.web.client.RestTemplateBuilder import org.springframework.context.annotation.Bean @@ -134,4 +136,9 @@ class TestConfiguration { return it } } + + @Bean + public InfoEndpoint getInfoEndpoint() { + return new InfoEndpoint(new ArrayList()); + } } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/postprocessors/BeaconConfigurationStartupProcessingTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/postprocessors/BeaconConfigurationStartupProcessingTests.groovy new file mode 100644 index 000000000..ed14ab8a2 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/postprocessors/BeaconConfigurationStartupProcessingTests.groovy @@ -0,0 +1,39 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration.postprocessors + +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest +import edu.internet2.tier.shibboleth.admin.ui.domain.BeaconConfiguration +import edu.internet2.tier.shibboleth.admin.ui.repository.BeaconConfigurationRepository +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.event.ContextRefreshedEvent +import org.springframework.core.env.Environment + +import static org.mockito.Mockito.mock +import static org.mockito.Mockito.when + +class BeaconConfigurationStartupProcessingTests extends AbstractBaseDataJpaTest { + @Autowired + BeaconConfigurationRepository repo + + def 'test'() { + when: + def event = mock(ContextRefreshedEvent) + def appContext = mock(ApplicationContext) + def env = mock(Environment) + when(event.getApplicationContext()).thenReturn(appContext) + when(appContext.getEnvironment()).thenReturn(env) + when(env.getProperty("shibui.beacon.installationID")).thenReturn(null) + when(env.getProperty("shibui.beacon.send.cron")).thenReturn(null) + + BeaconConfigurationStartupProcessing testObject = new BeaconConfigurationStartupProcessing().with { + it.beaconRepo = repo + it + } + testObject.onApplicationEvent(event) + + then: + BeaconConfiguration bc = repo.getReferenceById(1) + bc.getInstallationId() != null + bc.getSendCron() != null + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy index 3fb344313..c9bacb09a 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy @@ -11,7 +11,6 @@ import edu.internet2.tier.shibboleth.admin.ui.security.model.Group import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership import edu.internet2.tier.shibboleth.admin.ui.security.model.Role import edu.internet2.tier.shibboleth.admin.ui.security.model.User -import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorVersionService import edu.internet2.tier.shibboleth.admin.ui.service.EntityService import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator @@ -62,8 +61,6 @@ class EntitiesControllerIntegrationTests extends AbstractBaseDataJpaTest { @Subject def controller - EntityDescriptorVersionService versionService = Mock() - @Transactional def setup() { openSamlObjects.init()