From 25dea603eef99c7b7ca25eb1d2ba8104a2816f2a Mon Sep 17 00:00:00 2001 From: Roman Zabaluev Date: Fri, 25 Oct 2024 22:10:09 +0300 Subject: [PATCH 1/8] Misc --- .../{AuthController.java => AuthenticationController.java} | 4 ++-- .../{AccessController.java => AuthorizationController.java} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename api/src/main/java/io/kafbat/ui/controller/{AuthController.java => AuthenticationController.java} (97%) rename api/src/main/java/io/kafbat/ui/controller/{AccessController.java => AuthorizationController.java} (97%) diff --git a/api/src/main/java/io/kafbat/ui/controller/AuthController.java b/api/src/main/java/io/kafbat/ui/controller/AuthenticationController.java similarity index 97% rename from api/src/main/java/io/kafbat/ui/controller/AuthController.java rename to api/src/main/java/io/kafbat/ui/controller/AuthenticationController.java index e4532dda3..b50c64546 100644 --- a/api/src/main/java/io/kafbat/ui/controller/AuthController.java +++ b/api/src/main/java/io/kafbat/ui/controller/AuthenticationController.java @@ -13,13 +13,13 @@ @RestController @RequiredArgsConstructor @Slf4j -public class AuthController { +public class AuthenticationController { @GetMapping(value = "/auth", produces = {"text/html"}) public Mono getAuth(ServerWebExchange exchange) { Mono token = exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty()); return token - .map(AuthController::csrfToken) + .map(AuthenticationController::csrfToken) .defaultIfEmpty("") .map(csrfTokenHtmlInput -> createPage(exchange, csrfTokenHtmlInput)); } diff --git a/api/src/main/java/io/kafbat/ui/controller/AccessController.java b/api/src/main/java/io/kafbat/ui/controller/AuthorizationController.java similarity index 97% rename from api/src/main/java/io/kafbat/ui/controller/AccessController.java rename to api/src/main/java/io/kafbat/ui/controller/AuthorizationController.java index 5833f2e3c..ea1ef7739 100644 --- a/api/src/main/java/io/kafbat/ui/controller/AccessController.java +++ b/api/src/main/java/io/kafbat/ui/controller/AuthorizationController.java @@ -26,7 +26,7 @@ @RestController @RequiredArgsConstructor @Slf4j -public class AccessController implements AuthorizationApi { +public class AuthorizationController implements AuthorizationApi { private final AccessControlService accessControlService; From 1e668f8910805623ef700c8ca52de8e215d7a504 Mon Sep 17 00:00:00 2001 From: Roman Zabaluev Date: Sat, 26 Oct 2024 01:07:44 +0300 Subject: [PATCH 2/8] Impl authentication page backend --- .../auth/AbstractAuthSecurityConfig.java | 3 +- .../ApplicationConfigController.java | 10 +++- .../ui/service/ApplicationInfoService.java | 47 ++++++++++++++++++- .../main/resources/swagger/kafbat-ui-api.yaml | 42 ++++++++++++++++- 4 files changed, 98 insertions(+), 4 deletions(-) diff --git a/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java b/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java index f23a0dd2a..dc58a7299 100644 --- a/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java +++ b/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java @@ -18,7 +18,8 @@ protected AbstractAuthSecurityConfig() { "/login", "/logout", "/oauth2/**", - "/static/**" + "/static/**", + "/api/config/authentication" }; } diff --git a/api/src/main/java/io/kafbat/ui/controller/ApplicationConfigController.java b/api/src/main/java/io/kafbat/ui/controller/ApplicationConfigController.java index 5d5d4ed98..e8d763545 100644 --- a/api/src/main/java/io/kafbat/ui/controller/ApplicationConfigController.java +++ b/api/src/main/java/io/kafbat/ui/controller/ApplicationConfigController.java @@ -6,6 +6,7 @@ import io.kafbat.ui.api.ApplicationConfigApi; import io.kafbat.ui.config.ClustersProperties; import io.kafbat.ui.model.ActionDTO; +import io.kafbat.ui.model.AppAuthenticationSettingsDTO; import io.kafbat.ui.model.ApplicationConfigDTO; import io.kafbat.ui.model.ApplicationConfigPropertiesDTO; import io.kafbat.ui.model.ApplicationConfigValidationDTO; @@ -66,6 +67,13 @@ public Mono> getApplicationInfo(ServerWebExch return Mono.just(applicationInfoService.getApplicationInfo()).map(ResponseEntity::ok); } + @Override + public Mono> getAuthenticationSettings( + ServerWebExchange exchange) { + return Mono.just(applicationInfoService.getAuthenticationProperties()) + .map(ResponseEntity::ok); + } + @Override public Mono> getCurrentConfig(ServerWebExchange exchange) { var context = AccessContext.builder() @@ -109,7 +117,7 @@ public Mono> uploadConfigRelatedFile(Flux dynamicConfigOperations.uploadConfigRelatedFile((FilePart) file) - .map(path -> new UploadedFileInfoDTO().location(path.toString())) + .map(path -> new UploadedFileInfoDTO(path.toString())) .map(ResponseEntity::ok)) .doOnEach(sig -> audit(context, sig)); } diff --git a/api/src/main/java/io/kafbat/ui/service/ApplicationInfoService.java b/api/src/main/java/io/kafbat/ui/service/ApplicationInfoService.java index 0a04ce9d6..7cc7e3e41 100644 --- a/api/src/main/java/io/kafbat/ui/service/ApplicationInfoService.java +++ b/api/src/main/java/io/kafbat/ui/service/ApplicationInfoService.java @@ -1,36 +1,49 @@ package io.kafbat.ui.service; +import static io.kafbat.ui.api.model.AuthType.DISABLED; +import static io.kafbat.ui.api.model.AuthType.OAUTH2; import static io.kafbat.ui.model.ApplicationInfoDTO.EnabledFeaturesEnum; +import com.google.common.collect.Streams; +import io.kafbat.ui.model.AppAuthenticationSettingsDTO; import io.kafbat.ui.model.ApplicationInfoBuildDTO; import io.kafbat.ui.model.ApplicationInfoDTO; import io.kafbat.ui.model.ApplicationInfoLatestReleaseDTO; +import io.kafbat.ui.model.AuthTypeDTO; +import io.kafbat.ui.model.OAuthProviderDTO; import io.kafbat.ui.util.DynamicConfigOperations; import io.kafbat.ui.util.GithubReleaseInfo; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Properties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.info.BuildProperties; import org.springframework.boot.info.GitProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.core.ResolvableType; import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.stereotype.Service; -import reactor.core.publisher.Mono; @Service public class ApplicationInfoService { private final GithubReleaseInfo githubReleaseInfo = new GithubReleaseInfo(); + private final ApplicationContext applicationContext; private final DynamicConfigOperations dynamicConfigOperations; private final BuildProperties buildProperties; private final GitProperties gitProperties; public ApplicationInfoService(DynamicConfigOperations dynamicConfigOperations, + ApplicationContext applicationContext, @Autowired(required = false) BuildProperties buildProperties, @Autowired(required = false) GitProperties gitProperties) { + this.applicationContext = applicationContext; this.dynamicConfigOperations = dynamicConfigOperations; this.buildProperties = Optional.ofNullable(buildProperties).orElse(new BuildProperties(new Properties())); this.gitProperties = Optional.ofNullable(gitProperties).orElse(new GitProperties(new Properties())); @@ -68,6 +81,38 @@ private List getEnabledFeatures() { return enabledFeatures; } + public AppAuthenticationSettingsDTO getAuthenticationProperties() { + return new AppAuthenticationSettingsDTO() + .authType(AuthTypeDTO.fromValue(getAuthType())) + .oAuthProviders(getOAuthProviders()); + } + + private String getAuthType() { + return Optional.ofNullable(applicationContext.getEnvironment().getProperty("auth.type")) + .orElse(DISABLED.getValue()); + } + + @SuppressWarnings("unchecked") + private List getOAuthProviders() { + if (!getAuthType().equalsIgnoreCase(OAUTH2.getValue())) { + return Collections.emptyList(); + } + var type = ResolvableType.forClassWithGenerics(Iterable.class, ClientRegistration.class); + String[] names = this.applicationContext.getBeanNamesForType(type); + var bean = (Iterable) (names.length == 1 ? this.applicationContext.getBean(names[0]) : null); + + if (bean == null) { + return Collections.emptyList(); + } + + return Streams.stream(bean.iterator()) + .filter(r -> AuthorizationGrantType.AUTHORIZATION_CODE.equals(r.getAuthorizationGrantType())) + .map(r -> new OAuthProviderDTO() + .clientName(r.getClientName()) + .authorizationUri("/oauth2/authorization/" + r.getRegistrationId())) + .toList(); + } + // updating on startup and every hour @Scheduled(fixedRateString = "${github-release-info-update-rate:3600000}") public void updateGithubReleaseInfo() { diff --git a/contract/src/main/resources/swagger/kafbat-ui-api.yaml b/contract/src/main/resources/swagger/kafbat-ui-api.yaml index 19be2abaa..8312c6da5 100644 --- a/contract/src/main/resources/swagger/kafbat-ui-api.yaml +++ b/contract/src/main/resources/swagger/kafbat-ui-api.yaml @@ -2124,7 +2124,7 @@ paths: get: tags: - Authorization - summary: Get user authentication related info + summary: Get user authorization related info operationId: getUserAuthInfo responses: 200: @@ -2218,6 +2218,20 @@ paths: schema: $ref: '#/components/schemas/UploadedFileInfo' + /api/config/authentication: + get: + tags: + - ApplicationConfig + summary: Get authentication methods enabled for the app and other related settings + operationId: getAuthenticationSettings + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/AppAuthenticationSettings' + components: schemas: TopicSerdeSuggestion: @@ -2328,6 +2342,32 @@ components: htmlUrl: type: string + AppAuthenticationSettings: + type: object + properties: + authType: + $ref: '#/components/schemas/AuthType' + oAuthProviders: + type: array + items: + $ref: '#/components/schemas/OAuthProvider' + + OAuthProvider: + type: object + properties: + clientName: + type: string + authorizationUri: + type: string + + AuthType: + type: string + enum: + - DISABLED + - OAUTH2 + - LOGIN_FORM + - LDAP + Cluster: type: object properties: From d91e8b9241294dd163dd5fa344a8b498d165b1e8 Mon Sep 17 00:00:00 2001 From: Roman Zabaluev Date: Sun, 17 Nov 2024 20:45:12 +0800 Subject: [PATCH 3/8] Add POST auth endpoint --- .../main/resources/swagger/kafbat-ui-api.yaml | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/contract/src/main/resources/swagger/kafbat-ui-api.yaml b/contract/src/main/resources/swagger/kafbat-ui-api.yaml index 486a27bdc..da2d1a765 100644 --- a/contract/src/main/resources/swagger/kafbat-ui-api.yaml +++ b/contract/src/main/resources/swagger/kafbat-ui-api.yaml @@ -31,7 +31,6 @@ paths: items: $ref: '#/components/schemas/Cluster' - /api/clusters/{clusterName}/cache: post: tags: @@ -54,7 +53,6 @@ paths: 404: description: Not found - /api/clusters/{clusterName}/brokers: get: tags: @@ -432,7 +430,6 @@ paths: 404: description: Not found - /api/clusters/{clusterName}/topics/{topicName}: get: tags: @@ -2220,7 +2217,6 @@ paths: schema: $ref: '#/components/schemas/ApplicationConfigValidation' - /api/config/relatedfiles: post: tags: @@ -2258,6 +2254,26 @@ paths: schema: $ref: '#/components/schemas/AppAuthenticationSettings' + /auth: + post: + summary: Authenticate + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + username: + type: string + password: + type: string + responses: + '200': + description: OK + '401': + description: Unauthorized + components: schemas: TopicSerdeSuggestion: From 38142b9af6d2d1bff2022d315641a57abcbe694b Mon Sep 17 00:00:00 2001 From: German Osin Date: Mon, 23 Dec 2024 18:53:38 +0800 Subject: [PATCH 4/8] Fixed login page renderer --- .java-version | 1 + .../auth/AbstractAuthSecurityConfig.java | 7 ++- .../ui/config/auth/OAuthSecurityConfig.java | 15 ++++-- .../controller/AuthenticationController.java | 6 +++ .../kafbat/ui/util/StaticFileWebFilter.java | 52 +++++++++++++++++++ 5 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 .java-version create mode 100644 api/src/main/java/io/kafbat/ui/util/StaticFileWebFilter.java diff --git a/.java-version b/.java-version new file mode 100644 index 000000000..aabe6ec39 --- /dev/null +++ b/.java-version @@ -0,0 +1 @@ +21 diff --git a/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java b/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java index dc58a7299..d706daef8 100644 --- a/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java +++ b/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java @@ -19,7 +19,12 @@ protected AbstractAuthSecurityConfig() { "/logout", "/oauth2/**", "/static/**", - "/api/config/authentication" + "/api/config/authentication", + "/index.html", + "/assets/**", + "/manifest.json", + "/favicon/**", + "/api/authorization" }; } diff --git a/api/src/main/java/io/kafbat/ui/config/auth/OAuthSecurityConfig.java b/api/src/main/java/io/kafbat/ui/config/auth/OAuthSecurityConfig.java index 09c7df794..7b32f7d2a 100644 --- a/api/src/main/java/io/kafbat/ui/config/auth/OAuthSecurityConfig.java +++ b/api/src/main/java/io/kafbat/ui/config/auth/OAuthSecurityConfig.java @@ -3,6 +3,7 @@ import io.kafbat.ui.config.auth.logout.OAuthLogoutSuccessHandler; import io.kafbat.ui.service.rbac.AccessControlService; import io.kafbat.ui.service.rbac.extractor.ProviderAuthorityExtractor; +import io.kafbat.ui.util.StaticFileWebFilter; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -16,9 +17,11 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.SecurityWebFiltersOrder; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; @@ -50,7 +53,7 @@ public class OAuthSecurityConfig extends AbstractAuthSecurityConfig { public SecurityWebFilterChain configure(ServerHttpSecurity http, OAuthLogoutSuccessHandler logoutHandler) { log.info("Configuring OAUTH2 authentication."); - return http.authorizeExchange(spec -> spec + ServerHttpSecurity builder = http.authorizeExchange(spec -> spec .pathMatchers(AUTH_WHITELIST) .permitAll() .anyExchange() @@ -58,8 +61,14 @@ public SecurityWebFilterChain configure(ServerHttpSecurity http, OAuthLogoutSucc ) .oauth2Login(Customizer.withDefaults()) .logout(spec -> spec.logoutSuccessHandler(logoutHandler)) - .csrf(ServerHttpSecurity.CsrfSpec::disable) - .build(); + .csrf(ServerHttpSecurity.CsrfSpec::disable); + + + builder.addFilterAt(new StaticFileWebFilter( + "/login", new ClassPathResource("/static/index.html") + ), SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING); + + return builder.build(); } @Bean diff --git a/api/src/main/java/io/kafbat/ui/controller/AuthenticationController.java b/api/src/main/java/io/kafbat/ui/controller/AuthenticationController.java index b50c64546..1c7145847 100644 --- a/api/src/main/java/io/kafbat/ui/controller/AuthenticationController.java +++ b/api/src/main/java/io/kafbat/ui/controller/AuthenticationController.java @@ -3,6 +3,7 @@ import java.nio.charset.Charset; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.ClassPathResource; import org.springframework.security.web.server.csrf.CsrfToken; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.GetMapping; @@ -15,6 +16,11 @@ @Slf4j public class AuthenticationController { + @GetMapping(value = "/login", produces = {"text/html"}) + public Mono getLoginPage(ServerWebExchange exchange) { + return Mono.just(new ClassPathResource("static/index.html")); + } + @GetMapping(value = "/auth", produces = {"text/html"}) public Mono getAuth(ServerWebExchange exchange) { Mono token = exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty()); diff --git a/api/src/main/java/io/kafbat/ui/util/StaticFileWebFilter.java b/api/src/main/java/io/kafbat/ui/util/StaticFileWebFilter.java new file mode 100644 index 000000000..391fa8dbd --- /dev/null +++ b/api/src/main/java/io/kafbat/ui/util/StaticFileWebFilter.java @@ -0,0 +1,52 @@ +package io.kafbat.ui.util; + +import java.io.IOException; +import java.io.InputStream; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +public class StaticFileWebFilter implements WebFilter { + private ServerWebExchangeMatcher matcher; + private String contents; + + public StaticFileWebFilter(String path, ClassPathResource resource) { + this.matcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, path); + try (InputStream inputStream = resource.getInputStream()) { + this.contents = ResourceUtil.readAsString(resource); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return this.matcher.matches(exchange) + .filter(ServerWebExchangeMatcher.MatchResult::isMatch) + .switchIfEmpty(chain.filter(exchange).then(Mono.empty())) + .flatMap((matchResult) -> this.render(exchange)); + } + + private Mono render(ServerWebExchange exchange) { + String contextPath = exchange.getRequest().getPath().contextPath().value(); + String contentBody = contents + .replace("\"assets/", "\"" + contextPath + "/assets/") + .replace("PUBLIC-PATH-VARIABLE", contextPath); + + ServerHttpResponse result = exchange.getResponse(); + result.setStatusCode(HttpStatus.OK); + result.getHeaders().setContentType(MediaType.TEXT_HTML); + DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory(); + return result.writeWith(Mono.just(bufferFactory.wrap(contentBody.getBytes()))); + } + +} From 3cfd73804e811bc894b64c550806df401bd992f4 Mon Sep 17 00:00:00 2001 From: Roman Zabaluev Date: Mon, 23 Dec 2024 23:11:49 +0800 Subject: [PATCH 5/8] Polish --- .../auth/AbstractAuthSecurityConfig.java | 23 +++-- .../ui/config/auth/OAuthSecurityConfig.java | 7 +- .../controller/AuthenticationController.java | 91 +------------------ .../kafbat/ui/util/StaticFileWebFilter.java | 15 +-- 4 files changed, 30 insertions(+), 106 deletions(-) diff --git a/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java b/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java index d706daef8..407d05a1d 100644 --- a/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java +++ b/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java @@ -6,24 +6,29 @@ protected AbstractAuthSecurityConfig() { } + public static final String INDEX_HTML = "/static/index.html"; + protected static final String[] AUTH_WHITELIST = { - "/css/**", - "/js/**", - "/media/**", + /* STATIC */ + "/index.html", + "/assets/**", + "/manifest.json", + "/favicon.svg", + "/favicon/**", + + "/static/**", "/resources/**", + + /* ACTUATOR */ "/actuator/health/**", "/actuator/info", "/actuator/prometheus", - "/auth", + + /* AUTH */ "/login", "/logout", "/oauth2/**", - "/static/**", "/api/config/authentication", - "/index.html", - "/assets/**", - "/manifest.json", - "/favicon/**", "/api/authorization" }; diff --git a/api/src/main/java/io/kafbat/ui/config/auth/OAuthSecurityConfig.java b/api/src/main/java/io/kafbat/ui/config/auth/OAuthSecurityConfig.java index 7b32f7d2a..eb54f812b 100644 --- a/api/src/main/java/io/kafbat/ui/config/auth/OAuthSecurityConfig.java +++ b/api/src/main/java/io/kafbat/ui/config/auth/OAuthSecurityConfig.java @@ -53,7 +53,7 @@ public class OAuthSecurityConfig extends AbstractAuthSecurityConfig { public SecurityWebFilterChain configure(ServerHttpSecurity http, OAuthLogoutSuccessHandler logoutHandler) { log.info("Configuring OAUTH2 authentication."); - ServerHttpSecurity builder = http.authorizeExchange(spec -> spec + var builder = http.authorizeExchange(spec -> spec .pathMatchers(AUTH_WHITELIST) .permitAll() .anyExchange() @@ -64,9 +64,8 @@ public SecurityWebFilterChain configure(ServerHttpSecurity http, OAuthLogoutSucc .csrf(ServerHttpSecurity.CsrfSpec::disable); - builder.addFilterAt(new StaticFileWebFilter( - "/login", new ClassPathResource("/static/index.html") - ), SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING); + builder.addFilterAt(new StaticFileWebFilter("/login", new ClassPathResource(INDEX_HTML)), + SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING); return builder.build(); } diff --git a/api/src/main/java/io/kafbat/ui/controller/AuthenticationController.java b/api/src/main/java/io/kafbat/ui/controller/AuthenticationController.java index 1c7145847..c94c344c9 100644 --- a/api/src/main/java/io/kafbat/ui/controller/AuthenticationController.java +++ b/api/src/main/java/io/kafbat/ui/controller/AuthenticationController.java @@ -1,14 +1,10 @@ package io.kafbat.ui.controller; -import java.nio.charset.Charset; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.ClassPathResource; -import org.springframework.security.web.server.csrf.CsrfToken; -import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @RestController @@ -16,90 +12,11 @@ @Slf4j public class AuthenticationController { - @GetMapping(value = "/login", produces = {"text/html"}) - public Mono getLoginPage(ServerWebExchange exchange) { - return Mono.just(new ClassPathResource("static/index.html")); - } - - @GetMapping(value = "/auth", produces = {"text/html"}) - public Mono getAuth(ServerWebExchange exchange) { - Mono token = exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty()); - return token - .map(AuthenticationController::csrfToken) - .defaultIfEmpty("") - .map(csrfTokenHtmlInput -> createPage(exchange, csrfTokenHtmlInput)); - } - - private byte[] createPage(ServerWebExchange exchange, String csrfTokenHtmlInput) { - MultiValueMap queryParams = exchange.getRequest() - .getQueryParams(); - String contextPath = exchange.getRequest().getPath().contextPath().value(); - String page = - "\n" + "\n" + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Please sign in\n" - + " \n" - + " \n" - + " \n" - + " \n" - + "
\n" - + formLogin(queryParams, contextPath, csrfTokenHtmlInput) - + "
\n" - + " \n" - + ""; - - return page.getBytes(Charset.defaultCharset()); - } + private static final String INDEX_HTML = "/static/index.html"; - private String formLogin( - MultiValueMap queryParams, - String contextPath, String csrfTokenHtmlInput) { - - boolean isError = queryParams.containsKey("error"); - boolean isLogoutSuccess = queryParams.containsKey("logout"); - return - "
\n" - + " \n" - + createError(isError) - + createLogoutSuccess(isLogoutSuccess) - + "

\n" - + " \n" - + " \n" - + "

\n" + "

\n" - + " \n" - + " \n" - + "

\n" + csrfTokenHtmlInput - + " \n" - + "
\n"; - } - - private static String csrfToken(CsrfToken token) { - return " \n"; - } - - private static String createError(boolean isError) { - return isError - ? "
Invalid credentials
" - : ""; + @GetMapping(value = "/login", produces = {"text/html"}) + public Mono getLoginPage() { + return Mono.just(new ClassPathResource(INDEX_HTML)); } - private static String createLogoutSuccess(boolean isLogoutSuccess) { - return isLogoutSuccess - ? "
You have been signed out
" - : ""; - } } diff --git a/api/src/main/java/io/kafbat/ui/util/StaticFileWebFilter.java b/api/src/main/java/io/kafbat/ui/util/StaticFileWebFilter.java index 391fa8dbd..0234b5b97 100644 --- a/api/src/main/java/io/kafbat/ui/util/StaticFileWebFilter.java +++ b/api/src/main/java/io/kafbat/ui/util/StaticFileWebFilter.java @@ -1,7 +1,7 @@ package io.kafbat.ui.util; import java.io.IOException; -import java.io.InputStream; +import org.jetbrains.annotations.NotNull; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpMethod; @@ -16,20 +16,22 @@ import reactor.core.publisher.Mono; public class StaticFileWebFilter implements WebFilter { - private ServerWebExchangeMatcher matcher; - private String contents; + + private final ServerWebExchangeMatcher matcher; + private final String contents; public StaticFileWebFilter(String path, ClassPathResource resource) { this.matcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, path); - try (InputStream inputStream = resource.getInputStream()) { - this.contents = ResourceUtil.readAsString(resource); + + try { + this.contents = ResourceUtil.readAsString(resource); } catch (IOException e) { throw new RuntimeException(e); } } @Override - public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + public @NotNull Mono filter(@NotNull ServerWebExchange exchange, WebFilterChain chain) { return this.matcher.matches(exchange) .filter(ServerWebExchangeMatcher.MatchResult::isMatch) .switchIfEmpty(chain.filter(exchange).then(Mono.empty())) @@ -38,6 +40,7 @@ public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { private Mono render(ServerWebExchange exchange) { String contextPath = exchange.getRequest().getPath().contextPath().value(); + String contentBody = contents .replace("\"assets/", "\"" + contextPath + "/assets/") .replace("PUBLIC-PATH-VARIABLE", contextPath); From 10f0b8fcde334d4447ebc7e368351f2c19ec7aaa Mon Sep 17 00:00:00 2001 From: Roman Zabaluev Date: Mon, 23 Dec 2024 23:19:24 +0800 Subject: [PATCH 6/8] Apply filter for all configs --- .../ui/config/auth/AbstractAuthSecurityConfig.java | 2 -- .../ui/config/auth/BasicAuthSecurityConfig.java | 14 ++++++++------ .../kafbat/ui/config/auth/LdapSecurityConfig.java | 11 ++++++++--- .../kafbat/ui/config/auth/OAuthSecurityConfig.java | 4 +--- .../io/kafbat/ui/util/StaticFileWebFilter.java | 6 ++++++ 5 files changed, 23 insertions(+), 14 deletions(-) diff --git a/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java b/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java index 407d05a1d..62d9787bc 100644 --- a/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java +++ b/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java @@ -6,8 +6,6 @@ protected AbstractAuthSecurityConfig() { } - public static final String INDEX_HTML = "/static/index.html"; - protected static final String[] AUTH_WHITELIST = { /* STATIC */ "/index.html", diff --git a/api/src/main/java/io/kafbat/ui/config/auth/BasicAuthSecurityConfig.java b/api/src/main/java/io/kafbat/ui/config/auth/BasicAuthSecurityConfig.java index 7a25fb3a7..f5e7bc01c 100644 --- a/api/src/main/java/io/kafbat/ui/config/auth/BasicAuthSecurityConfig.java +++ b/api/src/main/java/io/kafbat/ui/config/auth/BasicAuthSecurityConfig.java @@ -1,6 +1,7 @@ package io.kafbat.ui.config.auth; import io.kafbat.ui.util.EmptyRedirectStrategy; +import io.kafbat.ui.util.StaticFileWebFilter; import java.net.URI; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -8,6 +9,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.SecurityWebFiltersOrder; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler; @@ -20,7 +22,6 @@ @Slf4j public class BasicAuthSecurityConfig extends AbstractAuthSecurityConfig { - public static final String LOGIN_URL = "/auth"; public static final String LOGOUT_URL = "/auth?logout"; @Bean @@ -33,19 +34,20 @@ public SecurityWebFilterChain configure(ServerHttpSecurity http) { final var logoutSuccessHandler = new RedirectServerLogoutSuccessHandler(); logoutSuccessHandler.setLogoutSuccessUrl(URI.create(LOGOUT_URL)); - - return http.authorizeExchange(spec -> spec + var builder = http.authorizeExchange(spec -> spec .pathMatchers(AUTH_WHITELIST) .permitAll() .anyExchange() .authenticated() ) - .formLogin(spec -> spec.loginPage(LOGIN_URL).authenticationSuccessHandler(authHandler)) .logout(spec -> spec .logoutSuccessHandler(logoutSuccessHandler) .requiresLogout(ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/logout"))) - .csrf(ServerHttpSecurity.CsrfSpec::disable) - .build(); + .csrf(ServerHttpSecurity.CsrfSpec::disable); + + builder.addFilterAt(new StaticFileWebFilter(), SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING); + + return builder.build(); } } diff --git a/api/src/main/java/io/kafbat/ui/config/auth/LdapSecurityConfig.java b/api/src/main/java/io/kafbat/ui/config/auth/LdapSecurityConfig.java index 1b5a8ca87..3a91ed98d 100644 --- a/api/src/main/java/io/kafbat/ui/config/auth/LdapSecurityConfig.java +++ b/api/src/main/java/io/kafbat/ui/config/auth/LdapSecurityConfig.java @@ -4,6 +4,7 @@ import io.kafbat.ui.service.rbac.AccessControlService; import io.kafbat.ui.service.rbac.extractor.RbacLdapAuthoritiesExtractor; +import io.kafbat.ui.util.StaticFileWebFilter; import java.util.Collection; import java.util.List; import java.util.Optional; @@ -23,6 +24,7 @@ import org.springframework.security.authentication.ReactiveAuthenticationManagerAdapter; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.SecurityWebFiltersOrder; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; @@ -121,7 +123,7 @@ public SecurityWebFilterChain configureLdap(ServerHttpSecurity http) { log.info("Active Directory support for LDAP has been enabled."); } - return http.authorizeExchange(spec -> spec + var builder = http.authorizeExchange(spec -> spec .pathMatchers(AUTH_WHITELIST) .permitAll() .anyExchange() @@ -129,8 +131,11 @@ public SecurityWebFilterChain configureLdap(ServerHttpSecurity http) { ) .formLogin(Customizer.withDefaults()) .logout(Customizer.withDefaults()) - .csrf(ServerHttpSecurity.CsrfSpec::disable) - .build(); + .csrf(ServerHttpSecurity.CsrfSpec::disable); + + builder.addFilterAt(new StaticFileWebFilter(), SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING); + + return builder.build(); } private static class UserDetailsMapper extends LdapUserDetailsMapper { diff --git a/api/src/main/java/io/kafbat/ui/config/auth/OAuthSecurityConfig.java b/api/src/main/java/io/kafbat/ui/config/auth/OAuthSecurityConfig.java index eb54f812b..4794b83ca 100644 --- a/api/src/main/java/io/kafbat/ui/config/auth/OAuthSecurityConfig.java +++ b/api/src/main/java/io/kafbat/ui/config/auth/OAuthSecurityConfig.java @@ -17,7 +17,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.ClassPathResource; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; @@ -64,8 +63,7 @@ public SecurityWebFilterChain configure(ServerHttpSecurity http, OAuthLogoutSucc .csrf(ServerHttpSecurity.CsrfSpec::disable); - builder.addFilterAt(new StaticFileWebFilter("/login", new ClassPathResource(INDEX_HTML)), - SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING); + builder.addFilterAt(new StaticFileWebFilter(), SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING); return builder.build(); } diff --git a/api/src/main/java/io/kafbat/ui/util/StaticFileWebFilter.java b/api/src/main/java/io/kafbat/ui/util/StaticFileWebFilter.java index 0234b5b97..1b74bd374 100644 --- a/api/src/main/java/io/kafbat/ui/util/StaticFileWebFilter.java +++ b/api/src/main/java/io/kafbat/ui/util/StaticFileWebFilter.java @@ -17,9 +17,15 @@ public class StaticFileWebFilter implements WebFilter { + private static final String INDEX_HTML = "/static/index.html"; + private final ServerWebExchangeMatcher matcher; private final String contents; + public StaticFileWebFilter() { + this("/login", new ClassPathResource(INDEX_HTML)); + } + public StaticFileWebFilter(String path, ClassPathResource resource) { this.matcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, path); From 00af933b18961d5c064179b6538ae0f42517c68c Mon Sep 17 00:00:00 2001 From: Roman Zabaluev Date: Wed, 25 Dec 2024 03:43:59 +0800 Subject: [PATCH 7/8] Change login URL and fix basic auth --- .../io/kafbat/ui/config/auth/BasicAuthSecurityConfig.java | 7 ++++++- contract/src/main/resources/swagger/kafbat-ui-api.yaml | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/io/kafbat/ui/config/auth/BasicAuthSecurityConfig.java b/api/src/main/java/io/kafbat/ui/config/auth/BasicAuthSecurityConfig.java index f5e7bc01c..bba01b4b0 100644 --- a/api/src/main/java/io/kafbat/ui/config/auth/BasicAuthSecurityConfig.java +++ b/api/src/main/java/io/kafbat/ui/config/auth/BasicAuthSecurityConfig.java @@ -22,7 +22,8 @@ @Slf4j public class BasicAuthSecurityConfig extends AbstractAuthSecurityConfig { - public static final String LOGOUT_URL = "/auth?logout"; + private static final String LOGIN_URL = "/login"; + private static final String LOGOUT_URL = "/auth?logout"; @Bean public SecurityWebFilterChain configure(ServerHttpSecurity http) { @@ -40,6 +41,10 @@ public SecurityWebFilterChain configure(ServerHttpSecurity http) { .anyExchange() .authenticated() ) + .formLogin(form -> form + .loginPage(LOGIN_URL) + .authenticationSuccessHandler(authHandler) + ) .logout(spec -> spec .logoutSuccessHandler(logoutSuccessHandler) .requiresLogout(ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/logout"))) diff --git a/contract/src/main/resources/swagger/kafbat-ui-api.yaml b/contract/src/main/resources/swagger/kafbat-ui-api.yaml index d1215ec87..dff80b4ce 100644 --- a/contract/src/main/resources/swagger/kafbat-ui-api.yaml +++ b/contract/src/main/resources/swagger/kafbat-ui-api.yaml @@ -2254,7 +2254,7 @@ paths: schema: $ref: '#/components/schemas/AppAuthenticationSettings' - /auth: + /login: post: summary: Authenticate requestBody: From bba68eb7aa5e3031e315caaa182a2b33c7ee210d Mon Sep 17 00:00:00 2001 From: Roman Zabaluev Date: Wed, 25 Dec 2024 04:19:37 +0800 Subject: [PATCH 8/8] Fix LDAP, refactor --- .../auth/AbstractAuthSecurityConfig.java | 20 +++++++++++++++++++ .../config/auth/BasicAuthSecurityConfig.java | 13 ++---------- .../ui/config/auth/LdapSecurityConfig.java | 16 +++++++++------ 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java b/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java index 62d9787bc..265bac03f 100644 --- a/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java +++ b/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java @@ -1,11 +1,19 @@ package io.kafbat.ui.config.auth; +import io.kafbat.ui.util.EmptyRedirectStrategy; +import java.net.URI; +import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler; +import org.springframework.security.web.server.authentication.logout.RedirectServerLogoutSuccessHandler; + abstract class AbstractAuthSecurityConfig { protected AbstractAuthSecurityConfig() { } + protected static final String LOGIN_URL = "/login"; + protected static final String LOGOUT_URL = "/auth?logout"; + protected static final String[] AUTH_WHITELIST = { /* STATIC */ "/index.html", @@ -30,4 +38,16 @@ protected AbstractAuthSecurityConfig() { "/api/authorization" }; + protected RedirectServerAuthenticationSuccessHandler emptyRedirectSuccessHandler() { + final var authHandler = new RedirectServerAuthenticationSuccessHandler(); + authHandler.setRedirectStrategy(new EmptyRedirectStrategy()); + return authHandler; + } + + protected RedirectServerLogoutSuccessHandler redirectLogoutSuccessHandler() { + final var logoutSuccessHandler = new RedirectServerLogoutSuccessHandler(); + logoutSuccessHandler.setLogoutSuccessUrl(URI.create(LOGOUT_URL)); + return logoutSuccessHandler; + } + } diff --git a/api/src/main/java/io/kafbat/ui/config/auth/BasicAuthSecurityConfig.java b/api/src/main/java/io/kafbat/ui/config/auth/BasicAuthSecurityConfig.java index bba01b4b0..db8ef8153 100644 --- a/api/src/main/java/io/kafbat/ui/config/auth/BasicAuthSecurityConfig.java +++ b/api/src/main/java/io/kafbat/ui/config/auth/BasicAuthSecurityConfig.java @@ -22,19 +22,10 @@ @Slf4j public class BasicAuthSecurityConfig extends AbstractAuthSecurityConfig { - private static final String LOGIN_URL = "/login"; - private static final String LOGOUT_URL = "/auth?logout"; - @Bean public SecurityWebFilterChain configure(ServerHttpSecurity http) { log.info("Configuring LOGIN_FORM authentication."); - final var authHandler = new RedirectServerAuthenticationSuccessHandler(); - authHandler.setRedirectStrategy(new EmptyRedirectStrategy()); - - final var logoutSuccessHandler = new RedirectServerLogoutSuccessHandler(); - logoutSuccessHandler.setLogoutSuccessUrl(URI.create(LOGOUT_URL)); - var builder = http.authorizeExchange(spec -> spec .pathMatchers(AUTH_WHITELIST) .permitAll() @@ -43,10 +34,10 @@ public SecurityWebFilterChain configure(ServerHttpSecurity http) { ) .formLogin(form -> form .loginPage(LOGIN_URL) - .authenticationSuccessHandler(authHandler) + .authenticationSuccessHandler(emptyRedirectSuccessHandler()) ) .logout(spec -> spec - .logoutSuccessHandler(logoutSuccessHandler) + .logoutSuccessHandler(redirectLogoutSuccessHandler()) .requiresLogout(ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/logout"))) .csrf(ServerHttpSecurity.CsrfSpec::disable); diff --git a/api/src/main/java/io/kafbat/ui/config/auth/LdapSecurityConfig.java b/api/src/main/java/io/kafbat/ui/config/auth/LdapSecurityConfig.java index 3a91ed98d..4d89a9568 100644 --- a/api/src/main/java/io/kafbat/ui/config/auth/LdapSecurityConfig.java +++ b/api/src/main/java/io/kafbat/ui/config/auth/LdapSecurityConfig.java @@ -1,7 +1,5 @@ package io.kafbat.ui.config.auth; -import static io.kafbat.ui.config.auth.AbstractAuthSecurityConfig.AUTH_WHITELIST; - import io.kafbat.ui.service.rbac.AccessControlService; import io.kafbat.ui.service.rbac.extractor.RbacLdapAuthoritiesExtractor; import io.kafbat.ui.util.StaticFileWebFilter; @@ -15,6 +13,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.ldap.core.DirContextOperations; import org.springframework.ldap.core.support.BaseLdapPathContextSource; import org.springframework.ldap.core.support.LdapContextSource; @@ -22,7 +21,6 @@ import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManagerAdapter; -import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.SecurityWebFiltersOrder; import org.springframework.security.config.web.server.ServerHttpSecurity; @@ -38,6 +36,7 @@ import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator; import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper; import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; @Configuration @EnableWebFluxSecurity @@ -45,7 +44,7 @@ @EnableConfigurationProperties(LdapProperties.class) @RequiredArgsConstructor @Slf4j -public class LdapSecurityConfig { +public class LdapSecurityConfig extends AbstractAuthSecurityConfig { private final LdapProperties props; @@ -129,8 +128,13 @@ public SecurityWebFilterChain configureLdap(ServerHttpSecurity http) { .anyExchange() .authenticated() ) - .formLogin(Customizer.withDefaults()) - .logout(Customizer.withDefaults()) + .formLogin(form -> form + .loginPage(LOGIN_URL) + .authenticationSuccessHandler(emptyRedirectSuccessHandler()) + ) + .logout(spec -> spec + .logoutSuccessHandler(redirectLogoutSuccessHandler()) + .requiresLogout(ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/logout"))) .csrf(ServerHttpSecurity.CsrfSpec::disable); builder.addFilterAt(new StaticFileWebFilter(), SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING);