diff --git a/backend/build.gradle b/backend/build.gradle index 925499873..ee84e1673 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -154,8 +154,6 @@ dependencies { runtimeOnly "com.microsoft.sqlserver:mssql-jdbc:${project.'sqlserverVersion'}" //Pacj4 sub-project runtimeOnly project(':pac4j-module') - //Beacon - runtimeOnly project(':beacon:spring') // runtime libraries for later java versions runtimeOnly 'org.glassfish.jaxb:jaxb-runtime:2.3.1' // com.sun.xml.bind package 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/configuration/CoreShibUiConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java index d1c1e9a0b..66ad8db9d 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,8 +1,11 @@ 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.FilterRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolversPositionOrderContainerRepository; import edu.internet2.tier.shibboleth.admin.ui.scheduled.EntityDescriptorFilesScheduledTasks; @@ -19,6 +22,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 +33,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,20 +47,28 @@ 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.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; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Import; import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.core.io.Resource; import org.springframework.jdbc.core.JdbcTemplate; +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; @@ -64,6 +77,12 @@ import org.springframework.web.util.UrlPathHelper; import javax.sql.DataSource; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; @SpringBootConfiguration @Import(SearchConfiguration.class) @@ -254,4 +273,49 @@ 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, + 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); + return result; + } + + @Bean + public List beaconEndpointUrl(@Value("${shibui.beacon.url}") String urls) throws MalformedURLException { + List result = new ArrayList<>(); + for (String url : Arrays.asList(urls.split(","))) { + result.add(new URL(url)); + } + return result; + } + + @Bean + public AuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher applicationEventPublisher, UserService userService) { + return new RecordLoginHandler(applicationEventPublisher, userService); + } + + class RecordLoginHandler extends DefaultAuthenticationEventPublisher { + private UserService userService; + + public RecordLoginHandler(ApplicationEventPublisher applicationEventPublisher, UserService userService) { + super(applicationEventPublisher); + this.userService = userService; + } + + @Override + public void publishAuthenticationSuccess(Authentication authentication) { + super.publishAuthenticationSuccess(authentication); + userService.updateLoginRecord(authentication.getName()); + } + } } \ No newline at end of file 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 a0b58ff01..76b3299af 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 @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration; +import edu.internet2.tier.shibboleth.admin.ui.domain.BeaconConfiguration; import edu.internet2.tier.shibboleth.admin.ui.security.DefaultAuditorAware; import edu.internet2.tier.shibboleth.admin.ui.security.model.Role; import edu.internet2.tier.shibboleth.admin.ui.security.model.User; @@ -65,6 +66,10 @@ public class SpringSecurityConfig { @Autowired private UserService userService; + public SpringSecurityConfig() { + BeaconConfiguration.setAuthMechanisms("default"); + } + private UserDetailsService adminUserService() { if (defaultPassword != null && !"".equals(defaultPassword)) { User adminUser = userRepository.findByUsername(rootUser).orElseGet(() ->{ 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..928a0f8fd --- /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 jakarta.persistence.EntityExistsException; +import jakarta.transaction.Transactional; +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 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/BeaconController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/BeaconController.java new file mode 100644 index 000000000..e41f17f39 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/BeaconController.java @@ -0,0 +1,34 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import com.fasterxml.jackson.core.JsonProcessingException; +import edu.internet2.tier.shibboleth.admin.ui.scheduled.BeaconReportingTask; +import edu.internet2.tier.shibboleth.admin.ui.service.IBeaconDataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Profile({"dev", "very-dangerous"}) +@RestController +@RequestMapping(value = "/api/beacon") +public class BeaconController { + @Autowired + BeaconReportingTask beaconReporter; + + @Autowired + private IBeaconDataService service; + + @GetMapping(value = "/detail") + public ResponseEntity getDetail() throws JsonProcessingException { + return ResponseEntity.ok(service.getBeaconData()); + } + + @PostMapping("/send") + public ResponseEntity forceSendBeaconData() { + beaconReporter.sendBeaconData(); + return ResponseEntity.ok("Manual push of beacon data completed"); + } +} \ 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..fcc9990fd --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/BeaconConfiguration.java @@ -0,0 +1,34 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Transient; +import lombok.Data; +import lombok.Setter; + +@Data +@Entity +public class BeaconConfiguration { + @Id + private int id = 1; + + private String installationId; + + private String sendCron; + + // Comma separated list of the auth mechanisms used. + @Setter + @Transient + private static String authMechanisms = "unset"; + + public static String getAuthMechanisms() { + return authMechanisms; + } + + /** + * 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/repository/EntityDescriptorRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java index 7fb999568..44ca96afe 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java @@ -60,4 +60,6 @@ public interface EntityDescriptorRepository extends JpaRepository getEntityDescriptorsNeedingApproval(@Param("groupIds") List groupIds); + @Query("SELECT COUNT(ed) FROM EntityDescriptor ed WHERE ed.serviceEnabled = true") + int getActiveEntityCount(); } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepository.java index 8c2273d77..e36d55d3c 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepository.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepository.java @@ -1,8 +1,12 @@ package edu.internet2.tier.shibboleth.admin.ui.repository; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; public interface FilterRepository extends CrudRepository { MetadataFilter findByResourceId(String resourceId); -} + + @Query("SELECT COUNT(f) FROM MetadataFilter f WHERE f.filterEnabled = true") + int getActiveFilterCount(); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepository.java index 07835ea9a..333fb2c11 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepository.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepository.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.repository; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; /** @@ -11,4 +12,7 @@ public interface MetadataResolverRepository extends CrudRepository endpointUrls; + + @Scheduled(cron="#{@getBeaconCronValue}") + @SchedulerLock(name = "sendBeaconData") + @Transactional(readOnly = true) + public void sendBeaconData() { + endpointUrls.forEach(url -> { + try { + 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"); + os.write(input, 0, input.length); + } + } catch (IOException e) { + e.printStackTrace(); + } + }); + + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersController.java index a2faab3d0..f20ce7889 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersController.java @@ -90,6 +90,13 @@ public User getCurrentUser(Principal principal) { return userService.getCurrentUser(); } + @PreAuthorize("hasRole('ADMIN')") + @Transactional(readOnly = true) + @GetMapping("/loginCount") + public int getDailyLoginCount() { + return userService.getDailyLoginCount(); + } + @PreAuthorize("hasRole('ADMIN')") @Transactional(readOnly = true) @GetMapping("/{username}") diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/UserLoginRecord.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/UserLoginRecord.java new file mode 100644 index 000000000..4981fd352 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/UserLoginRecord.java @@ -0,0 +1,28 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.Data; + +import java.util.Date; + +@Data +@Entity +public class UserLoginRecord { + @Id + @GeneratedValue + private Long id; + + private String username; + + private Date loginDate; + + public UserLoginRecord() { + } + + public UserLoginRecord(String username) { + this.username = username; + this.loginDate = new Date(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/UserLoginRecordRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/UserLoginRecordRepository.java new file mode 100644 index 000000000..db583b786 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/UserLoginRecordRepository.java @@ -0,0 +1,19 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.repository; + +import edu.internet2.tier.shibboleth.admin.ui.security.model.UserLoginRecord; +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.Optional; + +public interface UserLoginRecordRepository extends JpaRepository { + Optional findTopByUsername(String username); + + @Query(value = "SELECT count(*) FROM UserLoginRecord ulr WHERE ulr.loginDate >= :sinceDate") + int countLoginsSince(@Param("sinceDate") Date sinceDate); + + @Query(value = "SELECT count(DISTINCT ulr.username) FROM UserLoginRecord ulr WHERE ulr.loginDate >= :sinceDate") + int countUniqueUserLoginsSince(@Param("sinceDate") Date sinceDate); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java index 429dfa6c2..735ce0614 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java @@ -10,18 +10,24 @@ 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.security.model.UserLoginRecord; 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.repository.UserLoginRecordRepository; import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; import lombok.NoArgsConstructor; import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.time.DateUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.text.SimpleDateFormat; +import java.time.LocalDate; import java.util.ArrayList; +import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Optional; @@ -34,6 +40,8 @@ @Service @NoArgsConstructor public class UserService { + private final static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd-MM-yyyy"); + @Autowired private IGroupService groupService; @@ -43,6 +51,9 @@ public class UserService { @Autowired private RoleRepository roleRepository; + @Autowired + private UserLoginRecordRepository userLoginRecordRepository; + @Autowired private UserRepository userRepository; @@ -139,6 +150,8 @@ public Set getUserRoles(String username) { HashSet result = new HashSet<>(); user.ifPresent(value -> value.getRoles().forEach(role -> result.add(role.getName()))); return result; + + } // @TODO - probably delegate this out to something plugable at some point @@ -236,4 +249,29 @@ public void updateUserRole(User user) { public boolean currentUserCanEnable() { return getCurrentUser().getRole().equals("ROLE_ENABLE"); } + + /** + * Ensure there exists a login record for this username and current time + * @param username + */ + public void updateLoginRecord(String username) { + UserLoginRecord ulr = new UserLoginRecord(username); + userLoginRecordRepository.saveAndFlush(ulr); + } + + /** + * @return count of all logins in the last 24 hours + */ + public int getDailyLoginCount() { + Date since = DateUtils.addDays(new Date(), -1); + return userLoginRecordRepository.countLoginsSince(since); + } + + /** + * @return count of unique users logged in during the last 24 hours + */ + public int getDailyUniqueUserLogins() { + Date since = DateUtils.addDays(new Date(), -1); + return userLoginRecordRepository.countUniqueUserLoginsSince(since); + } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserService.java index 34e6ffd06..02a468848 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserService.java @@ -28,7 +28,6 @@ public class AdminUserService implements UserDetailsService { private final UserService userService; @Override - @Transactional(readOnly = true) public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userService .findByUsername(username) 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..97cb5f06d --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/BeaconDataServiceImpl.java @@ -0,0 +1,86 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +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.repository.BeaconConfigurationRepository; +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 lombok.SneakyThrows; +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.Map; + +public class BeaconDataServiceImpl implements IBeaconDataService { + private ObjectMapper mapper; + private String productName; + private String tierVersion; + private String version; + private String db; + + private EntityDescriptorRepository entityDescriptorRepository; + private MetadataResolverRepository metadataResolverRepository; + private FilterRepository filterRepository; + private GroupsRepository groupsRepository; + private RoleRepository roleRepository; + private BeaconConfigurationRepository beaconConfigurationRepository; + private UserService userService; + + public BeaconDataServiceImpl(String productName, InfoEndpoint info, String tierVersion, EntityDescriptorRepository entityDescriptorRepository, + MetadataResolverRepository metadataResolverRepository, FilterRepository filterRepository, GroupsRepository groupsRepository, + RoleRepository roleRepository, BeaconConfigurationRepository beaconConfigurationRepository, UserService userService, + HealthEndpoint health) { + mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // skip any null values + + this.productName = productName; + this.version = info.info().get("build") == null ? "unknown" : ((Map)info.info().get("build")).get("version").toString(); + this.db = StringUtils.substringBetween(health.healthForPath("db").toString(), "database=", ","); + + this.tierVersion = StringUtils.isBlank(tierVersion) ? "NA" : tierVersion; + + this.entityDescriptorRepository = entityDescriptorRepository; + this.metadataResolverRepository = metadataResolverRepository; + this.filterRepository = filterRepository; + this.groupsRepository = groupsRepository; + this.roleRepository = roleRepository; + this.beaconConfigurationRepository = beaconConfigurationRepository; + this.userService = userService; + } + + @Override + @SneakyThrows + public String getBeaconData() { + BeaconDetail detail = new BeaconDetail().setTbProduct(productName).setTbProductVersion(version).setTbTIERRelease(tierVersion).setShibui(getShibuiDetailData()); + + return mapper.writeValueAsString(detail); + } + + private ShibuiDetail getShibuiDetailData() { + BeaconConfiguration configuration = beaconConfigurationRepository.getReferenceById(1); + + ShibuiDetail detail = ShibuiDetail.builder() + .installationID(configuration.getInstallationId()) + .authMechanisms(Arrays.asList(BeaconConfiguration.getAuthMechanisms().split(","))) + .numberOfMetadataSources(entityDescriptorRepository.getActiveEntityCount()) + .numberOfMetadataProviders(metadataResolverRepository.getActiveMetadataProviderCount()) + .numberOfFilters(filterRepository.getActiveFilterCount()) + .dailyLogins(userService.getDailyLoginCount()) + .dailyUniqueUserLogins(userService.getDailyUniqueUserLogins()) + .numberOfGroups((int) groupsRepository.count()) + .numberOfRoles((int) roleRepository.count()) + .db(this.db) + .build(); + return 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/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 new file mode 100644 index 000000000..bef94540f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/beacon/BeaconDetail.java @@ -0,0 +1,19 @@ +package edu.internet2.tier.shibboleth.admin.ui.service.beacon; + +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +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; +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/beacon/ShibuiDetail.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/beacon/ShibuiDetail.java new file mode 100644 index 000000000..8d3b01330 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/beacon/ShibuiDetail.java @@ -0,0 +1,21 @@ +package edu.internet2.tier.shibboleth.admin.ui.service.beacon; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class ShibuiDetail { + private List authMechanisms; + private String db; + private int numberOfMetadataSources; + private int numberOfMetadataProviders; + private int numberOfFilters; + private int dailyLogins; + private int dailyUniqueUserLogins; + private int numberOfGroups; + private int numberOfRoles; + private String installationID; +} \ No newline at end of file diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 1fd9b2b28..3c4d83008 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -121,7 +121,10 @@ 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 +shibui.beacon.url=http://collector.testbed.tier.internet2.edu:5001 ### Swagger/Springdoc patterns springdoc.use-management-port=true @@ -129,7 +132,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/AbstractBaseDataJpaTest.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/AbstractBaseDataJpaTest.groovy index fda2f6b8c..8db64d5f8 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/AbstractBaseDataJpaTest.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/AbstractBaseDataJpaTest.groovy @@ -10,6 +10,7 @@ import edu.internet2.tier.shibboleth.admin.ui.security.repository.ApproversRepos 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.repository.UserLoginRecordRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceForTesting import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService @@ -63,6 +64,9 @@ abstract class AbstractBaseDataJpaTest extends Specification implements ResetsDa @Autowired RoleRepository roleRepository + @Autowired + UserLoginRecordRepository userLoginRecordRepository + @Autowired UserRepository userRepository 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..dfe0851cc 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,16 @@ 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.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.boot.SpringBootConfiguration import org.springframework.boot.web.client.RestTemplateBuilder import org.springframework.context.annotation.Bean @@ -32,6 +42,8 @@ import org.springframework.data.domain.AuditorAware import org.springframework.mail.javamail.JavaMailSender import org.springframework.mail.javamail.JavaMailSenderImpl +import java.time.Duration + /** * NOT A TEST - this is configuration FOR tests */ @@ -134,4 +146,25 @@ class TestConfiguration { return it } } + + @Bean + public InfoEndpoint getInfoEndpoint() { + return new InfoEndpoint(new ArrayList()); + } + + @Bean + public 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.plusSeconds(1)) { + @Override + HealthComponent healthForPath(@Selector(match = Selector.Match.ALL_REMAINING) String... path) { + return new Health(new Status(""), new HashMap()); + } + } + } } \ 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() diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/UserServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/UserServiceTests.groovy index 73c56aa91..df02e801c 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/UserServiceTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/UserServiceTests.groovy @@ -5,12 +5,17 @@ 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.security.model.UserLoginRecord +import org.apache.commons.lang.time.DateUtils + +import java.text.SimpleDateFormat class UserServiceTests extends AbstractBaseDataJpaTest { Role userRole def setup() { userRole = roleRepository.findByName("ROLE_USER").get() + userLoginRecordRepository.deleteAll() } protected createAdminUser() { @@ -163,4 +168,54 @@ class UserServiceTests extends AbstractBaseDataJpaTest { then: gbUpdated.ownedItems.size() == 1 } + + def "when user login - ensure record is created"() { + given: + def sinceDate = DateUtils.addDays(new Date(), -1); + UserLoginRecord ulr = new UserLoginRecord("username") + + expect: + userLoginRecordRepository.findTopByUsername("username").isEmpty() + userLoginRecordRepository.count() == 0 + + when: + userService.updateLoginRecord("username") + + then: + userLoginRecordRepository.findTopByUsername("username").isPresent() + userLoginRecordRepository.count() == 1 + userLoginRecordRepository.countLoginsSince(sinceDate) == 1 + userLoginRecordRepository.countUniqueUserLoginsSince(sinceDate) == 1 + + when: 'repeat login change results appropriately' + userService.updateLoginRecord("username") + + then: + userLoginRecordRepository.findTopByUsername("username").isPresent() + userLoginRecordRepository.count() == 2 + userLoginRecordRepository.countLoginsSince(sinceDate) == 2 + userLoginRecordRepository.countUniqueUserLoginsSince(sinceDate) == 1 + + when: 'new login' + userService.updateLoginRecord("username2") + + then: + userLoginRecordRepository.findTopByUsername("username2").isPresent() + userLoginRecordRepository.count() == 3 + userLoginRecordRepository.countLoginsSince(sinceDate) == 3 + userLoginRecordRepository.countUniqueUserLoginsSince(sinceDate) == 2 + + when: 'older logins in db, should be same counts' + Date older = DateUtils.addDays(new Date(), -3) + UserLoginRecord ulr2 = new UserLoginRecord("username").with { + it.loginDate = older + it + } + userLoginRecordRepository.save(ulr2) + + then: + userLoginRecordRepository.count() == 4 + userLoginRecordRepository.countLoginsSince(sinceDate) == 3 + userLoginRecordRepository.countUniqueUserLoginsSince(sinceDate) == 2 + } } \ No newline at end of file diff --git a/beacon/core/build.gradle b/beacon/core/build.gradle deleted file mode 100644 index 6c2705605..000000000 --- a/beacon/core/build.gradle +++ /dev/null @@ -1,42 +0,0 @@ -import org.springframework.boot.gradle.plugin.SpringBootPlugin - -plugins { - id 'org.springframework.boot' - id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'groovy' -} - -sourceCompatibility = 17 -targetCompatibility = 17 - -bootJar.enabled = false - -repositories { - jcenter() - maven { // for the springboot plugin - url "https://plugins.gradle.org/m2/" - } -} - -configurations { - compile - testCompile -} - -dependencyManagement { - imports { - mavenBom SpringBootPlugin.BOM_COORDINATES - } -} - -dependencies { - testCompile "org.springframework.boot:spring-boot-starter-test:${project.'springbootVersion'}" - implementation "org.apache.groovy:groovy-all:${project.'groovyVersion'}" - testImplementation platform("org.spockframework:spock-bom:2.3-groovy-4.0") - testImplementation "org.spockframework:spock-core" - testImplementation "org.spockframework:spock-spring" -} - -jar { - archiveName = "beacon-core-${version}.jar" -} \ No newline at end of file diff --git a/beacon/core/src/main/java/edu/internet2/tap/beacon/Beacon.java b/beacon/core/src/main/java/edu/internet2/tap/beacon/Beacon.java deleted file mode 100644 index 7e7dec5a5..000000000 --- a/beacon/core/src/main/java/edu/internet2/tap/beacon/Beacon.java +++ /dev/null @@ -1,22 +0,0 @@ -package edu.internet2.tap.beacon; - -/** - * Exposes expected names of environment variables holding beacon config data. - */ -public final class Beacon { - - private Beacon() { - } - - public static final String LOG_HOST = "LOGHOST"; - - public static final String LOG_PORT = "LOGPORT"; - - public static final String IMAGE = "IMAGE"; - - public static final String VERSION = "VERSION"; - - public static final String TIERVERSION = "TIERVERSION"; - - public static final String MAINTAINER = "MAINTAINER"; -} diff --git a/beacon/core/src/main/java/edu/internet2/tap/beacon/BeaconPublisher.java b/beacon/core/src/main/java/edu/internet2/tap/beacon/BeaconPublisher.java deleted file mode 100644 index 80f4f77e4..000000000 --- a/beacon/core/src/main/java/edu/internet2/tap/beacon/BeaconPublisher.java +++ /dev/null @@ -1,10 +0,0 @@ -package edu.internet2.tap.beacon; - -/** - * Simple SPI allowing implementations to publish to beacon service utilizing Runnable API - * so that publishing code could run in separate threads of execution. - * - * @author Dmitriy Kopylenko - */ -public interface BeaconPublisher extends Runnable { -} diff --git a/beacon/core/src/main/java/edu/internet2/tap/beacon/DefaultBeaconPublisher.java b/beacon/core/src/main/java/edu/internet2/tap/beacon/DefaultBeaconPublisher.java deleted file mode 100644 index eb077ddc9..000000000 --- a/beacon/core/src/main/java/edu/internet2/tap/beacon/DefaultBeaconPublisher.java +++ /dev/null @@ -1,90 +0,0 @@ -package edu.internet2.tap.beacon; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Collectors; - -import static edu.internet2.tap.beacon.Beacon.IMAGE; -import static edu.internet2.tap.beacon.Beacon.LOG_HOST; -import static edu.internet2.tap.beacon.Beacon.LOG_PORT; -import static edu.internet2.tap.beacon.Beacon.MAINTAINER; -import static edu.internet2.tap.beacon.Beacon.TIERVERSION; -import static edu.internet2.tap.beacon.Beacon.VERSION; - -/** - * Default implementation that knows the details about payload structure with its data and beacon endpoint details - * gathered by upstream components and passed to this implementation at object construction site. - * - * @author Dmitriy Kopylenko - */ -public class DefaultBeaconPublisher implements BeaconPublisher { - - private final URL endpointUrl; - - private final String jsonPayload; - - public DefaultBeaconPublisher(Map beaconDetails) { - //Do data validation checks here. If any of the necessary beacon data not available here, throw a Runtime exception - if (beaconDetails == null) { - throw new IllegalArgumentException("beaconDetails Map must not be null"); - } - if (beaconDetails.get(LOG_HOST) == null - || beaconDetails.get(LOG_PORT) == null - || beaconDetails.get(IMAGE) == null - || beaconDetails.get(VERSION) == null - || beaconDetails.get(TIERVERSION) == null - || beaconDetails.get(MAINTAINER) == null) { - throw new IllegalArgumentException("Not all the necessary beacon data is available to be able to publish to beacon"); - } - try { - this.endpointUrl = new URL(String.format("http://%s:%s", beaconDetails.get(LOG_HOST), beaconDetails.get(LOG_PORT))); - } catch (MalformedURLException ex) { - throw new IllegalArgumentException(ex.getMessage()); - } - - Map dataToPublish = new HashMap<>(); - dataToPublish.put("msgType", "TIERBEACON"); - dataToPublish.put("msgName", "TIER"); - dataToPublish.put("msgVersion", "1.0"); - dataToPublish.put("tbProduct", beaconDetails.get(IMAGE)); - dataToPublish.put("tbProductVersion", beaconDetails.get(VERSION)); - dataToPublish.put("tbTIERRelease", beaconDetails.get(TIERVERSION)); - dataToPublish.put("tbMaintainer", beaconDetails.get(MAINTAINER)); - - //Create JSON payload without any 3-rd party library - this.jsonPayload = "{" + dataToPublish.entrySet().stream() - .map(e -> "\"" + e.getKey() + "\"" + ":\"" + e.getValue() + "\"") - .collect(Collectors.joining(", ")) + "}"; - } - - @Override - public void run() { - try { - HttpURLConnection con = (HttpURLConnection) this.endpointUrl.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 = jsonPayload.getBytes("utf-8"); - os.write(input, 0, input.length); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - //getters used in unit tests and calling components for debugging purposes - public String getEndpointUri() { - return endpointUrl.toString(); - } - - public String getJsonPayload() { - return jsonPayload; - } -} diff --git a/beacon/core/src/test/groovy/edu/internet2/tap/beacon/DefaultBeaconPublisherTests.groovy b/beacon/core/src/test/groovy/edu/internet2/tap/beacon/DefaultBeaconPublisherTests.groovy deleted file mode 100644 index d3004ef13..000000000 --- a/beacon/core/src/test/groovy/edu/internet2/tap/beacon/DefaultBeaconPublisherTests.groovy +++ /dev/null @@ -1,45 +0,0 @@ -package edu.internet2.tap.beacon - -import spock.lang.Specification -import sun.security.x509.OtherName - -class DefaultBeaconPublisherTests extends Specification { - - def "DefaultBeaconPublisher invariants are enforced during object creation - null Map is passed"() { - when: - new DefaultBeaconPublisher(null) - - then: - thrown IllegalArgumentException - - } - - def "DefaultBeaconPublisher invariants are enforced during object creation - empty Map is passed"() { - when: - new DefaultBeaconPublisher([:]) - - then: - thrown IllegalArgumentException - } - - def "DefaultBeaconPublisher invariants are enforced during object creation - valid Beacon data Map is passed"() { - when: - def expectedJsonPaylaod = """{"msgType":"TIERBEACON", "tbMaintainer":"unittest_maintainer", "msgName":"TIER", "tbProduct":"image", "msgVersion":"1.0", "tbProductVersion":"v1", "tbTIERRelease":"tv1"}""" - - def configuredBeaconData = [LOGHOST : 'collector.testbed.tier.internet2.edu', - LOGPORT : '5001', - IMAGE : 'image', - VERSION : 'v1', - TIERVERSION: 'tv1', - MAINTAINER : 'unittest_maintainer'] - def p = new DefaultBeaconPublisher(configuredBeaconData) - println p.jsonPayload - - then: - noExceptionThrown() - p.endpointUri == 'http://collector.testbed.tier.internet2.edu:5001' - p.jsonPayload == expectedJsonPaylaod - - } - -} diff --git a/beacon/gradle.properties b/beacon/gradle.properties deleted file mode 100644 index 23dbf7481..000000000 --- a/beacon/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -group=edu.internet2.tap.beacon -version=1.0.0-SNAPSHOT diff --git a/beacon/spring/build.gradle b/beacon/spring/build.gradle deleted file mode 100644 index 4e7d46206..000000000 --- a/beacon/spring/build.gradle +++ /dev/null @@ -1,38 +0,0 @@ -import org.springframework.boot.gradle.plugin.SpringBootPlugin - -plugins { - id 'org.springframework.boot' - id 'io.spring.dependency-management' version '1.0.6.RELEASE' -} - -apply plugin: 'java' -sourceCompatibility = 17 -targetCompatibility = 17 - -bootJar.enabled = false - -repositories { - jcenter() - maven { // for the springboot plugin - url "https://plugins.gradle.org/m2/" - } -} - -configurations { - compile -} - -jar { - archiveName = "beacon-spring-${version}.jar" -} - -dependencyManagement { - imports { - mavenBom SpringBootPlugin.BOM_COORDINATES - } -} - -dependencies { - implementation project(':beacon:core') - implementation "org.springframework.boot:spring-boot-starter" -} \ No newline at end of file diff --git a/beacon/spring/src/main/java/edu/internet2/tap/beacon/configuration/BeaconPublishingConfiguration.java b/beacon/spring/src/main/java/edu/internet2/tap/beacon/configuration/BeaconPublishingConfiguration.java deleted file mode 100644 index fc8264794..000000000 --- a/beacon/spring/src/main/java/edu/internet2/tap/beacon/configuration/BeaconPublishingConfiguration.java +++ /dev/null @@ -1,61 +0,0 @@ -package edu.internet2.tap.beacon.configuration; - -import edu.internet2.tap.beacon.DefaultBeaconPublisher; -import edu.internet2.tap.beacon.configuration.condition.ConditionalOnBeaconEnvironmentVariablesPresent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.SpringBootConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.core.env.Environment; -import org.springframework.scheduling.annotation.Async; -import org.springframework.scheduling.annotation.Scheduled; - -import java.util.HashMap; -import java.util.Map; - -import static edu.internet2.tap.beacon.Beacon.IMAGE; -import static edu.internet2.tap.beacon.Beacon.LOG_HOST; -import static edu.internet2.tap.beacon.Beacon.LOG_PORT; -import static edu.internet2.tap.beacon.Beacon.MAINTAINER; -import static edu.internet2.tap.beacon.Beacon.TIERVERSION; -import static edu.internet2.tap.beacon.Beacon.VERSION; - -@SpringBootConfiguration -@ConditionalOnProperty(name = "shibui.beacon-enabled", havingValue = "true") -public class BeaconPublishingConfiguration { - - private static final Logger logger = LoggerFactory.getLogger(BeaconPublishingConfiguration.class); - - @Bean - @ConditionalOnBeaconEnvironmentVariablesPresent - public BeaconPublishingTask beaconPublisher(Environment env) { - logger.debug("Creating BeaconPublishingTask..."); - Map beaconData = new HashMap<>(); - beaconData.put(LOG_HOST, env.getProperty(LOG_HOST)); - beaconData.put(LOG_PORT, env.getProperty(LOG_PORT)); - beaconData.put(IMAGE, env.getProperty(IMAGE)); - beaconData.put(VERSION, env.getProperty(VERSION)); - beaconData.put(TIERVERSION, env.getProperty(TIERVERSION)); - beaconData.put(MAINTAINER, env.getProperty(MAINTAINER)); - return new BeaconPublishingTask(new DefaultBeaconPublisher(beaconData)); - } - - public static class BeaconPublishingTask { - private DefaultBeaconPublisher beaconPublisher; - - public BeaconPublishingTask(DefaultBeaconPublisher beaconPublisher) { - this.beaconPublisher = beaconPublisher; - } - - //Cron is based on the spec defined here: https://spaces.at.internet2.edu/display/TWGH/TIER+Instrumentation+-+The+TIER+Beacon - @Scheduled(cron = "0 ${random.int[0,59]} ${random.int[0,3]} ? * *") - @Async - void publish() { - logger.debug("Publishing payload: {} to beacon endpoint: {}", - beaconPublisher.getJsonPayload(), - beaconPublisher.getEndpointUri()); - beaconPublisher.run(); - } - } -} \ No newline at end of file diff --git a/beacon/spring/src/main/java/edu/internet2/tap/beacon/configuration/condition/BeaconEnvironmentVariablesCondition.java b/beacon/spring/src/main/java/edu/internet2/tap/beacon/configuration/condition/BeaconEnvironmentVariablesCondition.java deleted file mode 100644 index 84f8857fc..000000000 --- a/beacon/spring/src/main/java/edu/internet2/tap/beacon/configuration/condition/BeaconEnvironmentVariablesCondition.java +++ /dev/null @@ -1,48 +0,0 @@ -package edu.internet2.tap.beacon.configuration.condition; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.autoconfigure.condition.ConditionOutcome; -import org.springframework.boot.autoconfigure.condition.SpringBootCondition; -import org.springframework.context.annotation.Condition; -import org.springframework.context.annotation.ConditionContext; -import org.springframework.core.env.Environment; -import org.springframework.core.type.AnnotatedTypeMetadata; - -import static edu.internet2.tap.beacon.Beacon.IMAGE; -import static edu.internet2.tap.beacon.Beacon.LOG_HOST; -import static edu.internet2.tap.beacon.Beacon.LOG_PORT; -import static edu.internet2.tap.beacon.Beacon.MAINTAINER; -import static edu.internet2.tap.beacon.Beacon.TIERVERSION; -import static edu.internet2.tap.beacon.Beacon.VERSION; - -/** - * {@link Condition} that checks for required beacon environment variables. - * - * @author Dmitriy Kopylenko - * @see ConditionalOnBeaconEnvironmentVariablesPresent - */ -public class BeaconEnvironmentVariablesCondition extends SpringBootCondition { - - private static final String MATCHED_MSG = "Beacon properties are present. Beacon activation condition is matched."; - - private static final String NOT_MATCHED_MSG = "Beacon properties are not present. Beacon activation condition is not matched."; - - private static final Logger logger = LoggerFactory.getLogger(BeaconEnvironmentVariablesCondition.class); - - @Override - public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { - Environment env = context.getEnvironment(); - if (env.containsProperty(LOG_HOST) - && env.containsProperty(LOG_PORT) - && env.containsProperty(IMAGE) - && env.containsProperty(VERSION) - && env.containsProperty(TIERVERSION) - && env.containsProperty(MAINTAINER)) { - logger.debug(MATCHED_MSG); - return ConditionOutcome.match(MATCHED_MSG); - } - logger.debug(NOT_MATCHED_MSG); - return ConditionOutcome.noMatch(NOT_MATCHED_MSG); - } -} diff --git a/beacon/spring/src/main/java/edu/internet2/tap/beacon/configuration/condition/ConditionalOnBeaconEnvironmentVariablesPresent.java b/beacon/spring/src/main/java/edu/internet2/tap/beacon/configuration/condition/ConditionalOnBeaconEnvironmentVariablesPresent.java deleted file mode 100644 index 66049923b..000000000 --- a/beacon/spring/src/main/java/edu/internet2/tap/beacon/configuration/condition/ConditionalOnBeaconEnvironmentVariablesPresent.java +++ /dev/null @@ -1,21 +0,0 @@ -package edu.internet2.tap.beacon.configuration.condition; - -import org.springframework.context.annotation.Conditional; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * {@link Conditional} that matches specific beacon environment variables are all present. - * - * @author Dmitriy Kopylenko - */ -@Target({ ElementType.TYPE, ElementType.METHOD }) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Conditional(BeaconEnvironmentVariablesCondition.class) -public @interface ConditionalOnBeaconEnvironmentVariablesPresent { -} diff --git a/beacon/spring/src/main/resources/META-INF/spring.factories b/beacon/spring/src/main/resources/META-INF/spring.factories deleted file mode 100644 index ae9c29c00..000000000 --- a/beacon/spring/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1 +0,0 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=edu.internet2.tap.beacon.configuration.BeaconPublishingConfiguration \ No newline at end of file diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/AddNewUserFilter.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/AddNewUserFilter.java index b837fb081..fb757313f 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/AddNewUserFilter.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/AddNewUserFilter.java @@ -135,6 +135,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha if (user.getRole().equals(ROLE_NONE)) { ((HttpServletResponse) response).sendRedirect("/unsecured/error.html"); } else { + // User exists or has been created and has a role so we can continue on. chain.doFilter(request, response); // else, user is in the system already, carry on } } diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jSpringSecurityConfig.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jSpringSecurityConfig.java index 777fec504..a1bc022ca 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jSpringSecurityConfig.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jSpringSecurityConfig.java @@ -2,6 +2,7 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.EmailConfiguration; import edu.internet2.tier.shibboleth.admin.ui.configuration.SpringSecurityConfig; +import edu.internet2.tier.shibboleth.admin.ui.domain.BeaconConfiguration; import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; import edu.internet2.tier.shibboleth.admin.ui.security.service.IRolesService; import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; @@ -58,6 +59,7 @@ public Pac4jSpringSecurityConfig(final Config config, UserService userService, this.emailService = emailService; this.groupService = groupService; this.pac4jConfigurationProperties = pac4jConfigurationProperties; + BeaconConfiguration.setAuthMechanisms("Pac4J - SAML2 provider"); } @Bean diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/authenticator/ShibuiSAML2Authenticator.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/authenticator/ShibuiSAML2Authenticator.java index f2d2738e0..110611f2d 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/authenticator/ShibuiSAML2Authenticator.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/authenticator/ShibuiSAML2Authenticator.java @@ -28,5 +28,6 @@ public void validate(final Credentials credentials, final WebContext context, fi CommonProfile profile = (CommonProfile) credentials.getUserProfile(); profile.setRoles(userService.getUserRoles(profile.getUsername())); credentials.setUserProfile(profile); + userService.updateLoginRecord(profile.getUsername()); } } \ No newline at end of file diff --git a/testbed/authentication/shibui/application.yml b/testbed/authentication/shibui/application.yml index 942d3aaaf..fb64e02d0 100644 --- a/testbed/authentication/shibui/application.yml +++ b/testbed/authentication/shibui/application.yml @@ -3,7 +3,7 @@ server: forward-headers-strategy: NATIVE spring: profiles: - include: + include: dev shibui: user-bootstrap-resource: file:/conf/users.csv roles: ROLE_ADMIN,ROLE_NONE,ROLE_USER,ROLE_ENABLE,ROLE_PONY