diff --git a/.claude-volumes b/.claude-volumes new file mode 100644 index 0000000..28eb8f7 --- /dev/null +++ b/.claude-volumes @@ -0,0 +1,2 @@ +build +src/main/resources \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3f544f8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +gradlew text eol=lf +gradlew.bat text eol=crlf \ No newline at end of file diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a781ce3..095c1a8 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -22,14 +22,11 @@ jobs: - name: Checkout uses: actions/checkout@v3 - - uses: octokit/request-action@v2.x - id: get_latest_release - with: - route: GET /repos/codecentric/spring-boot-admin/releases/latest - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - run: "echo latest release: ${{ steps.get_latest_release.outputs }}" + - name: Read Spring Boot Admin version + id: sba_version + run: | + version=$(grep -E '^springBootAdmin\s*=' gradle/libs.versions.toml | sed -E 's/.*"([^"]+)".*/\1/') + echo "version=$version" >> "$GITHUB_OUTPUT" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 @@ -49,7 +46,7 @@ jobs: with: context: . push: true - tags: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest,${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ fromJson(steps.get_latest_release.outputs.data).tag_name }}' + tags: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest,${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sba_version.outputs.version }}' platforms: linux/amd64,linux/arm64 labels: | org.opencontainers.image.licenses=Apache-2.0 @@ -58,4 +55,3 @@ jobs: org.opencontainers.image.url=https://github.com/cloudflightio/spring-boot-admin-docker org.opencontainers.image.source=https://github.com/cloudflightio/spring-boot-admin-docker org.opencontainers.image.version=nightly - build-args: SPRING_BOOT_ADMIN_VERSION=${{ fromJson(steps.get_latest_release.outputs.data).tag_name }} diff --git a/Dockerfile b/Dockerfile index 65671f2..a7144fa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,8 @@ FROM docker.io/library/eclipse-temurin:17-jdk AS builder -ARG SPRING_BOOT_ADMIN_VERSION=2.7.3 - WORKDIR /src COPY . /src -RUN /src/gradlew build -PspringBootAdminVersion=$SPRING_BOOT_ADMIN_VERSION +RUN /src/gradlew build FROM docker.io/library/eclipse-temurin:17-jre diff --git a/README.md b/README.md index 20030e9..048b64f 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,97 @@ To configure settings for your instance, either use environment variables as documentation](https://docs.spring.io/spring-boot/docs/1.5.6.RELEASE/reference/html/boot-features-external-config.html) or mount a file called `application.yaml` to `/deployments/application.yaml` when running the container. + +### Security + +The admin UI, its API, and the client registration endpoints are locked down by +`SecurityConfig` and require an authenticated user. Only the static assets and +the login page are reachable anonymously. Authentication is delegated to an +OAuth2 / OpenID Connect provider, so you must configure an OAuth2 client before +the application will start serving the UI. + +The client registration and provider **must** be named `openid`. The provider's +`issuer-uri` is mandatory: it is used both for login and for building the +provider's logout URL on sign-out. The logout URL is constructed as +`/protocol/openid-connect/logout`, which matches +[Keycloak](https://www.keycloak.org/); other providers that do not expose that +path are not supported without code changes. + +```yaml +spring: + security: + oauth2: + client: + registration: + openid: + client-id: + client-secret: + scope: + - openid + - profile + - email + authorization-grant-type: authorization_code + provider: + openid: + issuer-uri: + user-name-attribute: preferred_username +``` + +### Service discovery + +Monitored applications are located through Spring Cloud discovery. Two options +are available. + +#### Kubernetes discovery + +The [Kubernetes discovery +client](https://docs.spring.io/spring-cloud-kubernetes/reference/index.html) is +on the classpath. When the application runs inside a cluster it discovers other +pods/services automatically; no additional configuration is required beyond the +usual RBAC allowing the pod to list services and endpoints. + +#### Simple (static) discovery + +To register targets explicitly — for local testing or for instances outside the +cluster — use the simple discovery client. Each entry needs the target's base +`uri` and the `management.context-path` metadata so SBA knows where the actuator +endpoints live: + +```yaml +spring: + cloud: + discovery: + client: + simple: + instances: + my-application: + - uri: http://localhost:18080 + metadata: + management.context-path: /actuator +``` + +##### Discovering more than one client + +The key under `instances` is the application name, and its value is a **list**, +so you can register several instances of the same application and add further +applications as additional keys: + +```yaml +spring: + cloud: + discovery: + client: + simple: + instances: + my-application: + - uri: http://my-application-1:18080 + metadata: + management.context-path: /actuator + - uri: http://my-application-2:18080 + metadata: + management.context-path: /actuator + another-application: + - uri: http://another-application:8080 + metadata: + management.context-path: /actuator +``` diff --git a/build.gradle b/build.gradle deleted file mode 100644 index cb10f9a..0000000 --- a/build.gradle +++ /dev/null @@ -1,32 +0,0 @@ -plugins { - id 'org.springframework.boot' version '2.7.2' - id 'io.spring.dependency-management' version '1.0.12.RELEASE' - id 'java' -} - -group = 'io.cloudflight' -sourceCompatibility = '17' -version = "${springBootAdminVersion}" - -repositories { - mavenCentral() -} - -ext { - set('springCloudVersion', "2021.0.3") -} - -dependencies { - implementation 'de.codecentric:spring-boot-admin-starter-server' - implementation 'org.springframework.cloud:spring-cloud-starter-kubernetes-fabric8-all' -} - -dependencyManagement { - imports { - mavenBom "de.codecentric:spring-boot-admin-dependencies:${springBootAdminVersion}" - mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" - } -} -tasks.named("jar") { - enabled = false -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..b575b2b --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,58 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.spring) + alias(libs.plugins.spring.boot) +} + +group = "io.cloudflight" +version = libs.versions.springBootAdmin.get() + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +kotlin { + compilerOptions { + freeCompilerArgs.addAll("-Xjsr305=strict") + jvmTarget = JvmTarget.JVM_17 + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation(platform(libs.spring.boot.bom)) + implementation(platform(libs.spring.boot.admin.bom)) + implementation(platform(libs.spring.cloud.bom)) + implementation(libs.spring.boot.admin.starter.server) + implementation(libs.spring.boot.starter.web) + implementation(libs.spring.boot.starter.actuator) + implementation(libs.spring.boot.starter.security) + implementation(libs.spring.boot.starter.oauth2.client) + implementation(libs.jackson.module.kotlin) + implementation(libs.kotlin.reflect) + implementation(libs.spring.cloud.starter.kubernetes.client.all) + + testImplementation(libs.spring.boot.starter.test) +} + +tasks.withType { + useJUnitPlatform() +} + +tasks.test { + filter { + includeTestsMatching("*Test") + isFailOnNoMatchingTests = false + } +} + +tasks.named("jar") { + enabled = false +} diff --git a/gradle.properties b/gradle.properties index caaed69..6e1e412 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,3 @@ -springBootAdminVersion=2.7.3 +org.gradle.caching=true +org.gradle.parallel=true +kotlin.code.style=official diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..9e72b13 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,33 @@ +[versions] +kotlin = "2.1.21" +springBoot = "3.5.4" +# Spring Boot Admin is versioned independently of Spring Boot (they happen to align today). +springBootAdmin = "3.5.4" +springCloud = "2025.0.3" + +[plugins] +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } +spring-boot = { id = "org.springframework.boot", version.ref = "springBoot" } + +[libraries] +spring-boot-bom = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "springBoot" } +spring-boot-admin-bom = { module = "de.codecentric:spring-boot-admin-dependencies", version.ref = "springBootAdmin" } +spring-cloud-bom = { module = "org.springframework.cloud:spring-cloud-dependencies", version.ref = "springCloud" } + +# Dependencies managed by Spring Boot BOM +spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web" } +spring-boot-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator" } +spring-boot-starter-security = { module = "org.springframework.boot:spring-boot-starter-security" } +spring-boot-starter-oauth2-client = { module = "org.springframework.boot:spring-boot-starter-oauth2-client" } +jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin" } +kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } + +# Spring Boot Admin server UI + registration endpoint (version managed by the SBA BOM). +spring-boot-admin-starter-server = { module = "de.codecentric:spring-boot-admin-starter-server" } + +# Discover Spring Boot Applications running in Kubernetes (version managed by the Spring Cloud BOM). +spring-cloud-starter-kubernetes-client-all = { module = "org.springframework.cloud:spring-cloud-starter-kubernetes-client-all" } + +# Test dependencies (managed by Spring Boot BOM) +spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8049c68..03b32a2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew.bat b/gradlew.bat index 53a6b23..f127cfd 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,91 +1,91 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index e1997e8..0000000 --- a/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'spring-boot-admin' diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..56bf62e --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "spring-boot-admin" diff --git a/src/main/java/io/cloudflight/springbootadmin/SpringBootAdminApplication.java b/src/main/java/io/cloudflight/springbootadmin/SpringBootAdminApplication.java deleted file mode 100644 index df9e60b..0000000 --- a/src/main/java/io/cloudflight/springbootadmin/SpringBootAdminApplication.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.cloudflight.springbootadmin; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import de.codecentric.boot.admin.server.config.EnableAdminServer; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.client.discovery.EnableDiscoveryClient; -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.EnableScheduling; - -@SpringBootApplication -@EnableAdminServer -@EnableDiscoveryClient -@EnableScheduling -public class SpringBootAdminApplication { - - public static void main(String[] args) { - SpringApplication.run(SpringBootAdminApplication.class, args); - } - -} diff --git a/src/main/kotlin/io/cloudflight/springbootadmin/SpringBootAdminApplication.kt b/src/main/kotlin/io/cloudflight/springbootadmin/SpringBootAdminApplication.kt new file mode 100644 index 0000000..b0e5df8 --- /dev/null +++ b/src/main/kotlin/io/cloudflight/springbootadmin/SpringBootAdminApplication.kt @@ -0,0 +1,15 @@ +package io.cloudflight.springbootadmin + +import de.codecentric.boot.admin.server.config.EnableAdminServer +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication +import org.springframework.cloud.client.discovery.EnableDiscoveryClient + +@EnableDiscoveryClient +@EnableAdminServer +@SpringBootApplication +class SpringBootAdminApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/src/main/kotlin/io/cloudflight/springbootadmin/config/SecurityConfig.kt b/src/main/kotlin/io/cloudflight/springbootadmin/config/SecurityConfig.kt new file mode 100644 index 0000000..0471295 --- /dev/null +++ b/src/main/kotlin/io/cloudflight/springbootadmin/config/SecurityConfig.kt @@ -0,0 +1,84 @@ +package io.cloudflight.springbootadmin.config + +import de.codecentric.boot.admin.server.config.AdminServerProperties +import jakarta.servlet.DispatcherType +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.invoke +import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher +import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher + +/** + * Locks down the Spring Boot Admin server so that only authenticated users + * (via the configured OAuth2 provider) can reach the UI or the actuator data + * it aggregates. + * + * The configuration deliberately: + * - permits only the static assets and the login/OAuth2 endpoints anonymously, + * - requires authentication for every other request (the admin UI, API, and + * the SBA instance registration endpoints), + */ +@Configuration +@EnableWebSecurity +class SecurityConfig( + private val adminServerProperties: AdminServerProperties, +) { + + @Bean + fun securityFilterChain(http: HttpSecurity, clientRegistrationRepository: InMemoryClientRegistrationRepository): SecurityFilterChain { + // Base path the SBA UI is served under (default ""). + val adminContextPath = adminServerProperties.contextPath + + // Factory for path matchers rooted at the SBA context path. + val mvc = PathPatternRequestMatcher.withDefaults() + + // After a successful login, send the user back to the SBA UI root. + val successHandler = SavedRequestAwareAuthenticationSuccessHandler().apply { + setTargetUrlParameter("redirectTo") + setDefaultTargetUrl("$adminContextPath/") + } + + http { + authorizeHttpRequests { + // The Spring Boot Admin UI streams instance/event updates over SSE, which + // Spring MVC processes asynchronously. In Spring Security 6 the + // AuthorizationFilter runs on ALL dispatcher types by default, so it would + // re-evaluate authorization on the ASYNC re-dispatch — when the SecurityContext + // is no longer populated — and deny access after the response is already + // committed. Authorization is enforced on the initial REQUEST dispatch, so + // permit the async/forward/error re-dispatches to pass through. + authorize(DispatcherTypeRequestMatcher(DispatcherType.ASYNC), permitAll) + authorize(DispatcherTypeRequestMatcher(DispatcherType.FORWARD), permitAll) + authorize(DispatcherTypeRequestMatcher(DispatcherType.ERROR), permitAll) + // Static assets and the login page itself are public. + authorize(mvc.matcher("$adminContextPath/login"), permitAll) + // Everything else — UI, API, registration — requires authentication. + authorize(anyRequest, authenticated) + } + + // Interactive browser login through the OAuth2 provider. + oauth2Login { + authenticationSuccessHandler = successHandler + } + + logout { + logoutSuccessHandler = OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository).apply { + setPostLogoutRedirectUri("{baseUrl}") + } + } + + csrf { + disable() + } + } + + return http.build() + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 8b13789..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..e69de29 diff --git a/src/test/kotlin/io/cloudflight/springbootadmin/config/SecurityConfigTest.kt b/src/test/kotlin/io/cloudflight/springbootadmin/config/SecurityConfigTest.kt new file mode 100644 index 0000000..07e0224 --- /dev/null +++ b/src/test/kotlin/io/cloudflight/springbootadmin/config/SecurityConfigTest.kt @@ -0,0 +1,45 @@ +package io.cloudflight.springbootadmin.config + +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.mockito.Mockito.mock +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@SpringBootTest +@AutoConfigureMockMvc +class SecurityConfigTest( + @Autowired val mockMvc: MockMvc, +) { + + @ParameterizedTest + @ValueSource(strings = ["/login"]) + fun `public endpoints are accessible without authentication`(path: String) { + mockMvc.perform(get(path)) + .andExpect(status().is2xxSuccessful()) + } + + @ParameterizedTest + @ValueSource(strings = ["/", "/applications"]) + fun `protected endpoints require authentication and redirect to login`(path: String) { + mockMvc.perform(get(path)) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost/login")) + } + + @Configuration + class Config { + + @Bean + fun inMemoryClientRegistrationRepository() = mock() + + } +} \ No newline at end of file