diff --git a/backend/src/main/app-resources/default.yml b/backend/src/main/app-resources/default.yml index 5ba247176..9ea447808 100644 --- a/backend/src/main/app-resources/default.yml +++ b/backend/src/main/app-resources/default.yml @@ -24,14 +24,14 @@ ## 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 +## 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 (random preferred) # beacon: # enabled: ture # installationID: [user-defined value] # urls: http://collector.testbed.tier.internet2.edu:5001 # productName: ShibUI # send: -# cron: 0 4 * * * * +# cron: 0 59 3 * * ? # pac4j-enabled: true # pac4j: 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 66ad8db9d..62c02f25a 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 @@ -4,6 +4,7 @@ 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.BeaconEventRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.FilterRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; @@ -45,16 +46,16 @@ import edu.internet2.tier.shibboleth.admin.util.LuceneUtility; import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions; import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; 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.SpringBootConfiguration; import org.springframework.boot.actuate.health.HealthEndpoint; 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; import org.springframework.boot.web.client.RestTemplateBuilder; @@ -68,7 +69,6 @@ import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; @@ -83,11 +83,13 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.Random; @SpringBootConfiguration @Import(SearchConfiguration.class) @ComponentScan(basePackages = "{ edu.internet2.tier.shibboleth.admin.ui.service }") @EnableConfigurationProperties({CustomPropertiesConfiguration.class, ShibUIConfiguration.class}) +@Slf4j public class CoreShibUiConfiguration { @Bean public OpenSamlObjects openSamlObjects() { @@ -275,18 +277,39 @@ public LockProvider lockProvider(DataSource dataSource) { } @Bean - public String getBeaconCronValue(BeaconConfigurationRepository repo) + public String getBeaconCronValue(BeaconConfigurationRepository repo, @Value("${shibui.beacon.send.cron:nodefault}") String valueFromConfig) { - Optional bc = repo.findById(1); - return bc.isPresent() ? bc.get().getSendCron() : "0 3 * * * *"; + Optional obc = repo.findById(1); + BeaconConfiguration bc; + if (obc.isEmpty()) { + //set random cron in db + BeaconConfiguration newbc = new BeaconConfiguration(); + Random rand = new Random(); + String cron = "0 " + rand.nextInt(60) + " " + rand.nextInt(4) + " * * ?"; + if (valueFromConfig.endsWith("?")) { + cron = valueFromConfig; + } + newbc.setSendCron(cron); + try { + repo.save(newbc); + } + catch(Exception e) { + log.debug("Error trying to create new BEACON CRON - should be ok"); + } + bc = repo.findById(1).get(); + } else { + bc = obc.get(); + } + log.info("Scheduling beacon cron: {}", bc.getSendCron()); + return bc.getSendCron(); } @Bean public IBeaconDataService getBeaconDataService(@Value("${shibui.beacon.productName:ShibUi}") String productName, InfoEndpoint info, @Value("#{environment.TIERVERSION}") String tierVersion, EntityDescriptorRepository entityDescriptorRepository, MetadataResolverRepository metadataResolverRepository, FilterRepository filterRepository, GroupsRepository groupsRepository, RoleRepository roleRepository, BeaconConfigurationRepository beaconConfigurationRepository, - UserService userService, HealthEndpoint healthEndpoint) { - BeaconDataServiceImpl result = new BeaconDataServiceImpl(productName, info, tierVersion, entityDescriptorRepository, metadataResolverRepository, filterRepository, groupsRepository, roleRepository, beaconConfigurationRepository, userService, healthEndpoint); + UserService userService, HealthEndpoint healthEndpoint, BeaconEventRepository beaconEventRepository) { + BeaconDataServiceImpl result = new BeaconDataServiceImpl(productName, info, tierVersion, entityDescriptorRepository, metadataResolverRepository, filterRepository, groupsRepository, roleRepository, beaconConfigurationRepository, userService, healthEndpoint, beaconEventRepository); return result; } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/SpringSecurityConfig.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/SpringSecurityConfig.java index 76b3299af..5b2e5a98a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/SpringSecurityConfig.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/SpringSecurityConfig.java @@ -110,7 +110,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf((csrf) -> csrf.csrfTokenRequestHandler(requestHandler)); http .authorizeHttpRequests() - .requestMatchers("/unsecured/**/*","/entities/**/*","/actuator/**").permitAll() + .requestMatchers("/unsecured/**/*","/entities/**/*","/actuator/**", "/api/beacon/send").permitAll() .anyRequest().hasAnyRole(acceptedAuthenticationRoles) .and().exceptionHandling().accessDeniedHandler((request, response, accessDeniedException) -> response.sendRedirect("/unsecured/error.html")) .and().authenticationProvider(new SimpleAuthenticationProvider(adminUserService())).formLogin() diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java index eea15896a..2d22a172d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java @@ -1,5 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.controller; +import edu.internet2.tier.shibboleth.admin.ui.domain.beacon.BeaconEvent; +import edu.internet2.tier.shibboleth.admin.ui.domain.beacon.BeaconEventType; import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; @@ -11,6 +13,7 @@ import edu.internet2.tier.shibboleth.admin.ui.service.DynamicRegistrationService; import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; import edu.internet2.tier.shibboleth.admin.ui.service.FilterService; +import edu.internet2.tier.shibboleth.admin.ui.service.IBeaconDataService; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tags; @@ -40,6 +43,9 @@ public class ActivateController { @Autowired private MetadataResolverService metadataResolverService; + + @Autowired + private IBeaconDataService beaconDataService; @PatchMapping(path = "/DynamicRegistration/{resourceId}/{mode}") @Transactional @@ -69,6 +75,9 @@ public ResponseEntity enableEntityDescriptor(@PathVariable String resourceId, public ResponseEntity enableFilter(@PathVariable String metadataResolverId, @PathVariable String resourceId, @PathVariable String mode) throws PersistentEntityNotFound, ForbiddenException, ScriptException { boolean status = "enable".equalsIgnoreCase(mode); MetadataFilter persistedFilter = filterService.updateFilterEnabledStatus(metadataResolverId, resourceId, status); + if (status) { + beaconDataService.addBeaconEvent(new BeaconEvent(BeaconEventType.METADATA_FILTER_ENABLED)); + } return ResponseEntity.ok(persistedFilter); } @@ -77,6 +86,9 @@ public ResponseEntity enableFilter(@PathVariable String metadataResolverId, @ public ResponseEntity enableProvider(@PathVariable String resourceId, @PathVariable String mode) throws PersistentEntityNotFound, ForbiddenException, MetadataFileNotFoundException, InitializationException { boolean status = "enable".equalsIgnoreCase(mode); MetadataResolver metadataResolver = metadataResolverService.updateMetadataResolverEnabledStatus(resourceId, status); + if (status) { + beaconDataService.addBeaconEvent(new BeaconEvent(BeaconEventType.METADATA_PROVIDER_ENABLED)); + } return ResponseEntity.ok(metadataResolver); } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/BeaconController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/BeaconController.java index e41f17f39..9b047142b 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/BeaconController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/BeaconController.java @@ -26,7 +26,8 @@ public ResponseEntity getDetail() throws JsonProcessingException { return ResponseEntity.ok(service.getBeaconData()); } - @PostMapping("/send") + // This should be a POST, but to make it easier to hit, this is a GET + @GetMapping("/send") public ResponseEntity forceSendBeaconData() { beaconReporter.sendBeaconData(); return ResponseEntity.ok("Manual push of beacon data completed"); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java index a6430fa76..530d0af97 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java @@ -1,5 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.controller; +import edu.internet2.tier.shibboleth.admin.ui.domain.beacon.BeaconEvent; +import edu.internet2.tier.shibboleth.admin.ui.domain.beacon.BeaconEventType; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.ITargetable; @@ -9,6 +11,7 @@ import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; 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.IBeaconDataService; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tags; @@ -50,6 +53,9 @@ public class MetadataFiltersController { private static final Supplier HTTP_400_BAD_REQUEST_EXCEPTION = () -> new HttpClientErrorException(BAD_REQUEST); private static final Supplier HTTP_404_CLIENT_ERROR_EXCEPTION = () -> new HttpClientErrorException(NOT_FOUND); + @Autowired + private IBeaconDataService beaconDataService; + @Autowired org.opensaml.saml.metadata.resolver.MetadataResolver chainingMetadataResolver; @@ -82,6 +88,7 @@ public ResponseEntity create(@PathVariable String metadataResolverId, @Reques reloadFiltersAndHandleScriptException(persistedMr.getResourceId()); MetadataFilter persistedFilter = newlyPersistedFilter(persistedMr.getMetadataFilters().stream(), newFilter.getResourceId()); + beaconDataService.addBeaconEvent(new BeaconEvent(BeaconEventType.METADATA_FILTER_CREATED)); return ResponseEntity.created(getResourceUriFor(persistedMr, newFilter.getResourceId())).body(persistedFilter); } @@ -110,6 +117,7 @@ public ResponseEntity delete(@PathVariable String metadataResolverId, @PathVa //TODO: do we need to reload filters here?!? //metadataResolverService.reloadFilters(persistedMr.getName()); + beaconDataService.addBeaconEvent(new BeaconEvent(BeaconEventType.METADATA_FILTER_REMOVED)); return ResponseEntity.noContent().build(); } @@ -189,8 +197,7 @@ private void reloadFiltersAndHandleScriptException(String resolverResourceId) { @PutMapping("/Filters/{resourceId}") @Transactional - public ResponseEntity update(@PathVariable String metadataResolverId, @PathVariable String resourceId, @RequestBody MetadataFilter updatedFilter) - throws ScriptException { + public ResponseEntity update(@PathVariable String metadataResolverId, @PathVariable String resourceId, @RequestBody MetadataFilter updatedFilter) throws ScriptException { MetadataResolver metadataResolver = findResolverOrThrowHttp404(metadataResolverId); //Now we operate directly on the filter attached to MetadataResolver, @@ -228,6 +235,7 @@ public ResponseEntity update(@PathVariable String metadataResolverId, @PathVa // TODO: do we need to reload filters here? We shouldnt throw a script exception here now - we check above... reloadFiltersAndHandleScriptException(metadataResolver.getResourceId()); + beaconDataService.addBeaconEvent(new BeaconEvent(BeaconEventType.METADATA_FILTER_MODIFIED)); return ResponseEntity.ok().body(persistedFilter); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java index 51d0d4753..ca065e7b2 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java @@ -1,12 +1,15 @@ package edu.internet2.tier.shibboleth.admin.ui.controller; import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; +import edu.internet2.tier.shibboleth.admin.ui.domain.beacon.BeaconEvent; +import edu.internet2.tier.shibboleth.admin.ui.domain.beacon.BeaconEventType; import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.MetadataResolverValidationService; import edu.internet2.tier.shibboleth.admin.ui.domain.versioning.Version; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import edu.internet2.tier.shibboleth.admin.ui.service.IBeaconDataService; import edu.internet2.tier.shibboleth.admin.ui.service.IndexWriterService; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverConverterService; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; @@ -52,27 +55,29 @@ @Slf4j @Tags(value = {@Tag(name = "metadata resolvers")}) public class MetadataResolversController { + @Autowired + private IBeaconDataService beaconDataService; @Autowired - MetadataResolverRepository resolverRepository; + org.opensaml.saml.metadata.resolver.MetadataResolver chainingMetadataResolver; @Autowired - MetadataResolverValidationService metadataResolverValidationService; + IndexWriterService indexWriterService; @Autowired - MetadataResolverService metadataResolverService; + MetadataResolverConverterService metadataResolverConverterService; @Autowired - MetadataResolversPositionOrderContainerService positionOrderContainerService; + MetadataResolverService metadataResolverService; @Autowired - IndexWriterService indexWriterService; + MetadataResolverValidationService metadataResolverValidationService; @Autowired - org.opensaml.saml.metadata.resolver.MetadataResolver chainingMetadataResolver; + MetadataResolversPositionOrderContainerService positionOrderContainerService; @Autowired - MetadataResolverConverterService metadataResolverConverterService; + MetadataResolverRepository resolverRepository; @Autowired MetadataResolverVersionService versionService; @@ -156,7 +161,8 @@ public ResponseEntity create(@RequestBody MetadataResolver newResolver) throw MetadataResolver persistedResolver = resolverRepository.save(newResolver); positionOrderContainerService.appendPositionOrderForNew(persistedResolver); doResolverInitialization(persistedResolver); - + beaconDataService.addBeaconEvent(new BeaconEvent(BeaconEventType.METADATA_PROVIDER_CREATED)); + newResolver.getMetadataFilters().forEach(f -> beaconDataService.addBeaconEvent(new BeaconEvent(BeaconEventType.METADATA_FILTER_CREATED))); return ResponseEntity.created(getResourceUriFor(persistedResolver)).body(persistedResolver); } @@ -168,8 +174,7 @@ public ResponseEntity update(@PathVariable String resourceId, @RequestBody Me return ResponseEntity.notFound().build(); } if (existingResolver.getVersion() != updatedResolver.getVersion()) { - log.info("Metadata Resolver version conflict. Latest resolver in database version: {}. Resolver version sent from UI: {}", - existingResolver.getVersion(), updatedResolver.getVersion()); + log.info("Metadata Resolver version conflict. Latest resolver in database version: {}. Resolver version sent from UI: {}", existingResolver.getVersion(), updatedResolver.getVersion()); return ResponseEntity.status(HttpStatus.CONFLICT).build(); } @@ -182,6 +187,7 @@ public ResponseEntity update(@PathVariable String resourceId, @RequestBody Me MetadataResolver persistedResolver = resolverRepository.save(updatedResolver); doResolverInitialization(persistedResolver); + beaconDataService.addBeaconEvent(new BeaconEvent(BeaconEventType.METADATA_PROVIDER_MODIFIED)); return ResponseEntity.ok(resolverRepository.findByResourceId(resourceId)); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/beacon/BeaconEvent.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/beacon/BeaconEvent.java new file mode 100644 index 000000000..c664bd0b8 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/beacon/BeaconEvent.java @@ -0,0 +1,30 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.beacon; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.Data; + +import java.util.Date; + +@Entity +@Data +public class BeaconEvent { + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + private Date eventTime; + + private String eventName; + + public BeaconEvent() { + eventTime = new Date(); + } + + public BeaconEvent(BeaconEventType eventType) { + eventTime = new Date(); + this.eventName = eventType.name(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/beacon/BeaconEventType.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/beacon/BeaconEventType.java new file mode 100644 index 000000000..ae108ae89 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/beacon/BeaconEventType.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.beacon; + +public enum BeaconEventType { + METADATA_SOURCE_MODIFIED, METADATA_SOURCE_REMOVED, METADATA_SOURCE_ENABLED, METADATA_SOURCE_CREATED, + METADATA_PROVIDER_MODIFIED, METADATA_PROVIDER_ENABLED, METADATA_PROVIDER_CREATED, + METADATA_FILTER_MODIFIED, METADATA_FILTER_REMOVED, METADATA_FILTER_ENABLED, METADATA_FILTER_CREATED +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/BeaconEventRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/BeaconEventRepository.java new file mode 100644 index 000000000..a6e2ee483 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/BeaconEventRepository.java @@ -0,0 +1,16 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository; + +import edu.internet2.tier.shibboleth.admin.ui.domain.beacon.BeaconEvent; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Date; +import java.util.List; + +public interface BeaconEventRepository extends JpaRepository { + + @Query(value = "SELECT be FROM BeaconEvent be WHERE be.eventTime >= :sinceDate") + List getEventsSince(@Param("sinceDate") Date sinceDate); + +} \ 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 index 5c25c8603..92708363a 100644 --- 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 @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.scheduled; import edu.internet2.tier.shibboleth.admin.ui.service.IBeaconDataService; +import lombok.extern.slf4j.Slf4j; import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; import org.springframework.beans.factory.annotation.Autowired; @@ -14,11 +15,13 @@ import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.List; @Configuration @ConditionalOnProperty(name = "shibui.beacon.enabled", matchIfMissing=true) @EnableSchedulerLock(defaultLockAtMostFor = "${shibui.maxTask.lockTime:30m}") +@Slf4j public class BeaconReportingTask { @Autowired IBeaconDataService dataService; @@ -33,16 +36,20 @@ public class BeaconReportingTask { public void sendBeaconData() { endpointUrls.forEach(url -> { try { + String dataOutput = dataService.getBeaconData(); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setRequestMethod("POST"); con.setRequestProperty("Content-Type", "application/json; utf-8"); con.setRequestProperty("Accept", "application/json"); con.setDoOutput(true); - try(OutputStream os = con.getOutputStream()){ - byte[] input = dataService.getBeaconData().getBytes("utf-8"); + try (OutputStream os = con.getOutputStream()) { + byte[] input = dataOutput.getBytes(StandardCharsets.UTF_8); os.write(input, 0, input.length); } - } catch (IOException e) { + log.info("TIER Beacon data sent to {} with response code: {}", url, con.getResponseCode()); + log.debug("TIER Beacon data load: {}", dataOutput); + } + catch (IOException e) { e.printStackTrace(); } }); 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 index 97cb5f06d..13657aae6 100644 --- 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 @@ -2,24 +2,30 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; -import edu.internet2.tier.shibboleth.admin.ui.service.beacon.BeaconDetail; -import edu.internet2.tier.shibboleth.admin.ui.service.beacon.ShibuiDetail; import edu.internet2.tier.shibboleth.admin.ui.domain.BeaconConfiguration; +import edu.internet2.tier.shibboleth.admin.ui.domain.beacon.BeaconEvent; +import edu.internet2.tier.shibboleth.admin.ui.domain.beacon.BeaconEventType; import edu.internet2.tier.shibboleth.admin.ui.repository.BeaconConfigurationRepository; +import edu.internet2.tier.shibboleth.admin.ui.repository.BeaconEventRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.FilterRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository; import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository; import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; +import edu.internet2.tier.shibboleth.admin.ui.service.beacon.BeaconDetail; +import edu.internet2.tier.shibboleth.admin.ui.service.beacon.ShibuiDetail; import lombok.SneakyThrows; +import org.apache.commons.lang.time.DateUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.info.InfoEndpoint; import java.util.Arrays; -import java.util.HashMap; +import java.util.Date; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; public class BeaconDataServiceImpl implements IBeaconDataService { private ObjectMapper mapper; @@ -35,11 +41,12 @@ public class BeaconDataServiceImpl implements IBeaconDataService { private RoleRepository roleRepository; private BeaconConfigurationRepository beaconConfigurationRepository; private UserService userService; + private BeaconEventRepository beaconEventRepository; public BeaconDataServiceImpl(String productName, InfoEndpoint info, String tierVersion, EntityDescriptorRepository entityDescriptorRepository, MetadataResolverRepository metadataResolverRepository, FilterRepository filterRepository, GroupsRepository groupsRepository, RoleRepository roleRepository, BeaconConfigurationRepository beaconConfigurationRepository, UserService userService, - HealthEndpoint health) { + HealthEndpoint health, BeaconEventRepository beaconEventRepository) { mapper = new ObjectMapper(); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // skip any null values @@ -56,16 +63,33 @@ public BeaconDataServiceImpl(String productName, InfoEndpoint info, String tierV this.roleRepository = roleRepository; this.beaconConfigurationRepository = beaconConfigurationRepository; this.userService = userService; + this.beaconEventRepository = beaconEventRepository; } @Override @SneakyThrows public String getBeaconData() { - BeaconDetail detail = new BeaconDetail().setTbProduct(productName).setTbProductVersion(version).setTbTIERRelease(tierVersion).setShibui(getShibuiDetailData()); - + BeaconDetail detail = new BeaconDetail().setTbProduct(productName).setTbProductVersion(version).setTbTIERRelease(tierVersion).setShibui(getShibuiDetailData()).setBeaconEvents(getBeaconEvents()); return mapper.writeValueAsString(detail); } + @Override + public Map getBeaconEvents() { + List results = beaconEventRepository.getEventsSince(DateUtils.addDays(new Date(), -1)); + Map counts = results.stream().collect(Collectors.groupingBy(e -> e.getEventName(), Collectors.counting())); + return counts; + } + + @Override + public void addBeaconEvent(BeaconEvent event) { + beaconEventRepository.save(event); + } + + @Override + public void addEntityDescEnabledEvent() { + addBeaconEvent(new BeaconEvent(BeaconEventType.METADATA_SOURCE_ENABLED)); + } + private ShibuiDetail getShibuiDetailData() { BeaconConfiguration configuration = beaconConfigurationRepository.getReferenceById(1); 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 index 4509b0584..1b0615945 100644 --- 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 @@ -1,7 +1,16 @@ package edu.internet2.tier.shibboleth.admin.ui.service; import com.fasterxml.jackson.core.JsonProcessingException; +import edu.internet2.tier.shibboleth.admin.ui.domain.beacon.BeaconEvent; + +import java.util.Map; public interface IBeaconDataService { String getBeaconData() throws JsonProcessingException; + + Map getBeaconEvents(); + + void addBeaconEvent(BeaconEvent event); + + void addEntityDescEnabledEvent(); } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java index 695b03036..ab40e84fc 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java @@ -10,6 +10,8 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.X509Data; import edu.internet2.tier.shibboleth.admin.ui.domain.XSBoolean; import edu.internet2.tier.shibboleth.admin.ui.domain.XSInteger; +import edu.internet2.tier.shibboleth.admin.ui.domain.beacon.BeaconEvent; +import edu.internet2.tier.shibboleth.admin.ui.domain.beacon.BeaconEventType; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.AssertionConsumerServiceRepresentation; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ContactRepresentation; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; @@ -73,6 +75,9 @@ @Slf4j @Service public class JPAEntityDescriptorServiceImpl implements EntityDescriptorService { + @Autowired + private IBeaconDataService beaconDataService; + @Autowired private EntityDescriptorRepository entityDescriptorRepository; @@ -261,6 +266,10 @@ public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation e ownershipRepository.deleteEntriesForOwnedObject(ed); ownershipRepository.save(new Ownership(userService.getCurrentUserGroup(), ed)); + if(ed.isServiceEnabled()) { + beaconDataService.addEntityDescEnabledEvent(); + } + beaconDataService.addBeaconEvent(new BeaconEvent(BeaconEventType.METADATA_SOURCE_CREATED)); return createRepresentationFromDescriptor(entityDescriptorRepository.save(ed)); } @@ -283,6 +292,7 @@ public EntityDescriptorRepresentation createNewEntityDescriptorFromXMLOrigin(Ent } EntityDescriptor savedEntity = entityDescriptorRepository.save(ed); + beaconDataService.addBeaconEvent(new BeaconEvent(BeaconEventType.METADATA_SOURCE_CREATED)); return createRepresentationFromDescriptor(savedEntity); } @@ -477,7 +487,7 @@ public void delete(String resourceId) throws ForbiddenException, PersistentEntit } ownershipRepository.deleteEntriesForOwnedObject(ed); entityDescriptorRepository.delete(ed); - + beaconDataService.addBeaconEvent(new BeaconEvent(BeaconEventType.METADATA_SOURCE_REMOVED)); } private EntityDescriptorProtocol determineEntityDescriptorProtocol(EntityDescriptor ed) { @@ -649,6 +659,8 @@ public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRe } validateEntityIdAndACSUrls(edRep); + boolean updateEnablesEd = !existingEd.isServiceEnabled() && edRep.isServiceEnabled(); + updateDescriptorFromRepresentation(existingEd, edRep); existingEd = entityDescriptorRepository.save(existingEd); ownershipRepository.deleteEntriesForOwnedObject(existingEd); @@ -656,6 +668,10 @@ public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRe public String getOwnerId() { return edRep.getIdOfOwner(); } public OwnerType getOwnerType() { return OwnerType.GROUP; } }, existingEd)); + if (updateEnablesEd) { + beaconDataService.addEntityDescEnabledEvent(); + } + beaconDataService.addBeaconEvent(new BeaconEvent(BeaconEventType.METADATA_SOURCE_MODIFIED)); return createRepresentationFromDescriptor(existingEd); } @@ -689,6 +705,7 @@ public EntityDescriptorRepresentation updateEntityDescriptorEnabledStatus(String ed.setServiceEnabled(enabled); if (enabled == true) { ed.setApproved(true); + beaconDataService.addEntityDescEnabledEvent(); } ed = entityDescriptorRepository.save(ed); return createRepresentationFromDescriptor(ed); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java index 03a72602e..ddfeef564 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java @@ -28,6 +28,9 @@ */ @Service public class JPAFilterServiceImpl implements FilterService { + @Autowired + IBeaconDataService beaconDataService; + @Autowired EntityDescriptorService entityDescriptorService; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/beacon/BeaconDetail.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/beacon/BeaconDetail.java index bef94540f..78503cf50 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/beacon/BeaconDetail.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/beacon/BeaconDetail.java @@ -1,8 +1,12 @@ package edu.internet2.tier.shibboleth.admin.ui.service.beacon; +import edu.internet2.tier.shibboleth.admin.ui.domain.beacon.BeaconEvent; import lombok.Data; import lombok.experimental.Accessors; +import java.util.List; +import java.util.Map; + @Data @Accessors(chain = true) public class BeaconDetail { @@ -16,4 +20,6 @@ public class BeaconDetail { private String tbMaintainer = "Unicon"; private ShibuiDetail shibui; + + private Map beaconEvents; } \ No newline at end of file diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 565121547..b8bc0b840 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -125,6 +125,7 @@ shibui.beacon.enabled=true shibui.beacon.productName=ShibUi shibui.beacon.installationID=UNICON-SHIBUI-TESTING shibui.beacon.url=http://collector.testbed.tier.internet2.edu:5001 +#shibui.beacon.send.cron=0 59 3 * * ? ### Swagger/Springdoc patterns springdoc.use-management-port=true diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/BaseDataJpaTestConfiguration.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/BaseDataJpaTestConfiguration.groovy index 0244c32a3..0a3ad3451 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/BaseDataJpaTestConfiguration.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/BaseDataJpaTestConfiguration.groovy @@ -10,7 +10,10 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.StringTrimModule import edu.internet2.tier.shibboleth.admin.ui.controller.support.RestControllersSupport 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.BeaconEventRepository import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository +import edu.internet2.tier.shibboleth.admin.ui.repository.FilterRepository import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.GroupUpdatedEntityListener import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.UserUpdatedEntityListener @@ -19,23 +22,37 @@ import edu.internet2.tier.shibboleth.admin.ui.security.permission.ShibUiPermissi import edu.internet2.tier.shibboleth.admin.ui.security.repository.DynamicRegistrationInfoRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceForTesting import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl 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.DirectoryService import edu.internet2.tier.shibboleth.admin.ui.service.DirectoryServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.service.IBeaconDataService import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.AttributeUtility import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions import org.springframework.boot.SpringBootConfiguration +import org.springframework.boot.actuate.endpoint.annotation.Selector +import org.springframework.boot.actuate.health.DefaultHealthContributorRegistry +import org.springframework.boot.actuate.health.Health +import org.springframework.boot.actuate.health.HealthComponent +import org.springframework.boot.actuate.health.HealthEndpoint +import org.springframework.boot.actuate.health.HealthEndpointGroup +import org.springframework.boot.actuate.health.HealthEndpointGroups +import org.springframework.boot.actuate.health.Status +import org.springframework.boot.actuate.info.InfoContributor +import org.springframework.boot.actuate.info.InfoEndpoint import org.springframework.context.annotation.Bean import org.springframework.context.annotation.ComponentScan import org.springframework.context.annotation.Import import org.springframework.context.annotation.Primary import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder +import java.time.Duration import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -116,7 +133,7 @@ class BaseDataJpaTestConfiguration { } @Bean - public IShibUiPermissionEvaluator shibUiPermissionEvaluator(DynamicRegistrationInfoRepository driRepo, EntityDescriptorRepository entityDescriptorRepository, UserService userService) { + IShibUiPermissionEvaluator shibUiPermissionEvaluator(DynamicRegistrationInfoRepository driRepo, EntityDescriptorRepository entityDescriptorRepository, UserService userService) { return new ShibUiPermissionDelegate(driRepo, entityDescriptorRepository, userService); } @@ -132,4 +149,33 @@ class BaseDataJpaTestConfiguration { it } } + + @Bean + InfoEndpoint getInfoEndpoint() { + return new InfoEndpoint(new ArrayList()); + } + + @Bean + HealthEndpoint getHealthEndpoint() { + return new HealthEndpoint(new DefaultHealthContributorRegistry(), new HealthEndpointGroups() { + @Override HealthEndpointGroup getPrimary() { return null } + + @Override Set getNames() { return null } + + @Override HealthEndpointGroup get(String name) { return null } + }, Duration.ZERO) { + @Override + HealthComponent healthForPath(@Selector(match = Selector.Match.ALL_REMAINING) String... path) { + return new Health(new Status(""), new HashMap()); + } + } + } + + @Bean + IBeaconDataService getBeaconDataService(InfoEndpoint info, EntityDescriptorRepository entityDescriptorRepository, MetadataResolverRepository metadataResolverRepository, FilterRepository filterRepository, + GroupsRepository groupsRepository, RoleRepository roleRepository, BeaconConfigurationRepository beaconConfigurationRepository, + UserService userService, HealthEndpoint healthEndpoint, BeaconEventRepository beaconEventRepository) { + BeaconDataServiceImpl result = new BeaconDataServiceImpl("SHIBUI_UNIT_TESTS", info, "NA", entityDescriptorRepository, metadataResolverRepository, filterRepository, groupsRepository, roleRepository, beaconConfigurationRepository, userService, healthEndpoint, beaconEventRepository); + return result; + } } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateControllerTests.groovy index 895d2c10c..a5fefdc2c 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateControllerTests.groovy @@ -3,6 +3,7 @@ package edu.internet2.tier.shibboleth.admin.ui.controller import com.fasterxml.jackson.databind.ObjectMapper import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.domain.beacon.BeaconEventType import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.DynamicRegistrationInfo import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.GrantType import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException @@ -16,6 +17,7 @@ import edu.internet2.tier.shibboleth.admin.ui.security.repository.DynamicRegistr import edu.internet2.tier.shibboleth.admin.ui.service.DynamicRegistrationService import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService import edu.internet2.tier.shibboleth.admin.ui.service.EntityService +import edu.internet2.tier.shibboleth.admin.ui.service.IBeaconDataService import edu.internet2.tier.shibboleth.admin.ui.util.WithMockAdmin import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils import org.springframework.beans.factory.annotation.Autowired @@ -34,6 +36,9 @@ class ActivateControllerTests extends AbstractBaseDataJpaTest { @Subject def controller + @Autowired + private IBeaconDataService service + @Autowired ObjectMapper mapper @@ -114,9 +119,9 @@ class ActivateControllerTests extends AbstractBaseDataJpaTest { entityManager.clear() EntityDescriptor entityDescriptor = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: false, idOfOwner: 'AAA') - entityDescriptor = entityDescriptorRepository.save(entityDescriptor) + def edRep = entDescriptorService.createNew(entityDescriptor) - defaultEntityDescriptorResourceId = entityDescriptor.getResourceId() + defaultEntityDescriptorResourceId = edRep.id def dynReg = new DynamicRegistrationInfo(resourceId: 'uuid-1', enabled: false, idOfOwner: "AAA", applicationType: 'apptype', approved: true, contacts: 'contacts', jwks: 'jwks', logoUri: 'logouri', policyUri: 'policyuri', @@ -145,6 +150,8 @@ class ActivateControllerTests extends AbstractBaseDataJpaTest { catch (Exception e) { e instanceof ForbiddenException } + + service.getBeaconEvents().get(BeaconEventType.METADATA_SOURCE_ENABLED.name()) == null } @WithMockUser(value = "DUser", roles = ["USER"]) @@ -156,10 +163,17 @@ class ActivateControllerTests extends AbstractBaseDataJpaTest { catch (Exception e) { e instanceof ForbiddenException } + + service.getBeaconEvents().get(BeaconEventType.METADATA_SOURCE_ENABLED.name()) == null } @WithMockUser(value = "DUser", roles = ["USER"]) def 'non-owner group cannot activate entity descriptor'() { + given: + EntityDescriptor ed = entityDescriptorRepository.findByResourceId(defaultEntityDescriptorResourceId) + ed.setIdOfOwner('AAA') + entityDescriptorRepository.saveAndFlush(ed) + expect: try { mockMvc.perform(patch("/api/activate/entityDescriptor/" + defaultEntityDescriptorResourceId + "/enable")) @@ -167,6 +181,8 @@ class ActivateControllerTests extends AbstractBaseDataJpaTest { catch (Exception e) { e instanceof ForbiddenException } + + service.getBeaconEvents().get(BeaconEventType.METADATA_SOURCE_ENABLED.name()) == null } // @WithMockAdmin @@ -196,6 +212,8 @@ class ActivateControllerTests extends AbstractBaseDataJpaTest { result.andExpect(status().isOk()) .andExpect(jsonPath("\$.id").value(defaultEntityDescriptorResourceId)) .andExpect(jsonPath("\$.serviceEnabled").value(true)) + + service.getBeaconEvents().get(BeaconEventType.METADATA_SOURCE_ENABLED.name()).longValue() == 1 } // @WithMockUser(value = "AUser", roles = ["USER"]) @@ -231,5 +249,7 @@ class ActivateControllerTests extends AbstractBaseDataJpaTest { result.andExpect(status().isOk()) .andExpect(jsonPath("\$.id").value('uuid-2')) .andExpect(jsonPath("\$.serviceEnabled").value(true)) + + service.getBeaconEvents().get(BeaconEventType.METADATA_SOURCE_ENABLED.name()).longValue() == 1 } } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy index 834ad180e..12e659538 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.configuration.JsonSchemaComponentsConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.domain.beacon.BeaconEventType import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.AssertionConsumerServiceRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException @@ -21,6 +22,7 @@ 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.IBeaconDataService import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers @@ -40,6 +42,7 @@ import org.springframework.test.web.servlet.result.MockMvcResultHandlers import org.springframework.test.web.servlet.setup.MockMvcBuilders import org.springframework.transaction.annotation.Transactional import org.springframework.web.client.RestTemplate +import org.springframework.web.util.NestedServletException import spock.lang.Subject import java.nio.charset.StandardCharsets @@ -58,6 +61,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @Import([JsonSchemaComponentsConfiguration.class, EDCConfig.class]) class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { + @Autowired + private IBeaconDataService service + @Autowired EntityDescriptorSchemaValidatingControllerAdvice controllerAdvice @@ -281,6 +287,8 @@ class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { } catch (ServletException expected) { expected.getCause() instanceof InvalidPatternMatchException } + + service.getBeaconEvents().get(BeaconEventType.METADATA_SOURCE_CREATED.name()) == null } @WithMockUser(value = "someUser", roles = ["USER"]) @@ -311,6 +319,8 @@ class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { .andExpect(jsonPath("\$.serviceEnabled").value(false)) .andExpect(jsonPath("\$.idOfOwner").value("testingGroupBBB")) + service.getBeaconEvents().get(BeaconEventType.METADATA_SOURCE_CREATED.name()).longValue() == 1 + when: "ACS url is bad" expectedEntityId = 'https://shib.org/blah/blah/again' edRep = new EntityDescriptorRepresentation() @@ -333,6 +343,9 @@ class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { } catch (ServletException expected) { expected.getCause() instanceof InvalidPatternMatchException } + + // Still just the one + service.getBeaconEvents().get(BeaconEventType.METADATA_SOURCE_CREATED.name()).longValue() == 1 } @WithMockAdmin @@ -364,6 +377,8 @@ class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { .andExpect(jsonPath("\$.entityId").value("https://shib")) .andExpect(jsonPath("\$.serviceEnabled").value(true)) .andExpect(jsonPath("\$.idOfOwner").value("admingroup")) + + service.getBeaconEvents().get(BeaconEventType.METADATA_SOURCE_CREATED.name()).longValue() == 1 } @WithMockUser(value = "someUser", roles = ["USER"]) @@ -384,6 +399,8 @@ class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { catch (Exception e) { e instanceof ForbiddenException } + + service.getBeaconEvents().get(BeaconEventType.METADATA_SOURCE_ENABLED.name()) == null } @WithMockAdmin @@ -424,6 +441,9 @@ class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { catch (Exception e) { e instanceof ObjectIdExistsException } + + // because we created the test objects directly in the repository, there should be no beacon records + service.getBeaconEvents().get(BeaconEventType.METADATA_SOURCE_CREATED.name()) == null } @WithMockAdmin @@ -614,12 +634,15 @@ class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { .andExpect(jsonPath("\$.assertionConsumerServices[0].makeDefault").value(false)) .andExpect(jsonPath("\$.assertionConsumerServices[0].locationUrl").value("https://test.scaldingspoon.org/test1/acs")) + service.getBeaconEvents().get(BeaconEventType.METADATA_SOURCE_CREATED.name()).longValue() == 1 + try { mockMvc.perform(post("/api/EntityDescriptor").contentType(APPLICATION_XML).content(postedBody).param("spName", spName)) } catch (Exception e) { e instanceof ObjectIdExistsException } + service.getBeaconEvents().get(BeaconEventType.METADATA_SOURCE_CREATED.name()).longValue() == 1 } @WithMockAdmin @@ -659,6 +682,8 @@ class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { catch (Exception e) { e instanceof ObjectIdExistsException } + + service.getBeaconEvents().get(BeaconEventType.METADATA_SOURCE_CREATED.name()) == null } @WithMockAdmin @@ -685,6 +710,8 @@ class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { .andExpect(jsonPath("\$.serviceEnabled").value(false)) .andExpect(jsonPath("\$.idOfOwner").value("admingroup")) .andExpect(jsonPath("\$.serviceProviderName").value("newName")) + + service.getBeaconEvents().get(BeaconEventType.METADATA_SOURCE_MODIFIED.name()).longValue() == 1 } @WithMockUser(value = "someUser", roles = ["USER"]) @@ -710,6 +737,8 @@ class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { catch (Exception e) { e instanceof ForbiddenException } + + service.getBeaconEvents().get(BeaconEventType.METADATA_SOURCE_MODIFIED.name()) == null } @WithMockUser(value = "someUser", roles = ["USER"]) @@ -735,6 +764,8 @@ class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { catch (Exception e) { e instanceof ForbiddenException } + + service.getBeaconEvents().get(BeaconEventType.METADATA_SOURCE_MODIFIED.name()) == null } @WithMockAdmin @@ -758,6 +789,8 @@ class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { catch (Exception e) { e instanceof ConcurrentModificationException } + + service.getBeaconEvents().get(BeaconEventType.METADATA_SOURCE_MODIFIED.name()) == null } @WithMockAdmin @@ -796,6 +829,8 @@ class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { .andExpect(jsonPath(shortNameToOAuth + "defaultAcrValues").isArray()) .andExpect(jsonPath(shortNameToOAuth + "attributes.requireAuthTime").value(Boolean.FALSE)) .andExpect(jsonPath(shortNameToOAuth + "attributes.defaultMaxAge").value(Integer.valueOf(0))) + + service.getBeaconEvents().get(BeaconEventType.METADATA_SOURCE_CREATED.name()).longValue() == 1 } @WithMockAdmin @@ -852,6 +887,8 @@ class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { .andExpect(jsonPath(shortNameToOAuth + "defaultAcrValues").isArray()) .andExpect(jsonPath(shortNameToOAuth + "attributes.requireAuthTime").value(Boolean.FALSE)) .andExpect(jsonPath(shortNameToOAuth + "attributes.defaultMaxAge").value(Integer.valueOf(0))) + + service.getBeaconEvents().get(BeaconEventType.METADATA_SOURCE_CREATED.name()).longValue() == 1 } @SneakyThrows diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerIntegrationTests.groovy index 2e46238d4..6849d0448 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerIntegrationTests.groovy @@ -9,13 +9,16 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.EntitiesVersioningCo import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverValidationConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.PlaceholderResolverComponentsConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.TestMetadataResolverControllerConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.beacon.BeaconEventType import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.repository.BeaconEventRepository import edu.internet2.tier.shibboleth.admin.ui.repository.FilterRepository import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository 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.IBeaconDataService import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverConverterService import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator @@ -31,16 +34,12 @@ import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Import import org.springframework.context.annotation.Profile -import org.springframework.http.HttpEntity -import org.springframework.http.HttpHeaders import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter import org.springframework.test.context.ActiveProfiles import org.springframework.test.web.servlet.setup.MockMvcBuilders import org.springframework.transaction.annotation.EnableTransactionManagement import spock.lang.Subject -import spock.util.matcher.HamcrestMatchers -import javax.script.ScriptException import java.lang.reflect.UndeclaredThrowableException import static edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget.EntityAttributesFilterTargetType.CONDITION_SCRIPT @@ -81,6 +80,12 @@ class MetadataFiltersControllerIntegrationTests extends AbstractBaseDataJpaTest @Autowired MetadataResolverRepository metadataResolverRepository + @Autowired + private IBeaconDataService beaconDataService + + @Autowired + private BeaconEventRepository beaconEventRepository + @Autowired ObjectMapper mapper @@ -93,6 +98,15 @@ class MetadataFiltersControllerIntegrationTests extends AbstractBaseDataJpaTest def setup() { mockMvcMetaResolverFilterController = MockMvcBuilders.standaloneSetup(controller).build() mockMvcMetadataResolverController = MockMvcBuilders.standaloneSetup(metadataResolverController).setMessageConverters(new MappingJackson2HttpMessageConverter(mapper)).build() + generator = new TestObjectGenerator(attributeUtility, customPropertiesConfiguration) + mapper = new ObjectMapper() + mapper.enable(SerializationFeature.INDENT_OUTPUT) + mapper.registerModule(new JavaTimeModule()) + } + + def cleanup() { + metadataResolverRepository.deleteAll() + beaconEventRepository.deleteAll() } @WithMockAdmin @@ -115,6 +129,8 @@ class MetadataFiltersControllerIntegrationTests extends AbstractBaseDataJpaTest then: updatedResultFromPUT.andExpect(status().isOk()) + + beaconDataService.getBeaconEvents().get(BeaconEventType.METADATA_FILTER_MODIFIED.name()).longValue() == 1 } @WithMockAdmin @@ -154,6 +170,8 @@ class MetadataFiltersControllerIntegrationTests extends AbstractBaseDataJpaTest then: updatedResultFromPUT.andExpect(status().isOk()) + beaconDataService.getBeaconEvents().get(BeaconEventType.METADATA_FILTER_MODIFIED.name()).longValue() == 1 + when: 'Update the script filter with bad script does not persist' def filterToUpdate = jsonSlurper.parseText(getResultBody(updatedResultFromPUT)) filterToUpdate.singleValue = """ @@ -288,6 +306,8 @@ class MetadataFiltersControllerIntegrationTests extends AbstractBaseDataJpaTest it } + beaconDataService.getBeaconEvents().get(BeaconEventType.METADATA_FILTER_CREATED.name()) == null + when: def jsonBody = mapper.writeValueAsString(filter) @@ -308,6 +328,8 @@ class MetadataFiltersControllerIntegrationTests extends AbstractBaseDataJpaTest then: resolverResult.andExpect(status().isOk()) resolverResult.andExpect(jsonPath("\$.metadataFilters").isEmpty()) + + beaconDataService.getBeaconEvents().get(BeaconEventType.METADATA_FILTER_CREATED.name()) == null } @TestConfiguration diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy index 1107af074..48ecfc602 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.beacon.BeaconEventType import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver @@ -15,6 +16,7 @@ import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.FilterRepository import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository import edu.internet2.tier.shibboleth.admin.ui.service.FilterService +import edu.internet2.tier.shibboleth.admin.ui.service.IBeaconDataService import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator @@ -47,6 +49,9 @@ class MetadataFiltersControllerTests extends AbstractBaseDataJpaTest { @Autowired AttributeUtility attributeUtility + @Autowired + private IBeaconDataService beaconDataService + @Autowired CustomPropertiesConfiguration customPropertiesConfiguration @@ -77,6 +82,7 @@ class MetadataFiltersControllerTests extends AbstractBaseDataJpaTest { controller = new MetadataFiltersController ( repository: metadataResolverRepository, filterRepository: metadataFilterRepository, + beaconDataService: beaconDataService, groupService: groupService, userService: userService, metadataResolverService: new MetadataResolverService() { @@ -239,8 +245,8 @@ class MetadataFiltersControllerTests extends AbstractBaseDataJpaTest { then: def expectedJson = new JsonSlurper().parseText(updatedFilterJson) expectedJson << [version: updatedFilter.getVersion()] - result.andExpect(status().isOk()) - .andExpect(content().json(JsonOutput.toJson(expectedJson), true)) + result.andExpect(status().isOk()).andExpect(content().json(JsonOutput.toJson(expectedJson), true)) + beaconDataService.getBeaconEvents().get(BeaconEventType.METADATA_FILTER_MODIFIED.name()).longValue() == 1 where: filterType | _ @@ -276,6 +282,7 @@ class MetadataFiltersControllerTests extends AbstractBaseDataJpaTest { then: result.andExpect(status().is(409)) + beaconDataService.getBeaconEvents().get(BeaconEventType.METADATA_FILTER_MODIFIED.name()) == null } @TestConfiguration diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy index 77a6f2b7c..b3b5d29b3 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy @@ -8,6 +8,7 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverConf import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverConverterConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverValidationConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.PlaceholderResolverComponentsConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.beacon.BeaconEventType import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver @@ -20,6 +21,7 @@ import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolversPositi 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 +import edu.internet2.tier.shibboleth.admin.ui.service.IBeaconDataService import edu.internet2.tier.shibboleth.admin.ui.service.IndexWriterService import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverConverterService import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService @@ -54,6 +56,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration, PlaceholderResolverComponentsConfiguration, MRCILocalConfig]) class MetadataResolversControllerIntegrationTests extends AbstractBaseDataJpaTest { + @Autowired + IBeaconDataService beaconDataService + @Autowired AttributeUtility attributeUtility @@ -112,6 +117,7 @@ class MetadataResolversControllerIntegrationTests extends AbstractBaseDataJpaTes then: result.andExpect(status().isCreated()).andExpect(content().contentType(APPLICATION_JSON)) .andExpect(jsonPath("\$.name").value("HTTPMetadata")) + beaconDataService.getBeaconEvents().get(BeaconEventType.METADATA_PROVIDER_CREATED.name()).longValue() == 1 } @WithMockAdmin @@ -213,6 +219,7 @@ class MetadataResolversControllerIntegrationTests extends AbstractBaseDataJpaTes then: result.andExpect(status().isCreated()).andExpect(content().contentType(APPLICATION_JSON)) .andExpect(jsonPath("\$.name").value(expectedName)) + beaconDataService.getBeaconEvents().get(BeaconEventType.METADATA_PROVIDER_CREATED.name()).longValue() == 1 } @WithMockAdmin @@ -232,6 +239,8 @@ class MetadataResolversControllerIntegrationTests extends AbstractBaseDataJpaTes result.andExpect(status().isCreated()).andExpect(content().contentType(APPLICATION_JSON)) .andExpect(jsonPath("\$.['@type']").value(resolver.getType())) + beaconDataService.getBeaconEvents().get(BeaconEventType.METADATA_PROVIDER_CREATED.name()).longValue() == 1 + cleanup: if (sourceDirectory != null) { def tmpDirectory = new File(sourceDirectory) @@ -266,6 +275,7 @@ class MetadataResolversControllerIntegrationTests extends AbstractBaseDataJpaTes then: result.andExpect(status().isCreated()) + beaconDataService.getBeaconEvents().get(BeaconEventType.METADATA_PROVIDER_CREATED.name()).longValue() == 1 } @WithMockAdmin @@ -293,6 +303,8 @@ class MetadataResolversControllerIntegrationTests extends AbstractBaseDataJpaTes updatedResult.andExpect(status().isOk()).andExpect(content().contentType(APPLICATION_JSON)) .andExpect(jsonPath("\$.name").value('Updated Resolver Name')) + beaconDataService.getBeaconEvents().get(BeaconEventType.METADATA_PROVIDER_MODIFIED.name()).longValue() == 1 + cleanup: if (sourceDirectory != null) { def tmpDirectory = new File(sourceDirectory) @@ -339,6 +351,8 @@ class MetadataResolversControllerIntegrationTests extends AbstractBaseDataJpaTes then: updatedResult.andExpect(status().isConflict()) + + beaconDataService.getBeaconEvents().get(BeaconEventType.METADATA_PROVIDER_MODIFIED.name()) == null } @WithMockAdmin @@ -362,6 +376,9 @@ class MetadataResolversControllerIntegrationTests extends AbstractBaseDataJpaTes then: createdResolver.metadataFilters.size() == 1 createdResolver.metadataFilters[0] instanceof EntityAttributesFilter + + beaconDataService.getBeaconEvents().get(BeaconEventType.METADATA_PROVIDER_CREATED.name()).longValue() == 1 + beaconDataService.getBeaconEvents().get(BeaconEventType.METADATA_FILTER_CREATED.name()).longValue() == 1 } @WithMockAdmin @@ -386,6 +403,7 @@ class MetadataResolversControllerIntegrationTests extends AbstractBaseDataJpaTes then: updatedResultFromPUT == updatedResultFromGET + beaconDataService.getBeaconEvents().get(BeaconEventType.METADATA_PROVIDER_MODIFIED.name()).longValue() == 1 } @TestConfiguration