diff --git a/src/main/java/org/kickerelo/kickerelo/config/SecurityConfiguration.java b/src/main/java/org/kickerelo/kickerelo/config/SecurityConfiguration.java new file mode 100644 index 0000000..695cb88 --- /dev/null +++ b/src/main/java/org/kickerelo/kickerelo/config/SecurityConfiguration.java @@ -0,0 +1,23 @@ +package org.kickerelo.kickerelo.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; + +import com.vaadin.flow.spring.security.VaadinWebSecurity; + +@Profile("prod") +@Configuration +class SecurityConfiguration extends VaadinWebSecurity { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeHttpRequests(auth -> auth + .requestMatchers("/app/admin/**", "/app/admin", "/app/app/admin/**", "/app/app/admin").hasAuthority("Kicker Admin") + .anyRequest().permitAll() + ) + .oauth2Login(org.springframework.security.config.Customizer.withDefaults()) + .logout(logout -> logout.logoutSuccessUrl("/")) + .csrf(csrf -> csrf.disable()); + } +} diff --git a/src/main/java/org/kickerelo/kickerelo/layout/KickerAppLayout.java b/src/main/java/org/kickerelo/kickerelo/layout/KickerAppLayout.java index ef3ff53..1b30871 100644 --- a/src/main/java/org/kickerelo/kickerelo/layout/KickerAppLayout.java +++ b/src/main/java/org/kickerelo/kickerelo/layout/KickerAppLayout.java @@ -12,13 +12,16 @@ import com.vaadin.flow.component.sidenav.SideNav; import com.vaadin.flow.component.sidenav.SideNavItem; import com.vaadin.flow.dom.Style; import com.vaadin.flow.router.Layout; +import org.kickerelo.kickerelo.util.AccessControlService; import org.kickerelo.kickerelo.views.*; @Layout @JsModule("./prefers-color-scheme.js") public class KickerAppLayout extends AppLayout { + AccessControlService accessControlService; - public KickerAppLayout() { + public KickerAppLayout(AccessControlService accessControlService) { + this.accessControlService = accessControlService; DrawerToggle drawerToggle = new DrawerToggle(); H1 title = new H1("Kicker-ELO"); @@ -26,6 +29,23 @@ public class KickerAppLayout extends AppLayout { addToNavbar(drawerToggle, title); + // Add login/logout button + if (accessControlService.userAllowedForRole("")) { + Anchor logoutLink = new Anchor("/logout", "Logout"); + logoutLink.getElement().getStyle() + .set("margin-left", "auto") + .set("margin-right", "10px") + .set("align-self", "center"); + addToNavbar(logoutLink); + } else { + Anchor loginLink = new Anchor("/oauth2/authorization/oidc", "Login"); + loginLink.getElement().getStyle() + .set("margin-left", "auto") + .set("margin-right", "10px") + .set("align-self", "center"); + addToNavbar(loginLink); + } + SideNav general = new SideNav("Allgemein"); general.setCollapsible(true); general.addItem(new SideNavItem("Spielerliste", PlayerListView.class, VaadinIcon.GROUP.create()), diff --git a/src/main/java/org/kickerelo/kickerelo/util/AccessControlService.java b/src/main/java/org/kickerelo/kickerelo/util/AccessControlService.java new file mode 100644 index 0000000..837f9a4 --- /dev/null +++ b/src/main/java/org/kickerelo/kickerelo/util/AccessControlService.java @@ -0,0 +1,5 @@ +package org.kickerelo.kickerelo.util; + +public interface AccessControlService { + boolean userAllowedForRole(String role); +} diff --git a/src/main/java/org/kickerelo/kickerelo/util/AccessControlServiceProdImpl.java b/src/main/java/org/kickerelo/kickerelo/util/AccessControlServiceProdImpl.java new file mode 100644 index 0000000..21dbf00 --- /dev/null +++ b/src/main/java/org/kickerelo/kickerelo/util/AccessControlServiceProdImpl.java @@ -0,0 +1,36 @@ +package org.kickerelo.kickerelo.util; + +import org.springframework.context.annotation.Profile; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Profile("prod") +public class AccessControlServiceProdImpl implements AccessControlService { + @Override + public boolean userAllowedForRole(String role) { + // Check if authentication is present + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth == null || !(auth.getPrincipal() instanceof OidcUser oidcUser)) return false; + + // Empty String means there just needs to be authentication, not a specific group + if (role.isEmpty()) return true; + + // Get the list of groups the user is part of + Object groupsObj = oidcUser.getClaims().getOrDefault("groups", List.of()); + if (!(groupsObj instanceof List)) return false; + + // Keep only Strings in the list + List listOfGroups = ((List) groupsObj).stream() + .filter(String.class::isInstance) + .map(String.class::cast) + .toList(); + + // Check if the user is part of the required group + return listOfGroups.contains(role); + } +} diff --git a/src/main/java/org/kickerelo/kickerelo/util/AccessControlServiceTestImpl.java b/src/main/java/org/kickerelo/kickerelo/util/AccessControlServiceTestImpl.java new file mode 100644 index 0000000..fbcd0f9 --- /dev/null +++ b/src/main/java/org/kickerelo/kickerelo/util/AccessControlServiceTestImpl.java @@ -0,0 +1,13 @@ +package org.kickerelo.kickerelo.util; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Component +@Profile("test") +public class AccessControlServiceTestImpl implements AccessControlService { + @Override + public boolean userAllowedForRole(String role) { + return true; + } +} diff --git a/src/main/java/org/kickerelo/kickerelo/views/AdminView.java b/src/main/java/org/kickerelo/kickerelo/views/AdminView.java index 5098cbb..79eec04 100644 --- a/src/main/java/org/kickerelo/kickerelo/views/AdminView.java +++ b/src/main/java/org/kickerelo/kickerelo/views/AdminView.java @@ -1,21 +1,29 @@ package org.kickerelo.kickerelo.views; +import org.kickerelo.kickerelo.exception.DuplicatePlayerException; +import org.kickerelo.kickerelo.exception.InvalidDataException; +import org.kickerelo.kickerelo.exception.PlayerNameNotSetException; +import org.kickerelo.kickerelo.service.KickerEloService; +import org.kickerelo.kickerelo.util.AccessControlService; + import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.html.Paragraph; import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.router.Route; -import org.kickerelo.kickerelo.exception.DuplicatePlayerException; -import org.kickerelo.kickerelo.exception.InvalidDataException; -import org.kickerelo.kickerelo.exception.PlayerNameNotSetException; -import org.kickerelo.kickerelo.service.KickerEloService; @Route("admin") public class AdminView extends VerticalLayout { - public AdminView(KickerEloService service) { - H2 subheader = new H2("Verwaltung"); + + public AdminView(KickerEloService service, AccessControlService accessControlService) { + // Deny access if user isn't part of the Kicker Admin group + if (!accessControlService.userAllowedForRole("Kicker Admin")) { + add(new Paragraph("Du bist nicht berechtigt, diese Seite zu sehen.")); + getUI().ifPresent(ui -> ui.navigate("")); + return; + } TextField spielername = new TextField("Spielername"); spielername.addClassName("bordered"); @@ -41,6 +49,7 @@ public class AdminView extends VerticalLayout { service.recalculateAll1vs1(); Notification.show("Recalculating finished").addThemeVariants(NotificationVariant.LUMO_SUCCESS); }); + Button recalc2vs2Button = new Button("2 vs 2 Elo neu berechnen", e -> { Notification.show("Recalculating Elo").addThemeVariants(NotificationVariant.LUMO_WARNING); service.recalculateAll2vs2(); diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties index 535b5ba..0db2142 100644 --- a/src/main/resources/application-test.properties +++ b/src/main/resources/application-test.properties @@ -1,9 +1,15 @@ server.port=${PORT:8080} logging.level.org.atmosphere = warn +logging.level.org.springframework.security=DEBUG spring.mustache.check-template-location = false spring.datasource.driver-class-name=org.h2.Driver spring.datasource.username=sa spring.datasource.password= spring.jpa.hibernate.ddl-auto=update -spring.jpa.show-sql=true \ No newline at end of file +spring.jpa.show-sql=true + +# == OIDC Configuration == +spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration + +vaadin.urlMapping=/app/* diff --git a/src/main/resources/static/icon.png b/src/main/resources/static/icon.png new file mode 100644 index 0000000..a9cfeec Binary files /dev/null and b/src/main/resources/static/icon.png differ