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 index 75e5fa728..7ed12a93e 100644 --- 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 @@ -11,7 +11,6 @@ @Data @Entity -@Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "username", "login" }) }) public class UserLoginRecord { @Id @GeneratedValue @@ -19,16 +18,13 @@ public class UserLoginRecord { private String username; - private String login; - private Date loginDate; public UserLoginRecord() { } - public UserLoginRecord(String username, Date loginDate, String formattedDate) { + public UserLoginRecord(String username) { this.username = username; - this.login = formattedDate; - this.loginDate = loginDate; + 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 index 22a97fabb..db583b786 100644 --- 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 @@ -9,8 +9,11 @@ import java.util.Optional; public interface UserLoginRecordRepository extends JpaRepository { - Optional findByUsernameAndLogin(String username, String formattedDate); + 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 e2a45b12f..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 @@ -251,21 +251,27 @@ public boolean currentUserCanEnable() { } /** - * Ensure there exists a login record for this username and today's date + * Ensure there exists a login record for this username and current time * @param username */ public void updateLoginRecord(String username) { - Date current = new Date(); - String formattedDate = DATE_FORMAT.format(current); - - if (userLoginRecordRepository.findByUsernameAndLogin(username,formattedDate).isEmpty()) { - UserLoginRecord ulr = new UserLoginRecord(username, current, formattedDate); - userLoginRecordRepository.saveAndFlush(ulr); - } + 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/service/BeaconDataServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/BeaconDataServiceImpl.java index 0047275d7..97cb5f06d 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 @@ -76,6 +76,7 @@ private ShibuiDetail getShibuiDetailData() { .numberOfMetadataProviders(metadataResolverRepository.getActiveMetadataProviderCount()) .numberOfFilters(filterRepository.getActiveFilterCount()) .dailyLogins(userService.getDailyLoginCount()) + .dailyUniqueUserLogins(userService.getDailyUniqueUserLogins()) .numberOfGroups((int) groupsRepository.count()) .numberOfRoles((int) roleRepository.count()) .db(this.db) 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 index 6bab7ef96..8d3b01330 100644 --- 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 @@ -14,6 +14,7 @@ public class ShibuiDetail { private int numberOfMetadataProviders; private int numberOfFilters; private int dailyLogins; + private int dailyUniqueUserLogins; private int numberOfGroups; private int numberOfRoles; private String installationID; 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 a04043631..d756032e8 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,14 @@ 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 @@ -141,4 +149,20 @@ class TestConfiguration { 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 } + }) { + @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/security/service/UserServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/UserServiceTests.groovy index 5b7b9feb0..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 @@ -171,47 +171,51 @@ class UserServiceTests extends AbstractBaseDataJpaTest { def "when user login - ensure record is created"() { given: - SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd-MM-yyyy") - - Date current = new Date(); - String formattedDate = DATE_FORMAT.format(current) - UserLoginRecord ulr = new UserLoginRecord("username", current, formattedDate) + def sinceDate = DateUtils.addDays(new Date(), -1); + UserLoginRecord ulr = new UserLoginRecord("username") expect: - userLoginRecordRepository.findByUsernameAndLogin("username", formattedDate).isEmpty() + userLoginRecordRepository.findTopByUsername("username").isEmpty() userLoginRecordRepository.count() == 0 when: userService.updateLoginRecord("username") then: - userLoginRecordRepository.findByUsernameAndLogin("username", formattedDate).isPresent() + userLoginRecordRepository.findTopByUsername("username").isPresent() userLoginRecordRepository.count() == 1 + userLoginRecordRepository.countLoginsSince(sinceDate) == 1 + userLoginRecordRepository.countUniqueUserLoginsSince(sinceDate) == 1 - when: 'try adding again should not change result' + when: 'repeat login change results appropriately' userService.updateLoginRecord("username") then: - userLoginRecordRepository.findByUsernameAndLogin("username", formattedDate).isPresent() - userLoginRecordRepository.count() == 1 - userService.getDailyLoginCount() == 1 + userLoginRecordRepository.findTopByUsername("username").isPresent() + userLoginRecordRepository.count() == 2 + userLoginRecordRepository.countLoginsSince(sinceDate) == 2 + userLoginRecordRepository.countUniqueUserLoginsSince(sinceDate) == 1 - when: + when: 'new login' userService.updateLoginRecord("username2") then: - userLoginRecordRepository.findByUsernameAndLogin("username2", formattedDate).isPresent() - userLoginRecordRepository.count() == 2 - userService.getDailyLoginCount() == 2 + 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) - String formattedDate2 = DATE_FORMAT.format(older) - UserLoginRecord ulr2 = new UserLoginRecord("username", older, formattedDate2) + UserLoginRecord ulr2 = new UserLoginRecord("username").with { + it.loginDate = older + it + } userLoginRecordRepository.save(ulr2) then: - userLoginRecordRepository.count() == 3 - userService.getDailyLoginCount() == 2 + userLoginRecordRepository.count() == 4 + userLoginRecordRepository.countLoginsSince(sinceDate) == 3 + userLoginRecordRepository.countUniqueUserLoginsSince(sinceDate) == 2 } } \ No newline at end of file diff --git a/delete.json b/delete.json deleted file mode 100644 index 93b3d2534..000000000 --- a/delete.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "git": { - "branch": "feature/shibui-2571", - "commit": { - "id": "b547925", - "time": "2023-05-23T20:32:38Z" - } - }, - "build": { - "artifact": "shibui", - "name": "backend", - "time": "2023-05-24T17:15:28.585Z", - "version": "1.18.0-SNAPSHOT", - "group": "edu.internet2.tier.shibboleth.admin.ui" - } -} \ 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 d9365ac06..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 @@ -136,7 +136,6 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha ((HttpServletResponse) response).sendRedirect("/unsecured/error.html"); } else { // User exists or has been created and has a role so we can continue on. - userService.updateLoginRecord(username); 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/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/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy b/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy index 2097ccaa1..216ccdafe 100644 --- a/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy +++ b/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy @@ -1,11 +1,9 @@ package net.unicon.shibui.pac4j -import edu.internet2.tier.shibboleth.admin.ui.security.model.Group 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.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.IRolesService @@ -30,7 +28,6 @@ import spock.lang.Subject import javax.servlet.FilterChain import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse -import java.text.SimpleDateFormat @DataJpaTest @ContextConfiguration(classes=[Pac4JTestingConfig]) @@ -57,9 +54,6 @@ class AddNewUserFilterTests extends Specification { @Autowired Pac4jConfigurationProperties pac4jConfigurationProperties - @Autowired - UserLoginRecordRepository userLoginRecordRepository - @Autowired UserRepository userRepository @@ -107,44 +101,6 @@ class AddNewUserFilterTests extends Specification { roles.each { roleRepository.save(it) } - - Group gb = new Group() - gb.setResourceId("testingGroupBBB") - gb.setName("Group BBB") - gb.setValidationRegex("^(?:https?:\\/\\/)?(?:[^.]+\\.)?shib\\.org(\\/.*)?\$") - gb = groupService.createGroup(gb) - - Optional userRole = roleRepository.findByName("ROLE_USER") - User user = new User(username: "someUser", roles:[userRole.get()], password: "foo") - user.setGroup(gb) - userService.save(user) - } - - def "user logged in"() { - given: - ['Username': 'someUser', - 'FirstName': 'Some', - 'LastName': 'User', - 'Email': 'someUser@institution.edu'].each { key, value -> - saml2Profile.getAttribute(profileMapping."get${key}"()) >> [value] - } - saml2Profile.getUsername() >> "someUser" - - SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd-MM-yyyy") - - Date current = new Date(); - String formattedDate = DATE_FORMAT.format(current) - - expect: - userLoginRecordRepository.findByUsernameAndLogin("someUser", formattedDate).isEmpty() - userLoginRecordRepository.count() == 0 - - when: - addNewUserFilter.doFilter(request, response, chain) - - then: - userLoginRecordRepository.findByUsernameAndLogin("someUser", formattedDate).isPresent() - userLoginRecordRepository.count() == 1 } def "new user created"() { 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