diff --git a/README.md b/README.md index cbfaca276..18fcd043f 100644 --- a/README.md +++ b/README.md @@ -152,11 +152,11 @@ users. As a user of a tenant can log in via `http://localhost:8060/`: -| username | password | role | -|---------------------|----------|------------------| -| boss@example.org | secret | view_reports_all | -| office@example.org | secret | view_reports_all | -| user@example.org | secret | | +| username | password | role | +|---------------------|----------|------------------------------------| +| boss@example.org | secret | view_reports_all, edit_authorities | +| office@example.org | secret | view_reports_all, edit_authorities | +| user@example.org | secret | | ### git hooks (optional) diff --git a/docker/keycloak/keycloak-export/zeiterfassung-realm-realm.json b/docker/keycloak/keycloak-export/zeiterfassung-realm-realm.json index b25d4ab9b..1491cf90a 100644 --- a/docker/keycloak/keycloak-export/zeiterfassung-realm-realm.json +++ b/docker/keycloak/keycloak-export/zeiterfassung-realm-realm.json @@ -92,6 +92,14 @@ "clientRole" : false, "containerId" : "zeiterfassung-realm", "attributes" : { } + }, { + "id" : "2c214c0a-fcc5-4a10-b989-5cc2043fcb8c", + "name" : "ZEITERFASSUNG_EDIT_AUTHORITIES", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "zeiterfassung-realm", + "attributes" : { } } ], "client" : { "realm-management" : [ { @@ -365,7 +373,7 @@ "name" : "ZEITERFASSUNG_PRIVILEGED", "path" : "/ZEITERFASSUNG_PRIVILEGED", "attributes" : { }, - "realmRoles" : [ "ZEITERFASSUNG_VIEW_REPORT_ALL" ], + "realmRoles" : [ "ZEITERFASSUNG_VIEW_REPORT_ALL", "ZEITERFASSUNG_EDIT_AUTHORITIES" ], "clientRoles" : { }, "subGroups" : [ ] } ], diff --git a/package-lock.json b/package-lock.json index d1641e3dd..4ca70ce90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "eslint-plugin-prettier": "4.2.1", "eslint-plugin-svelte3": "4.0.0", "eslint-plugin-unicorn": "44.0.2", + "fast-glob": "3.2.12", "lint-staged": "13.0.3", "postcss": "8.4.19", "postcss-cli": "10.0.0", diff --git a/package.json b/package.json index 6729cdf8e..0f6351fb5 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "eslint-plugin-prettier": "4.2.1", "eslint-plugin-svelte3": "4.0.0", "eslint-plugin-unicorn": "44.0.2", + "fast-glob": "3.2.12", "lint-staged": "13.0.3", "postcss": "8.4.19", "postcss-cli": "10.0.0", diff --git a/pom.xml b/pom.xml index 6251bbe85..62c830b53 100644 --- a/pom.xml +++ b/pom.xml @@ -49,6 +49,12 @@ thymeleaf-extras-springsecurity5 + + + org.springframework.session + spring-session-jdbc + + org.threeten threeten-extra diff --git a/rollup.config.js b/rollup.config.js index 03273ec3e..b2ce4b81c 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -5,6 +5,7 @@ import commonjs from "@rollup/plugin-commonjs"; import esbuild from "rollup-plugin-esbuild"; import svelte from "rollup-plugin-svelte"; import sveltePreprocess from "svelte-preprocess"; +import glob from "fast-glob"; const paths = { src: "src/main/javascript", @@ -14,11 +15,7 @@ const paths = { export default { input: { "custom-elements-polyfill": `@ungap/custom-elements`, - "date-fns-localized": `${paths.src}/bundles/date-fns-localized.ts`, - reports: `${paths.src}/bundles/reports.ts`, - "time-entries": `${paths.src}/bundles/time-entries.ts`, - turbo: `${paths.src}/bundles/turbo.ts`, - "user-common": `${paths.src}/bundles/user-common.ts`, + ...inputFiles(), }, output: { dir: paths.dist, @@ -54,3 +51,13 @@ export default { }), ], }; + +function inputFiles() { + const files = glob.sync(`${paths.src}/bundles/*.ts`); + return Object.fromEntries(files.map((file) => [entryName(file), file])); +} + +function entryName(file) { + const filename = file.slice(Math.max(0, file.lastIndexOf("/") + 1)); + return filename.slice(0, Math.max(0, filename.lastIndexOf("."))); +} diff --git a/src/main/css/style.css b/src/main/css/style.css index ebea85dbe..3b7e39abe 100644 --- a/src/main/css/style.css +++ b/src/main/css/style.css @@ -522,6 +522,18 @@ top: 0; left: 0; } + + .authorities { + } + + .authorities__item { + @apply transition-colors; + @apply hover:bg-blue-50; + } + + .authorities__item:has(input[type="checkbox"]:checked) { + @apply bg-blue-50; + } } @layer utilities { diff --git a/src/main/java/de/focusshift/zeiterfassung/security/AuthorityControllerAdvice.java b/src/main/java/de/focusshift/zeiterfassung/security/AuthorityControllerAdvice.java new file mode 100644 index 000000000..94838bafb --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/security/AuthorityControllerAdvice.java @@ -0,0 +1,31 @@ +package de.focusshift.zeiterfassung.security; + +import de.focusshift.zeiterfassung.user.CurrentUserProvider; +import de.focusshift.zeiterfassung.usermanagement.User; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ModelAttribute; + +import javax.servlet.http.HttpServletRequest; + +import static de.focusshift.zeiterfassung.security.SecurityRoles.ZEITERFASSUNG_EDIT_AUTHORITIES; +import static de.focusshift.zeiterfassung.security.SecurityRoles.ZEITERFASSUNG_VIEW_REPORT_ALL; + +@ControllerAdvice(basePackages = {"de.focusshift.zeiterfassung"}) +public class AuthorityControllerAdvice { + + private final CurrentUserProvider currentUserProvider; + + public AuthorityControllerAdvice(CurrentUserProvider currentUserProvider) { + this.currentUserProvider = currentUserProvider; + } + + @ModelAttribute + void authorities(Model model, HttpServletRequest request) { + + final User currentUser = currentUserProvider.getCurrentUser(); + + model.addAttribute("allowed_viewReportsAll", currentUser.hasAuthority(ZEITERFASSUNG_VIEW_REPORT_ALL)); + model.addAttribute("allowed_editAuthorities", currentUser.hasAuthority(ZEITERFASSUNG_EDIT_AUTHORITIES)); + } +} diff --git a/src/main/java/de/focusshift/zeiterfassung/security/ReloadAuthenticationAuthoritiesFilter.java b/src/main/java/de/focusshift/zeiterfassung/security/ReloadAuthenticationAuthoritiesFilter.java new file mode 100644 index 000000000..a12f8a523 --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/security/ReloadAuthenticationAuthoritiesFilter.java @@ -0,0 +1,98 @@ +package de.focusshift.zeiterfassung.security; + +import de.focusshift.zeiterfassung.user.CurrentUserProvider; +import de.focusshift.zeiterfassung.usermanagement.User; +import org.slf4j.Logger; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import javax.validation.constraints.NotNull; +import java.io.IOException; +import java.util.List; +import java.util.stream.Stream; + +import static de.focusshift.zeiterfassung.security.SessionService.RELOAD_AUTHORITIES; +import static java.lang.Boolean.TRUE; +import static java.lang.invoke.MethodHandles.lookup; +import static java.util.function.Predicate.not; +import static org.slf4j.LoggerFactory.getLogger; + +class ReloadAuthenticationAuthoritiesFilter extends OncePerRequestFilter { + + private static final Logger LOG = getLogger(lookup().lookupClass()); + + private final CurrentUserProvider currentUserProvider; + private final SessionService sessionService; + + ReloadAuthenticationAuthoritiesFilter(CurrentUserProvider currentUserProvider, SessionService sessionService) { + this.currentUserProvider = currentUserProvider; + this.sessionService = sessionService; + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + final HttpSession session = request.getSession(); + if (session == null) { + return true; + } + + final Boolean reload = (Boolean) session.getAttribute(RELOAD_AUTHORITIES); + return !TRUE.equals(reload); + } + + @Override + public void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain chain) throws ServletException, IOException { + final HttpSession session = request.getSession(); + sessionService.unmarkSessionToReloadAuthorities(session.getId()); + + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + final User user = currentUserProvider.getCurrentUser(); + final List updatedAuthorities = getUpdatedAuthorities(authentication, user); + + try { + final Authentication updatedAuthentication = getUpdatedAuthentication(updatedAuthorities, authentication); + SecurityContextHolder.getContext().setAuthentication(updatedAuthentication); + LOG.info("Updated authorities of person with the id {} from {} to {}", user.id(), authentication.getAuthorities(), updatedAuthorities); + } catch (ReloadAuthenticationException e) { + LOG.error(e.getMessage()); + } + + chain.doFilter(request, response); + } + + private List getUpdatedAuthorities(Authentication authentication, User user) { + + final Stream otherAuthorities = authentication.getAuthorities() + .stream() + .filter(not(grantedAuthority -> grantedAuthority.getAuthority().startsWith("ROLE_ZEITERFASSUNG"))); + + final Stream zeiterfassungAuthorities = user.authorities().stream() + .map(role -> new SimpleGrantedAuthority("ROLE_" + role.name())); + + return Stream.concat(otherAuthorities, zeiterfassungAuthorities).toList(); + } + + private Authentication getUpdatedAuthentication(List updatedAuthorities, Authentication authentication) throws ReloadAuthenticationException { + final Authentication updatedAuthentication; + if (authentication instanceof OAuth2AuthenticationToken) { + final OAuth2AuthenticationToken oAuth2Auth = (OAuth2AuthenticationToken) authentication; + updatedAuthentication = new OAuth2AuthenticationToken(oAuth2Auth.getPrincipal(), updatedAuthorities, oAuth2Auth.getAuthorizedClientRegistrationId()); + } else if (authentication instanceof UsernamePasswordAuthenticationToken) { + updatedAuthentication = new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), authentication.getCredentials(), updatedAuthorities); + } else { + throw new ReloadAuthenticationException("Could not update authentication with updated authorities, because of type mismatch"); + } + + return updatedAuthentication; + } +} diff --git a/src/main/java/de/focusshift/zeiterfassung/security/ReloadAuthenticationException.java b/src/main/java/de/focusshift/zeiterfassung/security/ReloadAuthenticationException.java new file mode 100644 index 000000000..2f18485b2 --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/security/ReloadAuthenticationException.java @@ -0,0 +1,7 @@ +package de.focusshift.zeiterfassung.security; + +public class ReloadAuthenticationException extends Exception { + ReloadAuthenticationException(String message) { + super(message); + } +} diff --git a/src/main/java/de/focusshift/zeiterfassung/security/SecurityRoles.java b/src/main/java/de/focusshift/zeiterfassung/security/SecurityRoles.java index 843728086..4c9c2a266 100644 --- a/src/main/java/de/focusshift/zeiterfassung/security/SecurityRoles.java +++ b/src/main/java/de/focusshift/zeiterfassung/security/SecurityRoles.java @@ -7,7 +7,8 @@ public enum SecurityRoles { ZEITERFASSUNG_OPERATOR, // used by fss employees only! ZEITERFASSUNG_USER, - ZEITERFASSUNG_VIEW_REPORT_ALL; + ZEITERFASSUNG_VIEW_REPORT_ALL, + ZEITERFASSUNG_EDIT_AUTHORITIES; private GrantedAuthority authority; diff --git a/src/main/java/de/focusshift/zeiterfassung/security/SecurityRules.java b/src/main/java/de/focusshift/zeiterfassung/security/SecurityRules.java new file mode 100644 index 000000000..5725a45d1 --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/security/SecurityRules.java @@ -0,0 +1,8 @@ +package de.focusshift.zeiterfassung.security; + +public class SecurityRules { + + public static final String ALLOW_EDIT_AUTHORITIES = "hasAuthority('ROLE_ZEITERFASSUNG_EDIT_AUTHORITIES')"; + + private SecurityRules() {} +} diff --git a/src/main/java/de/focusshift/zeiterfassung/security/SecurityWebConfiguration.java b/src/main/java/de/focusshift/zeiterfassung/security/SecurityWebConfiguration.java index 99f639fbf..aba9fe482 100644 --- a/src/main/java/de/focusshift/zeiterfassung/security/SecurityWebConfiguration.java +++ b/src/main/java/de/focusshift/zeiterfassung/security/SecurityWebConfiguration.java @@ -1,5 +1,6 @@ package de.focusshift.zeiterfassung.security; +import de.focusshift.zeiterfassung.user.CurrentUserProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest; import org.springframework.boot.actuate.health.HealthEndpoint; @@ -15,6 +16,7 @@ import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; @Configuration @@ -25,16 +27,21 @@ class SecurityWebConfiguration { private final AuthenticationSuccessHandler authenticationSuccessHandler; private final OidcClientInitiatedLogoutSuccessHandler oidcClientInitiatedLogoutSuccessHandler; private final OAuth2UserService oidcUserService; + private final CurrentUserProvider currentUserProvider; + private final SessionService sessionService; @Autowired SecurityWebConfiguration(AuthenticationEntryPoint authenticationEntryPoint, AuthenticationSuccessHandler authenticationSuccessHandler, OidcClientInitiatedLogoutSuccessHandler oidcClientInitiatedLogoutSuccessHandler, - OAuth2UserService oidcUserService) { + OAuth2UserService oidcUserService, + CurrentUserProvider currentUserProvider, SessionService sessionService) { this.authenticationEntryPoint = authenticationEntryPoint; this.authenticationSuccessHandler = authenticationSuccessHandler; this.oidcClientInitiatedLogoutSuccessHandler = oidcClientInitiatedLogoutSuccessHandler; this.oidcUserService = oidcUserService; + this.currentUserProvider = currentUserProvider; + this.sessionService = sessionService; } @Bean @@ -66,6 +73,9 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.logout() .logoutSuccessHandler(oidcClientInitiatedLogoutSuccessHandler); + http + .addFilterAfter(new ReloadAuthenticationAuthoritiesFilter(currentUserProvider, sessionService), BasicAuthenticationFilter.class); + //@formatter:on return http.build(); } diff --git a/src/main/java/de/focusshift/zeiterfassung/security/SessionService.java b/src/main/java/de/focusshift/zeiterfassung/security/SessionService.java new file mode 100644 index 000000000..40878673f --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/security/SessionService.java @@ -0,0 +1,21 @@ +package de.focusshift.zeiterfassung.security; + + +public interface SessionService { + + String RELOAD_AUTHORITIES = "reloadAuthorities"; + + /** + * Mark the session of the given username to reload the authorities on the next page request + * + * @param username to mark to reload authorities + */ + void markSessionToReloadAuthorities(String username); + + /** + * Unmark the session to not reload the authorities. + * + * @param sessionId to unmark the session + */ + void unmarkSessionToReloadAuthorities(String sessionId); +} diff --git a/src/main/java/de/focusshift/zeiterfassung/security/SessionServiceImpl.java b/src/main/java/de/focusshift/zeiterfassung/security/SessionServiceImpl.java new file mode 100644 index 000000000..24c7936b9 --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/security/SessionServiceImpl.java @@ -0,0 +1,35 @@ +package de.focusshift.zeiterfassung.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.Session; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Service +public class SessionServiceImpl implements SessionService { + + private final FindByIndexNameSessionRepository sessionRepository; + + @Autowired + SessionServiceImpl(FindByIndexNameSessionRepository sessionRepository) { + this.sessionRepository = sessionRepository; + } + + @Override + public void markSessionToReloadAuthorities(String username) { + final Map map = sessionRepository.findByPrincipalName(username); + for (final S session : map.values()) { + session.setAttribute(RELOAD_AUTHORITIES, true); + sessionRepository.save(session); + } + } + + @Override + public void unmarkSessionToReloadAuthorities(String sessionId) { + final S session = sessionRepository.findById(sessionId); + session.removeAttribute(RELOAD_AUTHORITIES); + sessionRepository.save(session); + } +} diff --git a/src/main/java/de/focusshift/zeiterfassung/security/SessionUpdater.java b/src/main/java/de/focusshift/zeiterfassung/security/SessionUpdater.java new file mode 100644 index 000000000..ef2cdbac3 --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/security/SessionUpdater.java @@ -0,0 +1,22 @@ +package de.focusshift.zeiterfassung.security; + +import de.focusshift.zeiterfassung.usermanagement.UserAuthoritiesUpdatedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +@Component +class SessionUpdater { + + private final SessionService sessionService; + + SessionUpdater(SessionService sessionService) { + this.sessionService = sessionService; + } + + @Async + @EventListener + public void handleUserAuthoritiesUpdated(UserAuthoritiesUpdatedEvent event) { + sessionService.markSessionToReloadAuthorities(event.user().id().value()); + } +} diff --git a/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUser.java b/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUser.java index 74717b8b9..429b10d57 100644 --- a/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUser.java +++ b/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUser.java @@ -1,4 +1,8 @@ package de.focusshift.zeiterfassung.tenantuser; -public record TenantUser(String id, Long localId, String givenName, String familyName, EMailAddress eMail) { +import de.focusshift.zeiterfassung.security.SecurityRoles; + +import java.util.Set; + +public record TenantUser(String id, Long localId, String givenName, String familyName, EMailAddress eMail, Set authorities) { } diff --git a/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUserCreatorAndUpdater.java b/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUserCreatorAndUpdater.java index bb2f3e7f3..4069ca208 100644 --- a/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUserCreatorAndUpdater.java +++ b/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUserCreatorAndUpdater.java @@ -1,12 +1,19 @@ package de.focusshift.zeiterfassung.tenantuser; +import de.focusshift.zeiterfassung.security.SecurityRoles; import org.springframework.context.event.EventListener; import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.stereotype.Component; import java.util.Optional; +import java.util.Set; import java.util.UUID; +import java.util.function.Predicate; + +import static java.util.function.Predicate.not; +import static java.util.stream.Collectors.toSet; @Component class TenantUserCreatorAndUpdater { @@ -23,14 +30,26 @@ public void handle(InteractiveAuthenticationSuccessEvent interactiveAuthenticati final UUID uuid = UUID.fromString(oidcUser.getSubject()); final EMailAddress eMailAddress = new EMailAddress(oidcUser.getEmail()); + final Set authorities = oidcUser.getAuthorities() + .stream() + .map(GrantedAuthority::getAuthority) + .filter(startsWith("ROLE_").and(not("ROLE_USER"::equals))) + .map(s -> s.substring("ROLE_".length())) + .map(SecurityRoles::valueOf) + .collect(toSet()); + final Optional maybeUser = tenantUserService.getUserByUuid(uuid); if (maybeUser.isEmpty()) { - tenantUserService.createNewUser(uuid, oidcUser.getGivenName(), oidcUser.getFamilyName(), eMailAddress); + tenantUserService.createNewUser(uuid, oidcUser.getGivenName(), oidcUser.getFamilyName(), eMailAddress, authorities); } else { final TenantUser user = maybeUser.get(); - final TenantUser tenantUser = new TenantUser(user.id(), user.localId(), oidcUser.getGivenName(), oidcUser.getFamilyName(), eMailAddress); + final TenantUser tenantUser = new TenantUser(user.id(), user.localId(), oidcUser.getGivenName(), oidcUser.getFamilyName(), eMailAddress, authorities); tenantUserService.updateUser(tenantUser); } } } + + private static Predicate startsWith(String prefix) { + return s -> s.startsWith(prefix); + } } diff --git a/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUserEntity.java b/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUserEntity.java index d097befa8..f549f1911 100644 --- a/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUserEntity.java +++ b/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUserEntity.java @@ -1,19 +1,19 @@ package de.focusshift.zeiterfassung.tenantuser; import de.focusshift.zeiterfassung.multitenant.AbstractTenantAwareEntity; +import de.focusshift.zeiterfassung.security.SecurityRoles; +import org.hibernate.annotations.LazyCollection; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.SequenceGenerator; -import javax.persistence.Table; +import javax.persistence.*; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import java.time.Instant; +import java.util.List; import java.util.Objects; +import static javax.persistence.EnumType.STRING; import static javax.persistence.GenerationType.SEQUENCE; +import static org.hibernate.annotations.LazyCollectionOption.FALSE; @Entity @Table(name = "tenant_user") @@ -53,15 +53,21 @@ public class TenantUserEntity extends AbstractTenantAwareEntity { @Size(max = 255) private String email; + @CollectionTable(name = "tenant_user_authorities", joinColumns = @JoinColumn(name = "tenant_user_id")) + @ElementCollection + @Enumerated(STRING) + @LazyCollection(FALSE) + private List authorities; + protected TenantUserEntity() { - this(null, null, null, null, null, null, null, null); + this(null, null, null, null, null, null, null, null, List.of()); } - protected TenantUserEntity(Long id, String uuid, Instant firstLoginAt, Instant lastLoginAt, String givenName, String familyName, String email) { - this(id, null, uuid, firstLoginAt, lastLoginAt, givenName, familyName, email); + protected TenantUserEntity(Long id, String uuid, Instant firstLoginAt, Instant lastLoginAt, String givenName, String familyName, String email, List authorities) { + this(id, null, uuid, firstLoginAt, lastLoginAt, givenName, familyName, email, authorities); } - protected TenantUserEntity(Long id, String tenantId, String uuid, Instant firstLoginAt, Instant lastLoginAt, String givenName, String familyName, String email) { + protected TenantUserEntity(Long id, String tenantId, String uuid, Instant firstLoginAt, Instant lastLoginAt, String givenName, String familyName, String email, List authorities) { super(tenantId); this.id = id; this.uuid = uuid; @@ -70,6 +76,7 @@ protected TenantUserEntity(Long id, String tenantId, String uuid, Instant firstL this.givenName = givenName; this.familyName = familyName; this.email = email; + this.authorities = authorities; } public Long getId() { @@ -100,6 +107,10 @@ public String getEmail() { return email; } + public List getAuthorities() { + return authorities; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUserRepository.java b/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUserRepository.java index fd186e773..1ee7b2e04 100644 --- a/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUserRepository.java +++ b/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUserRepository.java @@ -1,5 +1,7 @@ package de.focusshift.zeiterfassung.tenantuser; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.repository.CrudRepository; import org.springframework.lang.NonNull; @@ -12,4 +14,6 @@ interface TenantUserRepository extends CrudRepository { @NonNull List findAll(); + + Page findAll(Pageable pageable); } diff --git a/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUserService.java b/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUserService.java index 704e95b58..40d8ffb0a 100644 --- a/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUserService.java +++ b/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUserService.java @@ -1,5 +1,10 @@ package de.focusshift.zeiterfassung.tenantuser; +import de.focusshift.zeiterfassung.security.SecurityRoles; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -8,11 +13,13 @@ public interface TenantUserService { Optional getUserByUuid(UUID uuid); - TenantUser createNewUser(UUID uuid, String givenName, String familyName, EMailAddress eMailAddress); + TenantUser createNewUser(UUID uuid, String givenName, String familyName, EMailAddress eMailAddress, Collection authorities); TenantUser updateUser(TenantUser user); List findAllUsers(); + Page findAllUsers(Pageable pageable); + void deleteUser(Long id); } diff --git a/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUserServiceImpl.java b/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUserServiceImpl.java index ef7d4e49d..c517a522f 100644 --- a/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUserServiceImpl.java +++ b/src/main/java/de/focusshift/zeiterfassung/tenantuser/TenantUserServiceImpl.java @@ -1,13 +1,15 @@ package de.focusshift.zeiterfassung.tenantuser; +import de.focusshift.zeiterfassung.security.SecurityRoles; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import java.time.Clock; import java.time.Instant; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.util.*; @Service class TenantUserServiceImpl implements TenantUserService { @@ -29,12 +31,12 @@ public Optional getUserByUuid(UUID uuid) { } @Override - public TenantUser createNewUser(UUID uuid, String givenName, String familyName, EMailAddress eMailAddress) { + public TenantUser createNewUser(UUID uuid, String givenName, String familyName, EMailAddress eMailAddress, Collection authorities) { final Instant now = clock.instant(); final TenantUserEntity tenantUserEntity = - new TenantUserEntity(null, uuid.toString(), now, now, givenName, familyName, eMailAddress.value()); + new TenantUserEntity(null, uuid.toString(), now, now, givenName, familyName, eMailAddress.value(), distinct(authorities)); final TenantUserEntity persisted = tenantUserRepository.save(tenantUserEntity); @@ -53,7 +55,7 @@ public TenantUser updateUser(TenantUser user) { .orElseThrow(() -> new IllegalArgumentException(String.format("could not find user with id=%s", user.id()))); final TenantUserEntity next = - new TenantUserEntity(current.getId(), current.getUuid(), current.getFirstLoginAt(), now, user.givenName(), user.familyName(), user.eMail().value()); + new TenantUserEntity(current.getId(), current.getUuid(), current.getFirstLoginAt(), now, user.givenName(), user.familyName(), user.eMail().value(), distinct(user.authorities())); final TenantUserEntity persisted = tenantUserRepository.save(next); @@ -67,6 +69,14 @@ public List findAllUsers() { .toList(); } + @Override + public Page findAllUsers(Pageable pageable) { + final Page page = tenantUserRepository.findAll(pageable); + final List tenantUsers = page.stream().map(TenantUserServiceImpl::entityToTenantUser).toList(); + + return new PageImpl<>(tenantUsers, page.getPageable(), page.getTotalElements()); + } + @Override public void deleteUser(Long id) { tenantUserRepository.deleteById(id); @@ -78,7 +88,12 @@ private static TenantUser entityToTenantUser(TenantUserEntity tenantUserEntity) final String givenName = tenantUserEntity.getGivenName(); final String familyName = tenantUserEntity.getFamilyName(); final EMailAddress eMail = new EMailAddress(tenantUserEntity.getEmail()); + final HashSet authorities = new HashSet<>(tenantUserEntity.getAuthorities()); + + return new TenantUser(uuid, id, givenName, familyName, eMail, authorities); + } - return new TenantUser(uuid, id, givenName, familyName, eMail); + private static List distinct(Collection collection) { + return collection.stream().distinct().toList(); } } diff --git a/src/main/java/de/focusshift/zeiterfassung/timeclock/TimeClockControllerAdvice.java b/src/main/java/de/focusshift/zeiterfassung/timeclock/TimeClockControllerAdvice.java index 29eea2f8b..3595856d0 100644 --- a/src/main/java/de/focusshift/zeiterfassung/timeclock/TimeClockControllerAdvice.java +++ b/src/main/java/de/focusshift/zeiterfassung/timeclock/TimeClockControllerAdvice.java @@ -11,7 +11,13 @@ import java.time.Instant; import java.util.Optional; -@ControllerAdvice(basePackages = {"de.focusshift.zeiterfassung.feedback", "de.focusshift.zeiterfassung.report", "de.focusshift.zeiterfassung.timeclock", "de.focusshift.zeiterfassung.timeentry"}) +@ControllerAdvice(basePackages = { + "de.focusshift.zeiterfassung.feedback", + "de.focusshift.zeiterfassung.report", + "de.focusshift.zeiterfassung.timeclock", + "de.focusshift.zeiterfassung.timeentry", + "de.focusshift.zeiterfassung.usermanagement" +}) class TimeClockControllerAdvice { private final TimeClockService timeClockService; diff --git a/src/main/java/de/focusshift/zeiterfassung/usermanagement/User.java b/src/main/java/de/focusshift/zeiterfassung/usermanagement/User.java index 32fb7b598..919da3387 100644 --- a/src/main/java/de/focusshift/zeiterfassung/usermanagement/User.java +++ b/src/main/java/de/focusshift/zeiterfassung/usermanagement/User.java @@ -1,7 +1,14 @@ package de.focusshift.zeiterfassung.usermanagement; +import de.focusshift.zeiterfassung.security.SecurityRoles; import de.focusshift.zeiterfassung.tenantuser.EMailAddress; import de.focusshift.zeiterfassung.user.UserId; -public record User(UserId id, UserLocalId localId, String givenName, String familyName, EMailAddress email) { +import java.util.Set; + +public record User(UserId id, UserLocalId localId, String givenName, String familyName, EMailAddress email, Set authorities) { + + public boolean hasAuthority(SecurityRoles authority) { + return authorities().contains(authority); + } } diff --git a/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserAccountAuthoritiesDto.java b/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserAccountAuthoritiesDto.java new file mode 100644 index 000000000..cf4f07498 --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserAccountAuthoritiesDto.java @@ -0,0 +1,65 @@ +package de.focusshift.zeiterfassung.usermanagement; + +class UserAccountAuthoritiesDto { + + private boolean user; + private boolean viewReportAll; + private boolean editAuthorities; + + public boolean isUser() { + return user; + } + + public void setUser(boolean user) { + this.user = user; + } + + public boolean isViewReportAll() { + return viewReportAll; + } + + public void setViewReportAll(boolean viewReportAll) { + this.viewReportAll = viewReportAll; + } + + public boolean isEditAuthorities() { + return editAuthorities; + } + + public void setEditAuthorities(boolean editAuthorities) { + this.editAuthorities = editAuthorities; + } + + static Builder builder() { + return new Builder(); + } + + static class Builder { + private boolean user; + private boolean viewReportAll; + private boolean editAuthorities; + + public Builder user(boolean user) { + this.user = user; + return this; + } + + public Builder viewReportAll(boolean viewReportAll) { + this.viewReportAll = viewReportAll; + return this; + } + + public Builder editAuthorities(boolean editAuthorities) { + this.editAuthorities = editAuthorities; + return this; + } + + public UserAccountAuthoritiesDto build() { + final UserAccountAuthoritiesDto authoritiesDto = new UserAccountAuthoritiesDto(); + authoritiesDto.setUser(user); + authoritiesDto.setViewReportAll(viewReportAll); + authoritiesDto.setEditAuthorities(editAuthorities); + return authoritiesDto; + } + } +} diff --git a/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserAccountController.java b/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserAccountController.java new file mode 100644 index 000000000..766336c77 --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserAccountController.java @@ -0,0 +1,82 @@ +package de.focusshift.zeiterfassung.usermanagement; + +import de.focusshift.zeiterfassung.security.SecurityRoles; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static de.focusshift.zeiterfassung.security.SecurityRoles.*; +import static de.focusshift.zeiterfassung.security.SecurityRules.ALLOW_EDIT_AUTHORITIES; + +@Controller +@RequestMapping("/users/{id}/account") +@PreAuthorize(ALLOW_EDIT_AUTHORITIES) +class UserAccountController { + + private final UserManagementService userManagementService; + + UserAccountController(UserManagementService userManagementService) { + this.userManagementService = userManagementService; + } + + @GetMapping + String userAccount(@PathVariable("id") String id, Model model) { + + final User user = userById(id); + + model.addAttribute("userAccount", toUserAccountDto(user)); + + return "usermanagement/user-account"; + } + + @PostMapping("/authorities") + String userAuthorities(@PathVariable("id") String id, @ModelAttribute UserAccountAuthoritiesDto authoritiesDto, Model model) { + + final User user = userById(id); + + userManagementService.updateAuthorities(user, toAuthorities(authoritiesDto)); + + return "redirect:/users/%s/account".formatted(id); + } + + static UserAccountDto toUserAccountDto(User user) { + final UserAccountAuthoritiesDto authorities = toUserAccountAuthoritiesDto(user.authorities()); + return new UserAccountDto(user.localId().value(), user.givenName(), user.familyName(), fullName(user), user.email().value(), authorities); + } + + static Collection toAuthorities(UserAccountAuthoritiesDto authoritiesDto) { + final ArrayList authorities = new ArrayList<>(); + authorities.add(ZEITERFASSUNG_USER); + if (authoritiesDto.isViewReportAll()) { + authorities.add(ZEITERFASSUNG_VIEW_REPORT_ALL); + } + if (authoritiesDto.isEditAuthorities()) { + authorities.add(ZEITERFASSUNG_EDIT_AUTHORITIES); + } + return authorities; + } + + static UserAccountAuthoritiesDto toUserAccountAuthoritiesDto(Collection authorities) { + return UserAccountAuthoritiesDto.builder() + .user(authorities.contains(ZEITERFASSUNG_USER)) + .viewReportAll(authorities.contains(ZEITERFASSUNG_VIEW_REPORT_ALL)) + .editAuthorities(authorities.contains(ZEITERFASSUNG_EDIT_AUTHORITIES)) + .build(); + } + + private User userById(String id) { + return userManagementService.findAllUsersByLocalIds(List.of(UserLocalId.ofValue(id))) + .stream() + .findFirst() + .orElseThrow(); + } + + static String fullName(User user) { + return user.givenName() + " " + user.familyName(); + } +} diff --git a/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserAccountDto.java b/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserAccountDto.java new file mode 100644 index 000000000..08c349efa --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserAccountDto.java @@ -0,0 +1,4 @@ +package de.focusshift.zeiterfassung.usermanagement; + +public record UserAccountDto(Long id, String firstName, String lastName, String fullName, String email, UserAccountAuthoritiesDto authorities) { +} diff --git a/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserAuthoritiesUpdatedEvent.java b/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserAuthoritiesUpdatedEvent.java new file mode 100644 index 000000000..48400e1cf --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserAuthoritiesUpdatedEvent.java @@ -0,0 +1,4 @@ +package de.focusshift.zeiterfassung.usermanagement; + +public record UserAuthoritiesUpdatedEvent(User user) { +} diff --git a/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserDto.java b/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserDto.java new file mode 100644 index 000000000..cb0fa92da --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserDto.java @@ -0,0 +1,4 @@ +package de.focusshift.zeiterfassung.usermanagement; + +record UserDto(Long id, String firstName, String lastName, String fullName, String email, UserAccountAuthoritiesDto authorities) { +} diff --git a/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserLocalId.java b/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserLocalId.java index adbfdf2e3..774887a98 100644 --- a/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserLocalId.java +++ b/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserLocalId.java @@ -4,4 +4,8 @@ * Value identifying a user of the zeiterfassung application. */ public record UserLocalId(Long value) { + + public static UserLocalId ofValue(String value) { + return new UserLocalId(Long.valueOf(value)); + } } diff --git a/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserManagementController.java b/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserManagementController.java new file mode 100644 index 000000000..f62b84731 --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserManagementController.java @@ -0,0 +1,65 @@ +package de.focusshift.zeiterfassung.usermanagement; + +import de.focusshift.zeiterfassung.security.SecurityRoles; +import de.focusshift.zeiterfassung.web.html.PaginationDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.util.Collection; +import java.util.List; +import java.util.stream.IntStream; + +import static de.focusshift.zeiterfassung.security.SecurityRoles.*; +import static de.focusshift.zeiterfassung.security.SecurityRules.ALLOW_EDIT_AUTHORITIES; +import static de.focusshift.zeiterfassung.web.html.PaginationPageLinkBuilder.buildPageLinkPrefix; +import static java.util.stream.Collectors.toList; + +@Controller +@RequestMapping("/users") +@PreAuthorize(ALLOW_EDIT_AUTHORITIES) +class UserManagementController { + + private final UserManagementService userManagementService; + + UserManagementController(UserManagementService userManagementService) { + this.userManagementService = userManagementService; + } + + @GetMapping + String users(Pageable pageable, Model model) { + + final Page page = userManagementService.findAllUsers(pageable); + final List userDtos = page.map(UserManagementController::userToDto).toList(); + final PageImpl dtoPage = new PageImpl<>(userDtos, page.getPageable(), page.getTotalElements()); + + final String pageLinkPrefix = buildPageLinkPrefix(dtoPage.getPageable()); + final PaginationDto usersPagination = new PaginationDto<>(dtoPage, pageLinkPrefix); + model.addAttribute("usersPagination", usersPagination); + model.addAttribute("paginationPageNumbers", IntStream.rangeClosed(1, dtoPage.getTotalPages()).boxed().collect(toList())); + + return "usermanagement/users"; + } + + static UserDto userToDto(User user) { + final UserAccountAuthoritiesDto authorities = toUserAccountAuthoritiesDto(user.authorities()); + return new UserDto(user.localId().value(), user.givenName(), user.familyName(), fullName(user), user.email().value(), authorities); + } + + static UserAccountAuthoritiesDto toUserAccountAuthoritiesDto(Collection authorities) { + return UserAccountAuthoritiesDto.builder() + .user(authorities.contains(ZEITERFASSUNG_USER)) + .viewReportAll(authorities.contains(ZEITERFASSUNG_VIEW_REPORT_ALL)) + .editAuthorities(authorities.contains(ZEITERFASSUNG_EDIT_AUTHORITIES)) + .build(); + } + + static String fullName(User user) { + return user.givenName() + " " + user.familyName(); + } +} diff --git a/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserManagementService.java b/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserManagementService.java index 612c070dd..6652f70b1 100644 --- a/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserManagementService.java +++ b/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserManagementService.java @@ -1,7 +1,11 @@ package de.focusshift.zeiterfassung.usermanagement; +import de.focusshift.zeiterfassung.security.SecurityRoles; import de.focusshift.zeiterfassung.user.UserId; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import java.util.Collection; import java.util.List; import java.util.Optional; @@ -11,7 +15,11 @@ public interface UserManagementService { List findAllUsers(); + Page findAllUsers(Pageable pageable); + List findAllUsersByIds(List ids); List findAllUsersByLocalIds(List localIds); + + User updateAuthorities(User user, Collection toAuthorities); } diff --git a/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserManagementServiceImpl.java b/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserManagementServiceImpl.java index 30f1a5845..29f7d9af2 100644 --- a/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserManagementServiceImpl.java +++ b/src/main/java/de/focusshift/zeiterfassung/usermanagement/UserManagementServiceImpl.java @@ -1,10 +1,17 @@ package de.focusshift.zeiterfassung.usermanagement; +import de.focusshift.zeiterfassung.security.SecurityRoles; import de.focusshift.zeiterfassung.tenantuser.TenantUser; import de.focusshift.zeiterfassung.tenantuser.TenantUserService; import de.focusshift.zeiterfassung.user.UserId; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Optional; @@ -12,9 +19,11 @@ class UserManagementServiceImpl implements UserManagementService { private final TenantUserService tenantUserService; + private final ApplicationEventPublisher publisher; - UserManagementServiceImpl(TenantUserService tenantUserService) { + UserManagementServiceImpl(TenantUserService tenantUserService, ApplicationEventPublisher publisher) { this.tenantUserService = tenantUserService; + this.publisher = publisher; } @Override @@ -30,6 +39,14 @@ public List findAllUsers() { .toList(); } + @Override + public Page findAllUsers(Pageable pageable) { + final Page page = tenantUserService.findAllUsers(pageable); + final List users = page.map(UserManagementServiceImpl::tenantUserToUser).toList(); + + return new PageImpl<>(users, page.getPageable(), page.getTotalElements()); + } + @Override public List findAllUsersByIds(List userIds) { return findAllUsers().stream().filter(user -> userIds.contains(user.id())).toList(); @@ -40,7 +57,22 @@ public List findAllUsersByLocalIds(List localIds) { return findAllUsers().stream().filter(user -> localIds.contains(user.localId())).toList(); } + @Override + public User updateAuthorities(User user, Collection authorities) { + + final TenantUser tenantUser = new TenantUser(user.id().value(), user.localId().value(), user.givenName(), + user.familyName(), user.email(), new HashSet<>(authorities)); + + final TenantUser updatedTenantUser = tenantUserService.updateUser(tenantUser); + final User updatedUser = tenantUserToUser(updatedTenantUser); + + publisher.publishEvent(new UserAuthoritiesUpdatedEvent(updatedUser)); + + return updatedUser; + } + private static User tenantUserToUser(TenantUser tenantUser) { - return new User(new UserId(tenantUser.id()), new UserLocalId(tenantUser.localId()), tenantUser.givenName(), tenantUser.familyName(), tenantUser.eMail()); + return new User(new UserId(tenantUser.id()), new UserLocalId(tenantUser.localId()), tenantUser.givenName(), + tenantUser.familyName(), tenantUser.eMail(), tenantUser.authorities()); } } diff --git a/src/main/java/de/focusshift/zeiterfassung/web/html/PaginationDto.java b/src/main/java/de/focusshift/zeiterfassung/web/html/PaginationDto.java new file mode 100644 index 000000000..cde815272 --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/web/html/PaginationDto.java @@ -0,0 +1,37 @@ +package de.focusshift.zeiterfassung.web.html; + +import org.springframework.data.domain.Page; + +import java.util.Objects; + +public class PaginationDto { + + private final Page page; + private final String pageLinkPrefix; + public PaginationDto(Page page, String pageLinkPrefix) { + this.page = page; + this.pageLinkPrefix = pageLinkPrefix; + } + + public Page getPage() { + return page; + } + + public String hrefForPage(int pageNumber) { + // TODO "page" is configurable in application properties with `spring.data.web.pageable.page-parameter=page` + return pageLinkPrefix + "&page=" + pageNumber; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PaginationDto that = (PaginationDto) o; + return Objects.equals(page, that.page) && Objects.equals(pageLinkPrefix, that.pageLinkPrefix); + } + + @Override + public int hashCode() { + return Objects.hash(page, pageLinkPrefix); + } +} diff --git a/src/main/java/de/focusshift/zeiterfassung/web/html/PaginationPageLinkBuilder.java b/src/main/java/de/focusshift/zeiterfassung/web/html/PaginationPageLinkBuilder.java new file mode 100644 index 000000000..96c802569 --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/web/html/PaginationPageLinkBuilder.java @@ -0,0 +1,53 @@ +package de.focusshift.zeiterfassung.web.html; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.emptyMap; + +public class PaginationPageLinkBuilder { + + private PaginationPageLinkBuilder() { + } + + /** + * Builds a pagination link prefix without the `page` query parameter. + * + * @param pageable + * @return + */ + public static String buildPageLinkPrefix(Pageable pageable) { + return buildPageLinkPrefix(pageable, emptyMap()); + } + + /** + * Builds a pagination link prefix without the `page` query parameter. + * + * @param pageable + * @param parameters + * @return + */ + public static String buildPageLinkPrefix(Pageable pageable, Map parameters) { + + final List linkParameters = new ArrayList<>(); + + for (Map.Entry entry : parameters.entrySet()) { + linkParameters.add(entry.getKey() + "=" + entry.getValue()); + } + + for (Sort.Order order : pageable.getSort()) { + linkParameters.add("sort=" + order.getProperty() + "," + order.getDirection()); + } + + if (pageable.isPaged()) { + // TODO 'size' name is configurable `spring.data.web.pageable.size-parameter=size` + linkParameters.add("size=" + pageable.getPageSize()); + } + + return "?" + String.join("&", linkParameters); + } +} diff --git a/src/main/javascript/bundles/user-common.ts b/src/main/javascript/bundles/user-common.ts index 7320c6f5d..41349a256 100644 --- a/src/main/javascript/bundles/user-common.ts +++ b/src/main/javascript/bundles/user-common.ts @@ -1,5 +1,6 @@ import "../components/avatar"; import "../components/feedback-form"; +import "../components/form"; import "../components/navigation"; import "../components/time-clock"; import { initFeedbackHeartView } from "../components/feedback-heart"; diff --git a/src/main/javascript/components/form/autosubmit.js b/src/main/javascript/components/form/autosubmit.js new file mode 100644 index 000000000..9bea59e4f --- /dev/null +++ b/src/main/javascript/components/form/autosubmit.js @@ -0,0 +1,154 @@ +let keyupSubmit; + +document.addEventListener("keyup", function (event) { + if ( + event.defaultPrevented || + event.metaKey || + whitespaceKeys.has(event.key) || + modifierKeys.has(event.key) || + navigationKeys.has(event.key) || + uiKeys.has(event.key) || + deviceKeys.has(event.key) || + functionKeys.has(event.key) || + mediaKeys.has(event.key) || + audioControlKeys.has(event.key) + ) { + return; + } + + const { autoSubmit = "", autoSubmitDelay = 0 } = event.target.dataset; + if (autoSubmit) { + const button = document.querySelector("#" + autoSubmit); + if (button) { + const submit = () => button.click(); + if (autoSubmitDelay) { + clearTimeout(keyupSubmit); + keyupSubmit = setTimeout(submit, Number(autoSubmitDelay)); + } else { + submit(); + } + } + } +}); + +document.addEventListener("change", function (event) { + if (event.defaultPrevented) { + return; + } + + const { autoSubmit = "" } = event.target.dataset; + if (autoSubmit) { + const button = document.querySelector("#" + autoSubmit); + if (button) { + button.closest("form").requestSubmit(button); + } + } +}); + +const whitespaceKeys = new Set(["Enter", "Tab", "Alt"]); + +const modifierKeys = new Set([ + "AltGraph", + "CapsLock", + "Control", + "Fn", + "FnLock", + "Hyper", + "Meta", + "NumLock", + "ScrollLock", + "Shift", + "Super", + "Symbol", + "SymbolLock", +]); + +const navigationKeys = new Set([ + "ArrowDown", + "ArrowLeft", + "ArrowRight", + "ArrowUp", + "End", + "Home", + "PageDown", + "PageUp", + "End", +]); + +const uiKeys = new Set([ + "Accept", + "ContextMenu", + "Execute", + "Find", + "Help", + "Pause", + "Play", + "Props", + "Select", + "ZoomIn", + "ZoomOut", +]); + +const deviceKeys = new Set([ + "BrightnessDown", + "BrightnessUp", + "Eject", + "LogOff", + "Power", + "PowerOff", + "PrintScreen", + "Hibernate", + "Standby", + "WakeUp", +]); + +const functionKeys = new Set([ + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12", + "F13", + "F14", + "F15", + "F16", + "F17", + "F18", + "F19", + "F20", + "Soft1", + "Soft2", + "Soft3", + "Soft4", +]); + +const mediaKeys = new Set([ + "ChannelDown", + "ChannelUp", + "MediaFastForward", + "MediaPause", + "MediaPlay", + "MediaPlayPause", + "MediaRecord", + "MediaRewind", + "MediaStop", + "MediaTrackNext", + "MediaTrackPrevious", +]); + +const audioControlKeys = new Set([ + "AudioVolumeDown", + "AudioVolumeMute", + "AudioVolumeUp", + "MicrophoneToggle", + "MicrophoneVolumeDown", + "MicrophoneVolumeMute", + "MicrophoneVolumeUp", +]); diff --git a/src/main/javascript/components/form/index.js b/src/main/javascript/components/form/index.js new file mode 100644 index 000000000..8e12045ae --- /dev/null +++ b/src/main/javascript/components/form/index.js @@ -0,0 +1 @@ +import "./autosubmit"; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3411bd32d..15de95804 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -28,9 +28,16 @@ spring.jpa.hibernate.ddl-auto=none spring.jpa.open-in-view=false spring.liquibase.change-log=classpath:/db/changelog/db.changelog-main.xml +# SESSION +spring.session.store-type=jdbc +spring.session.jdbc.initialize-schema=never + # i18n spring.messages.fallback-to-system-locale=false +spring.data.web.pageable.default-page-size=50 +spring.data.web.pageable.one-indexed-parameters=true + # Cache & compression spring.web.resources.cache.period=365d spring.web.resources.cache.cachecontrol.no-cache=false diff --git a/src/main/resources/db/changelog/changelog-1.1.0-add-authorities-to-tenant-user.xml b/src/main/resources/db/changelog/changelog-1.1.0-add-authorities-to-tenant-user.xml new file mode 100644 index 000000000..d5a598ff9 --- /dev/null +++ b/src/main/resources/db/changelog/changelog-1.1.0-add-authorities-to-tenant-user.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/db/changelog/changelog-1.1.0-add-spring-session.xml b/src/main/resources/db/changelog/changelog-1.1.0-add-spring-session.xml new file mode 100644 index 000000000..77dfa8b43 --- /dev/null +++ b/src/main/resources/db/changelog/changelog-1.1.0-add-spring-session.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/db/changelog/db.changelog-main.xml b/src/main/resources/db/changelog/db.changelog-main.xml index 205b9d309..392ab7c6c 100644 --- a/src/main/resources/db/changelog/db.changelog-main.xml +++ b/src/main/resources/db/changelog/db.changelog-main.xml @@ -8,5 +8,7 @@ + + diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index f5a91ff4b..ef4b7531c 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -7,6 +7,10 @@ error.page-not-found.title=Seite nicht gefunden error.page-not-found.paragraph=Entschuldigung. Ich habe die Seite überall gesucht und konnte sie leider nicht finden. error.page-not-found.goto.previous=Zurück zur vorigen Seite error.page-not-found.goto.start=Weiter zur Startseite +error.not-authorized.title=Keine Berechtigung +error.not-authorized.paragraph=Du hast keine Berechtigung diese Seite zu sehen. Melde dich bei einer verantwortlichen Person, falls dies angepasst werden muss. +error.not-authorized.goto.previous=Zurück zur vorigen Seite +error.not-authorized.goto.start=Weiter zur Startseite error.server-error.title=Uuups, das ist mein Fehler. error.server-error.paragraph=Ich bin nicht sicher was passiert ist, aber der Server sagt etwas ist schief gelaufen. Ich werde das herausfinden und lösen! Probiere es bitte später noch einmal. error.server-error.goto.previous=Zurück zur vorigen Seite @@ -159,3 +163,15 @@ feedback.form.close=Schließen feedback.form.submit=Feedback geben feedback.form.cancel=Schließen feedback.kudos=Vielen Dank! Wir freuen uns über dein wertvolles Feedback! + +users.pagination.total.text=Personen +users.pagination.navigation.aria-label=Personenübersicht Seitenzahlen + +# pagination +pagination.page=Seite {0} +pagination.page.last=Letzte Seite, Seite {0} +pagination.page.next=Nächste Seite +pagination.page.previous=Vorherige Seite +pagination.page.size.label.text=Zeige +pagination.page.size.total.text=von {0} {1} +pagination.page.size.button.text=Aktualisieren diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index c0845ade0..e9bdbbd98 100644 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -7,6 +7,10 @@ error.page-not-found.title=Not Found error.page-not-found.paragraph=Sorry, I looked everywhere, and still came up empty handed. error.page-not-found.goto.previous=Back to previous page error.page-not-found.goto.start=Continue on startpage +error.not-authorized.title=Not authorized +error.not-authorized.paragraph=You do not have permission to view this page. Contact a responsible person if your permission needs to be adjusted. +error.not-authorized.goto.previous=Back to previous page +error.not-authorized.goto.start=Continue on startpage error.server-error.title=Oops, that's my bad. error.server-error.paragraph=I'm not exactly sure what happened, but the server says something is wrong. error.server-error.goto.previous=Back to previous page @@ -158,3 +162,15 @@ feedback.form.close=Close feedback.form.submit=Send feedback feedback.form.cancel=Close feedback.kudos=Thank you! We appreciate your valuable feedback! + +users.pagination.total.text=Users +users.pagination.navigation.aria-label=User overview pagination + +# pagination +pagination.page=Page {0} +pagination.page.last=Last page, page {0} +pagination.page.next=Next page +pagination.page.previous=Previous page +pagination.page.size.label.text=Show +pagination.page.size.total.text=of {0} {1} +pagination.page.size.button.text=Refresh diff --git a/src/main/resources/static/images/403_sloth.svg b/src/main/resources/static/images/403_sloth.svg new file mode 100644 index 000000000..a53d4ab9a --- /dev/null +++ b/src/main/resources/static/images/403_sloth.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/templates/error/403.html b/src/main/resources/templates/error/403.html new file mode 100644 index 000000000..fe364f162 --- /dev/null +++ b/src/main/resources/templates/error/403.html @@ -0,0 +1,55 @@ + + + + Zeiterfassung + + + +
+
+
+

+ 403 + +

+

+
+ + +
+
+ +
+
+
+ + diff --git a/src/main/resources/templates/fragments/pagination.html b/src/main/resources/templates/fragments/pagination.html new file mode 100644 index 000000000..524e22331 --- /dev/null +++ b/src/main/resources/templates/fragments/pagination.html @@ -0,0 +1,100 @@ + + + + + pagination + + + + + diff --git a/src/main/resources/templates/fragments/select.html b/src/main/resources/templates/fragments/select.html new file mode 100644 index 000000000..792ec9340 --- /dev/null +++ b/src/main/resources/templates/fragments/select.html @@ -0,0 +1,39 @@ + + + + + select + + + + + + + + + + + + diff --git a/src/main/resources/templates/timeentries/_navigation.html b/src/main/resources/templates/timeentries/_navigation.html index 12b958c31..aeac930d6 100644 --- a/src/main/resources/templates/timeentries/_navigation.html +++ b/src/main/resources/templates/timeentries/_navigation.html @@ -31,9 +31,9 @@ diff --git a/src/main/resources/templates/usermanagement/user-account.html b/src/main/resources/templates/usermanagement/user-account.html new file mode 100644 index 000000000..2f60862af --- /dev/null +++ b/src/main/resources/templates/usermanagement/user-account.html @@ -0,0 +1,151 @@ + + + + Zeiterfassung - Personen + + + + + + +
+
+ +
+
+

+ Konto von + +

+
+
+ +
+ +
+
+
+

+ + Berechtigungen +

+
+ +
    +
  • +

    Berichte

    +
      +
    • + + +
    • +
    +
  • +
  • +

    Berechtigungen

    +
      +
    • + + +
    • +
    +
  • +
+
+ +
+
+
+
+
+
+
+ + diff --git a/src/main/resources/templates/usermanagement/users.html b/src/main/resources/templates/usermanagement/users.html new file mode 100644 index 000000000..7d8f6d37b --- /dev/null +++ b/src/main/resources/templates/usermanagement/users.html @@ -0,0 +1,212 @@ + + + + Zeiterfassung - Personen + + + +
+
+

Personen

+
+ + + + + + + + + + + + + + + + + + + + + + +
+ Personen +

+ Information: Es werden hier nur Personen angezeigt, welche + sich mindestens einmal in der Zeiterfassung angemeldet haben. +

+
VornameNachnameE-Mail + Berechtigungen + Aktionen
+ + +
+ + Benutzer + + + + + Berichte sehen + + + + + Berechtigungen pflegen + + +
+
+ + Konto + +
+
+ +
+
+ + + + + + von 42 Personen + +
+ +
+
+
+
+
+
+ + diff --git a/src/test/java/de/focusshift/zeiterfassung/report/ReportControllerTest.java b/src/test/java/de/focusshift/zeiterfassung/report/ReportControllerTest.java index 0426212c6..df785ff56 100644 --- a/src/test/java/de/focusshift/zeiterfassung/report/ReportControllerTest.java +++ b/src/test/java/de/focusshift/zeiterfassung/report/ReportControllerTest.java @@ -16,12 +16,9 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.threeten.extra.YearWeek; -import java.time.Clock; -import java.time.LocalDate; -import java.time.Month; -import java.time.Year; -import java.time.YearMonth; +import java.time.*; import java.util.List; +import java.util.Set; import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oidcLogin; @@ -162,7 +159,7 @@ void ensureWeekReportCsvDownloadUrlWithUsersParam() throws Exception { void ensureWeekReportUserFilterRelatedUrlsAreNotAddedWhenCurrentUserHasNoPermission() throws Exception { when(reportPermissionService.findAllPermittedUsersForCurrentUser()) - .thenReturn(List.of(new User(new UserId("batman"), new UserLocalId(1L), "", "", new EMailAddress("")))); + .thenReturn(List.of(new User(new UserId("batman"), new UserLocalId(1L), "", "", new EMailAddress(""), Set.of()))); when(reportService.getReportWeek(Year.of(2022), 1, new UserId("batman"))) .thenReturn(anyReportWeek()); @@ -174,9 +171,9 @@ void ensureWeekReportUserFilterRelatedUrlsAreNotAddedWhenCurrentUserHasNoPermiss @Test void ensureWeekReportUserFilterRelatedUrls() throws Exception { - final User batman = new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress("")); - final User joker = new User(new UserId("joker"), new UserLocalId(2L), "Jack", "Napier", new EMailAddress("")); - final User robin = new User(new UserId("robin"), new UserLocalId(3L), "Dick", "Grayson", new EMailAddress("")); + final User batman = new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress(""), Set.of()); + final User joker = new User(new UserId("joker"), new UserLocalId(2L), "Jack", "Napier", new EMailAddress(""), Set.of()); + final User robin = new User(new UserId("robin"), new UserLocalId(3L), "Dick", "Grayson", new EMailAddress(""), Set.of()); when(reportPermissionService.findAllPermittedUsersForCurrentUser()) .thenReturn(List.of(batman, joker, robin)); @@ -198,9 +195,9 @@ void ensureWeekReportUserFilterRelatedUrls() throws Exception { @Test void ensureWeekReportUserFilterRelatedUrlsForEveryone() throws Exception { - final User batman = new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress("")); - final User joker = new User(new UserId("joker"), new UserLocalId(2L), "Jack", "Napier", new EMailAddress("")); - final User robin = new User(new UserId("robin"), new UserLocalId(3L), "Dick", "Grayson", new EMailAddress("")); + final User batman = new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress(""), Set.of()); + final User joker = new User(new UserId("joker"), new UserLocalId(2L), "Jack", "Napier", new EMailAddress(""), Set.of()); + final User robin = new User(new UserId("robin"), new UserLocalId(3L), "Dick", "Grayson", new EMailAddress(""), Set.of()); when(reportPermissionService.findAllPermittedUsersForCurrentUser()) .thenReturn(List.of(batman, joker, robin)); @@ -226,9 +223,9 @@ void ensureWeekReportUserFilterRelatedUrlsForEveryone() throws Exception { @Test void ensureWeekReportUserFilterRelatedUrlsForWithSelectedUser() throws Exception { - final User batman = new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress("")); - final User joker = new User(new UserId("joker"), new UserLocalId(2L), "Jack", "Napier", new EMailAddress("")); - final User robin = new User(new UserId("robin"), new UserLocalId(3L), "Dick", "Grayson", new EMailAddress("")); + final User batman = new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress(""), Set.of()); + final User joker = new User(new UserId("joker"), new UserLocalId(2L), "Jack", "Napier", new EMailAddress(""), Set.of()); + final User robin = new User(new UserId("robin"), new UserLocalId(3L), "Dick", "Grayson", new EMailAddress(""), Set.of()); when(reportPermissionService.findAllPermittedUsersForCurrentUser()) .thenReturn(List.of(batman, joker, robin)); @@ -357,7 +354,7 @@ void ensureMonthReportCsvDownloadUrlWithUsersParam() throws Exception { void ensureMonthReportUserFilterRelatedUrlsAreNotAddedWhenCurrentUserHasNoPermission() throws Exception { when(reportPermissionService.findAllPermittedUsersForCurrentUser()) - .thenReturn(List.of(new User(new UserId("batman"), new UserLocalId(1L), "", "", new EMailAddress("")))); + .thenReturn(List.of(new User(new UserId("batman"), new UserLocalId(1L), "", "", new EMailAddress(""), Set.of()))); when(reportService.getReportMonth(YearMonth.of(2022, 1), new UserId("batman"))) .thenReturn(anyReportMonth()); @@ -369,9 +366,9 @@ void ensureMonthReportUserFilterRelatedUrlsAreNotAddedWhenCurrentUserHasNoPermis @Test void ensureMonthReportUserFilterRelatedUrls() throws Exception { - final User batman = new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress("")); - final User joker = new User(new UserId("joker"), new UserLocalId(2L), "Jack", "Napier", new EMailAddress("")); - final User robin = new User(new UserId("robin"), new UserLocalId(3L), "Dick", "Grayson", new EMailAddress("")); + final User batman = new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress(""), Set.of()); + final User joker = new User(new UserId("joker"), new UserLocalId(2L), "Jack", "Napier", new EMailAddress(""), Set.of()); + final User robin = new User(new UserId("robin"), new UserLocalId(3L), "Dick", "Grayson", new EMailAddress(""), Set.of()); when(reportPermissionService.findAllPermittedUsersForCurrentUser()) .thenReturn(List.of(batman, joker, robin)); @@ -393,9 +390,9 @@ void ensureMonthReportUserFilterRelatedUrls() throws Exception { @Test void ensureMonthReportUserFilterRelatedUrlsForEveryone() throws Exception { - final User batman = new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress("")); - final User joker = new User(new UserId("joker"), new UserLocalId(2L), "Jack", "Napier", new EMailAddress("")); - final User robin = new User(new UserId("robin"), new UserLocalId(3L), "Dick", "Grayson", new EMailAddress("")); + final User batman = new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress(""), Set.of()); + final User joker = new User(new UserId("joker"), new UserLocalId(2L), "Jack", "Napier", new EMailAddress(""), Set.of()); + final User robin = new User(new UserId("robin"), new UserLocalId(3L), "Dick", "Grayson", new EMailAddress(""), Set.of()); when(reportPermissionService.findAllPermittedUsersForCurrentUser()) .thenReturn(List.of(batman, joker, robin)); @@ -421,9 +418,9 @@ void ensureMonthReportUserFilterRelatedUrlsForEveryone() throws Exception { @Test void ensureMonthReportUserFilterRelatedUrlsForWithSelectedUser() throws Exception { - final User batman = new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress("")); - final User joker = new User(new UserId("joker"), new UserLocalId(2L), "Jack", "Napier", new EMailAddress("")); - final User robin = new User(new UserId("robin"), new UserLocalId(3L), "Dick", "Grayson", new EMailAddress("")); + final User batman = new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress(""), Set.of()); + final User joker = new User(new UserId("joker"), new UserLocalId(2L), "Jack", "Napier", new EMailAddress(""), Set.of()); + final User robin = new User(new UserId("robin"), new UserLocalId(3L), "Dick", "Grayson", new EMailAddress(""), Set.of()); when(reportPermissionService.findAllPermittedUsersForCurrentUser()) .thenReturn(List.of(batman, joker, robin)); diff --git a/src/test/java/de/focusshift/zeiterfassung/report/ReportCsvServiceTest.java b/src/test/java/de/focusshift/zeiterfassung/report/ReportCsvServiceTest.java index 5abeba178..4b2110746 100644 --- a/src/test/java/de/focusshift/zeiterfassung/report/ReportCsvServiceTest.java +++ b/src/test/java/de/focusshift/zeiterfassung/report/ReportCsvServiceTest.java @@ -15,15 +15,11 @@ import java.io.PrintWriter; import java.io.StringWriter; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.Year; -import java.time.YearMonth; -import java.time.ZoneId; -import java.time.ZonedDateTime; +import java.time.*; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Locale; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -80,7 +76,7 @@ void ensureWeekReportCsvRoundsWorkedHoursToTwoDigit() { mockDateFormatter("dd.MM.yyyy"); - final User batman = new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress("batman@example.org")); + final User batman = new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress("batman@example.org"), Set.of()); final ZonedDateTime from = ZonedDateTime.of(LocalDateTime.of(2021, 1, 4, 10, 0), ZONE_ID_BERLIN); final ZonedDateTime to = ZonedDateTime.of(LocalDateTime.of(2021, 1, 4, 10, 30), ZONE_ID_BERLIN); @@ -106,7 +102,7 @@ void ensureWeekReportCsvContainsSummarizedInfoPerDay() { mockDateFormatter("dd.MM.yyyy"); - final User batman = new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress("batman@example.org")); + final User batman = new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress("batman@example.org"), Set.of()); // day one final ZonedDateTime d1_1_From = ZonedDateTime.of(LocalDateTime.of(2021, 1, 4, 10, 0), ZONE_ID_BERLIN); @@ -165,7 +161,7 @@ void ensureMonthReportCsvRoundsWorkedHoursToTwoDigit() { mockDateFormatter("dd.MM.yyyy"); - final User batman = new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress("batman@example.org")); + final User batman = new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress("batman@example.org"), Set.of()); final ZonedDateTime from = ZonedDateTime.of(LocalDateTime.of(2021, 1, 4, 10, 0), ZONE_ID_BERLIN); final ZonedDateTime to = ZonedDateTime.of(LocalDateTime.of(2021, 1, 4, 10, 30), ZONE_ID_BERLIN); @@ -197,7 +193,7 @@ void ensureMonthReportCsvContainsSummarizedInfoPerDay() { mockDateFormatter("dd.MM.yyyy"); - final User batman = new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress("batman@example.org")); + final User batman = new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress("batman@example.org"), Set.of()); // week one, day one final ZonedDateTime d1_1_From = ZonedDateTime.of(LocalDateTime.of(2021, 1, 4, 10, 0), ZONE_ID_BERLIN); diff --git a/src/test/java/de/focusshift/zeiterfassung/report/ReportPermissionServiceTest.java b/src/test/java/de/focusshift/zeiterfassung/report/ReportPermissionServiceTest.java index 75713563c..ccf1fc238 100644 --- a/src/test/java/de/focusshift/zeiterfassung/report/ReportPermissionServiceTest.java +++ b/src/test/java/de/focusshift/zeiterfassung/report/ReportPermissionServiceTest.java @@ -14,6 +14,7 @@ import org.springframework.security.authentication.TestingAuthenticationToken; import java.util.List; +import java.util.Set; import static de.focusshift.zeiterfassung.security.SecurityRoles.ZEITERFASSUNG_VIEW_REPORT_ALL; import static org.assertj.core.api.Assertions.assertThat; @@ -72,7 +73,7 @@ void ensureFilterUserLocalIdsByCurrentUserHasPermissionForReturnsListForCurrentU .thenReturn(new TestingAuthenticationToken("", "", List.of())); when(currentUserProvider.getCurrentUser()) - .thenReturn(new User(new UserId(""), new UserLocalId(2L), "", "", new EMailAddress(""))); + .thenReturn(new User(new UserId(""), new UserLocalId(2L), "", "", new EMailAddress(""), Set.of())); final List actual = sut.filterUserLocalIdsByCurrentUserHasPermissionFor( List.of(new UserLocalId(1L), new UserLocalId(2L), new UserLocalId(3L))); @@ -87,7 +88,7 @@ void ensureFilterUserLocalIdsByCurrentUserHasPermissionForReturnsEmptyList() { .thenReturn(new TestingAuthenticationToken("", "", List.of())); when(currentUserProvider.getCurrentUser()) - .thenReturn(new User(new UserId(""), new UserLocalId(2L), "", "", new EMailAddress(""))); + .thenReturn(new User(new UserId(""), new UserLocalId(2L), "", "", new EMailAddress(""), Set.of())); final List actual = sut.filterUserLocalIdsByCurrentUserHasPermissionFor( List.of(new UserLocalId(1L), new UserLocalId(3L))); @@ -103,9 +104,9 @@ void ensureFindAllPermittedUserLocalIdsForCurrentUser() { when(userManagementService.findAllUsers()) .thenReturn(List.of( - new User(new UserId(""), new UserLocalId(1L), "", "", new EMailAddress("")), - new User(new UserId(""), new UserLocalId(2L), "", "", new EMailAddress("")), - new User(new UserId(""), new UserLocalId(3L), "", "", new EMailAddress("")) + new User(new UserId(""), new UserLocalId(1L), "", "", new EMailAddress(""), Set.of()), + new User(new UserId(""), new UserLocalId(2L), "", "", new EMailAddress(""), Set.of()), + new User(new UserId(""), new UserLocalId(3L), "", "", new EMailAddress(""), Set.of()) )); assertThat(sut.findAllPermittedUserLocalIdsForCurrentUser()) @@ -119,7 +120,7 @@ void ensureFindAllPermittedUserLocalIdsForCurrentUserReturnsCurrentUserOnly() { .thenReturn(new TestingAuthenticationToken("", "", List.of())); when(currentUserProvider.getCurrentUser()) - .thenReturn(new User(new UserId(""), new UserLocalId(2L), "", "", new EMailAddress(""))); + .thenReturn(new User(new UserId(""), new UserLocalId(2L), "", "", new EMailAddress(""), Set.of())); assertThat(sut.findAllPermittedUserLocalIdsForCurrentUser()).containsOnly(new UserLocalId(2L)); } @@ -131,9 +132,9 @@ void ensureFindAllPermittedUsersForCurrentUser() { .thenReturn(new TestingAuthenticationToken("", "", List.of(ZEITERFASSUNG_VIEW_REPORT_ALL.authority()))); final List userList = List.of( - new User(new UserId(""), new UserLocalId(1L), "", "", new EMailAddress("")), - new User(new UserId(""), new UserLocalId(2L), "", "", new EMailAddress("")), - new User(new UserId(""), new UserLocalId(3L), "", "", new EMailAddress("")) + new User(new UserId(""), new UserLocalId(1L), "", "", new EMailAddress(""), Set.of()), + new User(new UserId(""), new UserLocalId(2L), "", "", new EMailAddress(""), Set.of()), + new User(new UserId(""), new UserLocalId(3L), "", "", new EMailAddress(""), Set.of()) ); when(userManagementService.findAllUsers()).thenReturn(userList); @@ -148,9 +149,9 @@ void ensureFindAllPermittedUsersForCurrentUserReturnsOnlyItself() { .thenReturn(new TestingAuthenticationToken("", "", List.of())); when(currentUserProvider.getCurrentUser()) - .thenReturn(new User(new UserId(""), new UserLocalId(2L), "", "", new EMailAddress(""))); + .thenReturn(new User(new UserId(""), new UserLocalId(2L), "", "", new EMailAddress(""), Set.of())); assertThat(sut.findAllPermittedUsersForCurrentUser()) - .containsOnly(new User(new UserId(""), new UserLocalId(2L), "", "", new EMailAddress(""))); + .containsOnly(new User(new UserId(""), new UserLocalId(2L), "", "", new EMailAddress(""), Set.of())); } } diff --git a/src/test/java/de/focusshift/zeiterfassung/report/ReportServiceRawTest.java b/src/test/java/de/focusshift/zeiterfassung/report/ReportServiceRawTest.java index 0fb26f4e9..08eae653f 100644 --- a/src/test/java/de/focusshift/zeiterfassung/report/ReportServiceRawTest.java +++ b/src/test/java/de/focusshift/zeiterfassung/report/ReportServiceRawTest.java @@ -14,14 +14,9 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.time.Duration; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.Year; -import java.time.YearMonth; -import java.time.ZoneId; -import java.time.ZonedDateTime; +import java.time.*; import java.util.List; +import java.util.Set; import static java.time.DayOfWeek.MONDAY; import static org.assertj.core.api.Assertions.assertThat; @@ -98,7 +93,7 @@ void ensureReportWeekWithOneTimeEntryADay() { .thenReturn(List.of(firstTimeEntry, secondTimeEntry)); when(userManagementService.findAllUsersByIds(List.of(new UserId("batman")))) - .thenReturn(List.of(new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress("batman@example.org")))); + .thenReturn(List.of(new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress("batman@example.org"), Set.of()))); final ReportWeek actualReportWeek = sut.getReportWeek(Year.of(2021), 1, new UserId("batman")); @@ -130,7 +125,7 @@ void ensureReportWeekWithMultipleTimeEntriesADay() { .thenReturn(List.of(morningTimeEntry, noonTimeEntry)); when(userManagementService.findAllUsersByIds(List.of(new UserId("batman")))) - .thenReturn(List.of(new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress("batman@example.org")))); + .thenReturn(List.of(new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress("batman@example.org"), Set.of()))); final ReportWeek actualReportWeek = sut.getReportWeek(Year.of(2021), 1, new UserId("batman")); @@ -158,7 +153,7 @@ void ensureReportWeekWithTimeEntryTouchingNextDayIsReportedForStartingDate() { .thenReturn(List.of(timeEntry)); when(userManagementService.findAllUsersByIds(List.of(new UserId("batman")))) - .thenReturn(List.of(new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress("batman@example.org")))); + .thenReturn(List.of(new User(new UserId("batman"), new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress("batman@example.org"), Set.of()))); final ReportWeek actualReportWeek = sut.getReportWeek(Year.of(2021), 1, new UserId("batman")); @@ -259,7 +254,7 @@ void ensureReportMonthDecemberWithOneTimeEntryAWeek() { .thenReturn(List.of(w1_d1_TimeEntry, w1_d2_TimeEntry, w2_d1_TimeEntry, w2_d2_TimeEntry, w3_d1_TimeEntry, w3_d2_TimeEntry, w4_d1_TimeEntry, w4_d2_TimeEntry)); when(userManagementService.findAllUsersByIds(List.of(batman))) - .thenReturn(List.of(new User(batman, new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress("batman@example.org")))); + .thenReturn(List.of(new User(batman, new UserLocalId(1L), "Bruce", "Wayne", new EMailAddress("batman@example.org"), Set.of()))); final ReportMonth actualReportMonth = sut.getReportMonth(YearMonth.of(2021, 1), batman); diff --git a/src/test/java/de/focusshift/zeiterfassung/timeentry/TimeEntryRepositoryIT.java b/src/test/java/de/focusshift/zeiterfassung/timeentry/TimeEntryRepositoryIT.java index 8fe0f121c..f3a7ee792 100644 --- a/src/test/java/de/focusshift/zeiterfassung/timeentry/TimeEntryRepositoryIT.java +++ b/src/test/java/de/focusshift/zeiterfassung/timeentry/TimeEntryRepositoryIT.java @@ -16,13 +16,9 @@ import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.test.annotation.Rollback; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.ZoneId; -import java.time.ZoneOffset; +import java.time.*; import java.util.List; +import java.util.Set; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @@ -46,8 +42,8 @@ void countAllByOwner() { tenantService.create("ab143c2f"); prepareSecurityContextWithTenantId("ab143c2f"); - final TenantUser batman = tenantUserService.createNewUser(UUID.randomUUID(), "Bruce", "Wayne", new EMailAddress("batman@example.org")); - final TenantUser superman = tenantUserService.createNewUser(UUID.randomUUID(), "Kent", "Clark", new EMailAddress("Clark@example.org")); + final TenantUser batman = tenantUserService.createNewUser(UUID.randomUUID(), "Bruce", "Wayne", new EMailAddress("batman@example.org"), Set.of()); + final TenantUser superman = tenantUserService.createNewUser(UUID.randomUUID(), "Kent", "Clark", new EMailAddress("Clark@example.org"), Set.of()); final LocalDateTime anyDate = LocalDateTime.of(2022, 9, 24, 14, 0, 0, 0); @@ -69,8 +65,8 @@ void ensureFindAllByOwnerForTimePeriodIncludesTimeEntryWithStartAtPeriodStartAnd tenantService.create("ab143c2f"); prepareSecurityContextWithTenantId("ab143c2f"); - final TenantUser batman = tenantUserService.createNewUser(UUID.randomUUID(), "Bruce", "Wayne", new EMailAddress("batman@example.org")); - final TenantUser superman = tenantUserService.createNewUser(UUID.randomUUID(), "Kent", "Clark", new EMailAddress("Clark@example.org")); + final TenantUser batman = tenantUserService.createNewUser(UUID.randomUUID(), "Bruce", "Wayne", new EMailAddress("batman@example.org"), Set.of()); + final TenantUser superman = tenantUserService.createNewUser(UUID.randomUUID(), "Kent", "Clark", new EMailAddress("Clark@example.org"), Set.of()); final LocalDate periodFrom = LocalDate.of(2022, 1, 3); final LocalDate periodToExclusive = LocalDate.of(2022, 1, 10); @@ -114,8 +110,8 @@ void ensureFindAllByOwnerForTimePeriodIncludesTimeEntryWithStartWithinPeriodAndE tenantService.create("ab143c2f"); prepareSecurityContextWithTenantId("ab143c2f"); - final TenantUser batman = tenantUserService.createNewUser(UUID.randomUUID(), "Bruce", "Wayne", new EMailAddress("batman@example.org")); - final TenantUser superman = tenantUserService.createNewUser(UUID.randomUUID(), "Kent", "Clark", new EMailAddress("Clark@example.org")); + final TenantUser batman = tenantUserService.createNewUser(UUID.randomUUID(), "Bruce", "Wayne", new EMailAddress("batman@example.org"), Set.of()); + final TenantUser superman = tenantUserService.createNewUser(UUID.randomUUID(), "Kent", "Clark", new EMailAddress("Clark@example.org"), Set.of()); final LocalDate periodFrom = LocalDate.of(2022, 1, 3); final LocalDate periodToExclusive = LocalDate.of(2022, 1, 10); @@ -159,8 +155,8 @@ void ensureFindAllByOwnerForTimePeriodIncludesTimeEntryWithStartBeforePeriodAndE tenantService.create("ab143c2f"); prepareSecurityContextWithTenantId("ab143c2f"); - final TenantUser batman = tenantUserService.createNewUser(UUID.randomUUID(), "Bruce", "Wayne", new EMailAddress("batman@example.org")); - final TenantUser superman = tenantUserService.createNewUser(UUID.randomUUID(), "Kent", "Clark", new EMailAddress("Clark@example.org")); + final TenantUser batman = tenantUserService.createNewUser(UUID.randomUUID(), "Bruce", "Wayne", new EMailAddress("batman@example.org"), Set.of()); + final TenantUser superman = tenantUserService.createNewUser(UUID.randomUUID(), "Kent", "Clark", new EMailAddress("Clark@example.org"), Set.of()); final LocalDate periodFrom = LocalDate.of(2022, 1, 3); final LocalDate periodToExclusive = LocalDate.of(2022, 1, 10); @@ -204,8 +200,8 @@ void ensureFindAllByOwnerForTimePeriodIncludesTimeEntryWithStartBeforePeriodAndE tenantService.create("ab143c2f"); prepareSecurityContextWithTenantId("ab143c2f"); - final TenantUser batman = tenantUserService.createNewUser(UUID.randomUUID(), "Bruce", "Wayne", new EMailAddress("batman@example.org")); - final TenantUser superman = tenantUserService.createNewUser(UUID.randomUUID(), "Kent", "Clark", new EMailAddress("Clark@example.org")); + final TenantUser batman = tenantUserService.createNewUser(UUID.randomUUID(), "Bruce", "Wayne", new EMailAddress("batman@example.org"), Set.of()); + final TenantUser superman = tenantUserService.createNewUser(UUID.randomUUID(), "Kent", "Clark", new EMailAddress("Clark@example.org"), Set.of()); final LocalDate periodFrom = LocalDate.of(2022, 1, 3); final LocalDate periodToExclusive = LocalDate.of(2022, 1, 10); @@ -249,8 +245,8 @@ void ensureFindAllByOwnerForTimePeriodIncludesTimeEntryStartingBeforePeriodAndEn tenantService.create("ab143c2f"); prepareSecurityContextWithTenantId("ab143c2f"); - final TenantUser batman = tenantUserService.createNewUser(UUID.randomUUID(), "Bruce", "Wayne", new EMailAddress("batman@example.org")); - final TenantUser superman = tenantUserService.createNewUser(UUID.randomUUID(), "Kent", "Clark", new EMailAddress("Clark@example.org")); + final TenantUser batman = tenantUserService.createNewUser(UUID.randomUUID(), "Bruce", "Wayne", new EMailAddress("batman@example.org"), Set.of()); + final TenantUser superman = tenantUserService.createNewUser(UUID.randomUUID(), "Kent", "Clark", new EMailAddress("Clark@example.org"), Set.of()); final LocalDate periodFrom = LocalDate.of(2022, 1, 3); final LocalDate periodToExclusive = LocalDate.of(2022, 1, 10);