mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-25 02:14:38 -05:00
feat(security)!: make basic auth required on OSS (#9688)
* feat(security)!: make basic auth required on OSS * clean(security)!: put the auth filter code into a publisher * clean(security)!: add unit tests * fix(core): merge --------- Co-authored-by: nKwiatkowski <nkwiatkowski@kestra.io>
This commit is contained in:
@@ -74,9 +74,6 @@ kestra:
|
|||||||
path: /tmp/kestra-wd/tmp
|
path: /tmp/kestra-wd/tmp
|
||||||
anonymous-usage-report:
|
anonymous-usage-report:
|
||||||
enabled: false
|
enabled: false
|
||||||
server:
|
|
||||||
basic-auth:
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
datasources:
|
datasources:
|
||||||
postgres:
|
postgres:
|
||||||
|
|||||||
3
Makefile
3
Makefile
@@ -130,9 +130,6 @@ datasources:
|
|||||||
username: kestra
|
username: kestra
|
||||||
password: k3str4
|
password: k3str4
|
||||||
kestra:
|
kestra:
|
||||||
server:
|
|
||||||
basic-auth:
|
|
||||||
enabled: false
|
|
||||||
encryption:
|
encryption:
|
||||||
secret-key: 3ywuDa/Ec61VHkOX3RlI9gYq7CaD0mv0Pf3DHtAXA6U=
|
secret-key: 3ywuDa/Ec61VHkOX3RlI9gYq7CaD0mv0Pf3DHtAXA6U=
|
||||||
repository:
|
repository:
|
||||||
|
|||||||
@@ -184,7 +184,6 @@ kestra:
|
|||||||
|
|
||||||
server:
|
server:
|
||||||
basic-auth:
|
basic-auth:
|
||||||
enabled: false
|
|
||||||
# These URLs will not be authenticated, by default we open some of the Micronaut default endpoints but not all for security reasons
|
# These URLs will not be authenticated, by default we open some of the Micronaut default endpoints but not all for security reasons
|
||||||
open-urls:
|
open-urls:
|
||||||
- "/ping"
|
- "/ping"
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ services:
|
|||||||
kestra:
|
kestra:
|
||||||
server:
|
server:
|
||||||
basic-auth:
|
basic-auth:
|
||||||
enabled: false
|
|
||||||
username: "admin@kestra.io" # it must be a valid email address
|
username: "admin@kestra.io" # it must be a valid email address
|
||||||
password: kestra
|
password: kestra
|
||||||
repository:
|
repository:
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ kestra:
|
|||||||
secret-key: not_really_a_secret_only_for_unit_test
|
secret-key: not_really_a_secret_only_for_unit_test
|
||||||
anonymous-usage-report:
|
anonymous-usage-report:
|
||||||
enabled: false
|
enabled: false
|
||||||
server:
|
|
||||||
basic-auth:
|
|
||||||
enabled: false
|
|
||||||
datasources:
|
datasources:
|
||||||
postgres:
|
postgres:
|
||||||
url: jdbc:postgresql://postgres:5432/kestra_unit
|
url: jdbc:postgresql://postgres:5432/kestra_unit
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ public class MiscController {
|
|||||||
@Get("/configs")
|
@Get("/configs")
|
||||||
@ExecuteOn(TaskExecutors.IO)
|
@ExecuteOn(TaskExecutors.IO)
|
||||||
@Operation(tags = {"Misc"}, summary = "Retrieve the instance configuration.", description = "Global endpoint available to all users.")
|
@Operation(tags = {"Misc"}, summary = "Retrieve the instance configuration.", description = "Global endpoint available to all users.")
|
||||||
public Configuration getConfiguration() throws JsonProcessingException {
|
public Configuration getConfiguration() throws JsonProcessingException {
|
||||||
Configuration.ConfigurationBuilder<?, ?> builder = Configuration
|
Configuration.ConfigurationBuilder<?, ?> builder = Configuration
|
||||||
.builder()
|
.builder()
|
||||||
.uuid(instanceService.fetch())
|
.uuid(instanceService.fetch())
|
||||||
@@ -104,8 +104,7 @@ public class MiscController {
|
|||||||
.preview(Preview.builder()
|
.preview(Preview.builder()
|
||||||
.initial(this.initialPreviewRows)
|
.initial(this.initialPreviewRows)
|
||||||
.max(this.maxPreviewRows)
|
.max(this.maxPreviewRows)
|
||||||
.build()
|
.build())
|
||||||
).isBasicAuthEnabled(basicAuthService.isEnabled())
|
|
||||||
.isAiEnabled(applicationContext.containsBean(AiController.class))
|
.isAiEnabled(applicationContext.containsBean(AiController.class))
|
||||||
.systemNamespace(namespaceUtils.getSystemFlowNamespace())
|
.systemNamespace(namespaceUtils.getSystemFlowNamespace())
|
||||||
.resourceToFilters(QueryFilter.Resource.asResourceList())
|
.resourceToFilters(QueryFilter.Resource.asResourceList())
|
||||||
@@ -174,8 +173,6 @@ public class MiscController {
|
|||||||
|
|
||||||
Preview preview;
|
Preview preview;
|
||||||
|
|
||||||
Boolean isBasicAuthEnabled;
|
|
||||||
|
|
||||||
String systemNamespace;
|
String systemNamespace;
|
||||||
|
|
||||||
List<String> hiddenLabelsPrefixes;
|
List<String> hiddenLabelsPrefixes;
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ import io.micronaut.web.router.RouteMatchUtils;
|
|||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import org.reactivestreams.Publisher;
|
import org.reactivestreams.Publisher;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
import reactor.core.scheduler.Schedulers;
|
|
||||||
|
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
|
||||||
@Filter("/**")
|
@Filter("/**")
|
||||||
@Requires(property = "kestra.server-type", pattern = "(WEBSERVER|STANDALONE)")
|
@Requires(property = "kestra.server-type", pattern = "(WEBSERVER|STANDALONE)")
|
||||||
@@ -42,15 +42,10 @@ public class AuthenticationFilter implements HttpServerFilter {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, ServerFilterChain chain) {
|
public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, ServerFilterChain chain) {
|
||||||
return Mono.fromCallable(() -> basicAuthService.isEnabled())
|
return Mono.fromCallable(basicAuthService::configuration)
|
||||||
.subscribeOn(Schedulers.boundedElastic())
|
.subscribeOn(Schedulers.boundedElastic())
|
||||||
.flux()
|
.flux()
|
||||||
.switchMap(enabled -> {
|
.flatMap(basicAuthConfiguration -> {
|
||||||
if (!enabled) {
|
|
||||||
return chain.proceed(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
BasicAuthService.SaltedBasicAuthConfiguration basicAuthConfiguration = this.basicAuthService.configuration();
|
|
||||||
boolean isOpenUrl = Optional.ofNullable(basicAuthConfiguration.getOpenUrls())
|
boolean isOpenUrl = Optional.ofNullable(basicAuthConfiguration.getOpenUrls())
|
||||||
.map(Collection::stream)
|
.map(Collection::stream)
|
||||||
.map(stream -> stream.anyMatch(s -> request.getPath().startsWith(s)))
|
.map(stream -> stream.anyMatch(s -> request.getPath().startsWith(s)))
|
||||||
@@ -68,13 +63,14 @@ public class AuthenticationFilter implements HttpServerFilter {
|
|||||||
|
|
||||||
if (basicAuth.isEmpty() ||
|
if (basicAuth.isEmpty() ||
|
||||||
!basicAuth.get().username().equals(basicAuthConfiguration.getUsername()) ||
|
!basicAuth.get().username().equals(basicAuthConfiguration.getUsername()) ||
|
||||||
!AuthUtils.encodePassword(basicAuthConfiguration.getSalt(), basicAuth.get().password()).equals(basicAuthConfiguration.getPassword())
|
!AuthUtils.encodePassword(basicAuthConfiguration.getSalt(),
|
||||||
|
basicAuth.get().password()).equals(basicAuthConfiguration.getPassword())
|
||||||
) {
|
) {
|
||||||
return Flux.just(HttpResponse.unauthorized().header("WWW-Authenticate", PREFIX + " realm=" + basicAuthConfiguration.getRealm()));
|
return Flux.just(HttpResponse.unauthorized());
|
||||||
}
|
}
|
||||||
|
|
||||||
return chain.proceed(request);
|
return chain.proceed(request);
|
||||||
});
|
}) ;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("rawtypes")
|
@SuppressWarnings("rawtypes")
|
||||||
|
|||||||
@@ -50,22 +50,21 @@ public class BasicAuthService {
|
|||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
private void init() {
|
private void init() {
|
||||||
if (Boolean.TRUE.equals(this.basicAuthConfiguration.getEnabled())) {
|
if (basicAuthConfiguration == null){
|
||||||
this.save(this.basicAuthConfiguration);
|
settingRepository.save(Setting.builder()
|
||||||
} else if (Boolean.FALSE.equals(this.basicAuthConfiguration.getEnabled())) {
|
.key(BASIC_AUTH_SETTINGS_KEY)
|
||||||
this.unsecure();
|
.value(new BasicAuthConfiguration())
|
||||||
|
.build());
|
||||||
|
} else if (basicAuthConfiguration.getUsername() == null || basicAuthConfiguration.getPassword() == null){
|
||||||
|
settingRepository.save(Setting.builder()
|
||||||
|
.key(BASIC_AUTH_SETTINGS_KEY)
|
||||||
|
.value(basicAuthConfiguration)
|
||||||
|
.build());
|
||||||
|
} else {
|
||||||
|
save(basicAuthConfiguration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEnabled() {
|
|
||||||
BasicAuthConfiguration basicAuthConfiguration = configuration();
|
|
||||||
if (basicAuthConfiguration == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Boolean.TRUE.equals(basicAuthConfiguration.getEnabled()) && basicAuthConfiguration.getUsername() != null && basicAuthConfiguration.getPassword() != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void save(BasicAuthConfiguration basicAuthConfiguration) {
|
public void save(BasicAuthConfiguration basicAuthConfiguration) {
|
||||||
save(null, basicAuthConfiguration);
|
save(null, basicAuthConfiguration);
|
||||||
}
|
}
|
||||||
@@ -75,6 +74,10 @@ public class BasicAuthService {
|
|||||||
throw new IllegalArgumentException("Invalid username for Basic Authentication. Please provide a valid email address.");
|
throw new IllegalArgumentException("Invalid username for Basic Authentication. Please provide a valid email address.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (basicAuthConfiguration.getUsername() == null) {
|
||||||
|
throw new IllegalArgumentException("No user name set for Basic Authentication. Please provide a user name.");
|
||||||
|
}
|
||||||
|
|
||||||
if (basicAuthConfiguration.getPassword() == null) {
|
if (basicAuthConfiguration.getPassword() == null) {
|
||||||
throw new IllegalArgumentException("No password set for Basic Authentication. Please provide a password.");
|
throw new IllegalArgumentException("No password set for Basic Authentication. Please provide a password.");
|
||||||
}
|
}
|
||||||
@@ -114,18 +117,6 @@ public class BasicAuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void unsecure() {
|
|
||||||
BasicAuthConfiguration configuration = configuration();
|
|
||||||
if (configuration == null || Boolean.FALSE.equals(configuration.getEnabled())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
settingRepository.save(Setting.builder()
|
|
||||||
.key(BASIC_AUTH_SETTINGS_KEY)
|
|
||||||
.value(configuration.withEnabled(false))
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public SaltedBasicAuthConfiguration configuration() {
|
public SaltedBasicAuthConfiguration configuration() {
|
||||||
return settingRepository.findByKey(BASIC_AUTH_SETTINGS_KEY)
|
return settingRepository.findByKey(BASIC_AUTH_SETTINGS_KEY)
|
||||||
.map(Setting::getValue)
|
.map(Setting::getValue)
|
||||||
@@ -138,8 +129,6 @@ public class BasicAuthService {
|
|||||||
@EqualsAndHashCode
|
@EqualsAndHashCode
|
||||||
@ConfigurationProperties("kestra.server.basic-auth")
|
@ConfigurationProperties("kestra.server.basic-auth")
|
||||||
public static class BasicAuthConfiguration {
|
public static class BasicAuthConfiguration {
|
||||||
@With
|
|
||||||
private Boolean enabled;
|
|
||||||
private String username;
|
private String username;
|
||||||
protected String password;
|
protected String password;
|
||||||
private String realm;
|
private String realm;
|
||||||
@@ -148,13 +137,11 @@ public class BasicAuthService {
|
|||||||
@SuppressWarnings("MnInjectionPoints")
|
@SuppressWarnings("MnInjectionPoints")
|
||||||
@ConfigurationInject
|
@ConfigurationInject
|
||||||
public BasicAuthConfiguration(
|
public BasicAuthConfiguration(
|
||||||
@Nullable Boolean enabled,
|
|
||||||
@Nullable String username,
|
@Nullable String username,
|
||||||
@Nullable String password,
|
@Nullable String password,
|
||||||
@Nullable String realm,
|
@Nullable String realm,
|
||||||
@Nullable List<String> openUrls
|
@Nullable List<String> openUrls
|
||||||
) {
|
) {
|
||||||
this.enabled = enabled;
|
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
this.realm = Optional.ofNullable(realm).orElse("Kestra");
|
this.realm = Optional.ofNullable(realm).orElse("Kestra");
|
||||||
@@ -165,12 +152,11 @@ public class BasicAuthService {
|
|||||||
String username,
|
String username,
|
||||||
String password
|
String password
|
||||||
) {
|
) {
|
||||||
this(true, username, password, null, null);
|
this(username, password, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public BasicAuthConfiguration(BasicAuthConfiguration basicAuthConfiguration) {
|
public BasicAuthConfiguration(BasicAuthConfiguration basicAuthConfiguration) {
|
||||||
if (basicAuthConfiguration != null) {
|
if (basicAuthConfiguration != null) {
|
||||||
this.enabled = basicAuthConfiguration.getEnabled();
|
|
||||||
this.username = basicAuthConfiguration.getUsername();
|
this.username = basicAuthConfiguration.getUsername();
|
||||||
this.password = basicAuthConfiguration.getPassword();
|
this.password = basicAuthConfiguration.getPassword();
|
||||||
this.realm = basicAuthConfiguration.getRealm();
|
this.realm = basicAuthConfiguration.getRealm();
|
||||||
@@ -181,7 +167,6 @@ public class BasicAuthService {
|
|||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
BasicAuthConfiguration withUsernamePassword(String username, String password) {
|
BasicAuthConfiguration withUsernamePassword(String username, String password) {
|
||||||
return new BasicAuthConfiguration(
|
return new BasicAuthConfiguration(
|
||||||
this.enabled,
|
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
this.realm,
|
this.realm,
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
package io.kestra.webserver.controllers.api;
|
|
||||||
|
|
||||||
import io.kestra.webserver.services.BasicAuthService;
|
|
||||||
import io.micronaut.context.annotation.Property;
|
|
||||||
import io.micronaut.http.HttpRequest;
|
|
||||||
import io.micronaut.http.client.annotation.Client;
|
|
||||||
import io.micronaut.reactor.http.client.ReactorHttpClient;
|
|
||||||
import io.kestra.core.junit.annotations.KestraTest;
|
|
||||||
import jakarta.inject.Inject;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
@KestraTest
|
|
||||||
@Property(name = "kestra.server.basic-auth.enabled", value = "true")
|
|
||||||
class MiscControllerSecuredTest {
|
|
||||||
@Inject
|
|
||||||
@Client("/")
|
|
||||||
ReactorHttpClient client;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
private BasicAuthService.BasicAuthConfiguration basicAuthConfiguration;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getConfiguration() {
|
|
||||||
var response = client.toBlocking().retrieve(HttpRequest.GET("/api/v1/configs").basicAuth(
|
|
||||||
basicAuthConfiguration.getUsername(),
|
|
||||||
basicAuthConfiguration.getPassword()
|
|
||||||
), MiscController.Configuration.class);
|
|
||||||
|
|
||||||
assertThat(response.getIsBasicAuthEnabled()).isTrue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
package io.kestra.webserver.controllers.api;
|
package io.kestra.webserver.controllers.api;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
import io.kestra.core.junit.annotations.KestraTest;
|
import io.kestra.core.junit.annotations.KestraTest;
|
||||||
import io.kestra.webserver.services.BasicAuthService;
|
import io.kestra.webserver.services.BasicAuthService;
|
||||||
|
import io.kestra.webserver.services.BasicAuthService.BasicAuthConfiguration;
|
||||||
import io.micronaut.context.annotation.Property;
|
import io.micronaut.context.annotation.Property;
|
||||||
import io.micronaut.http.HttpRequest;
|
import io.micronaut.http.HttpRequest;
|
||||||
import io.micronaut.http.client.annotation.Client;
|
import io.micronaut.http.client.annotation.Client;
|
||||||
@@ -11,8 +14,6 @@ import jakarta.inject.Inject;
|
|||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
@KestraTest
|
@KestraTest
|
||||||
@Property(name = "kestra.system-flows.namespace", value = "some.system.ns")
|
@Property(name = "kestra.system-flows.namespace", value = "some.system.ns")
|
||||||
class MiscControllerTest {
|
class MiscControllerTest {
|
||||||
@@ -23,6 +24,9 @@ class MiscControllerTest {
|
|||||||
@Inject
|
@Inject
|
||||||
BasicAuthService basicAuthService;
|
BasicAuthService basicAuthService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
BasicAuthConfiguration basicAuthConfiguration;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void ping() {
|
void ping() {
|
||||||
var response = client.toBlocking().retrieve("/ping", String.class);
|
var response = client.toBlocking().retrieve("/ping", String.class);
|
||||||
@@ -38,7 +42,6 @@ class MiscControllerTest {
|
|||||||
assertThat(response.getUuid()).isNotNull();
|
assertThat(response.getUuid()).isNotNull();
|
||||||
assertThat(response.getIsTaskRunEnabled()).isFalse();
|
assertThat(response.getIsTaskRunEnabled()).isFalse();
|
||||||
assertThat(response.getIsAnonymousUsageEnabled()).isTrue();
|
assertThat(response.getIsAnonymousUsageEnabled()).isTrue();
|
||||||
assertThat(response.getIsBasicAuthEnabled()).isFalse();
|
|
||||||
assertThat(response.getIsAiEnabled()).isFalse();
|
assertThat(response.getIsAiEnabled()).isFalse();
|
||||||
assertThat(response.getSystemNamespace()).isEqualTo("some.system.ns");
|
assertThat(response.getSystemNamespace()).isEqualTo("some.system.ns");
|
||||||
}
|
}
|
||||||
@@ -70,7 +73,7 @@ class MiscControllerTest {
|
|||||||
MiscController.Configuration.class)
|
MiscController.Configuration.class)
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
basicAuthService.unsecure();
|
basicAuthService.save(basicAuthConfiguration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,36 @@
|
|||||||
package io.kestra.webserver.controllers.api;
|
package io.kestra.webserver.controllers.api;
|
||||||
|
|
||||||
import io.kestra.core.Helpers;
|
|
||||||
import io.kestra.core.models.collectors.Usage;
|
|
||||||
import io.micronaut.http.HttpRequest;
|
|
||||||
import io.micronaut.reactor.http.client.ReactorHttpClient;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import io.kestra.core.junit.annotations.KestraTest;
|
||||||
|
import io.kestra.core.models.collectors.Usage;
|
||||||
|
import io.micronaut.http.HttpRequest;
|
||||||
|
import io.micronaut.http.client.annotation.Client;
|
||||||
|
import io.micronaut.reactor.http.client.ReactorHttpClient;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
@KestraTest
|
||||||
class MiscUsageControllerTest {
|
class MiscUsageControllerTest {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@Client("/")
|
||||||
|
ReactorHttpClient client;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void usages() throws URISyntaxException {
|
void usages() {
|
||||||
Helpers.runApplicationContext(new String[]{"test"}, Map.of("kestra.server-type", "STANDALONE"), (applicationContext, embeddedServer) -> {
|
var response = client.toBlocking().retrieve(HttpRequest.GET("/api/v1/main/usages/all"), Usage.class);
|
||||||
try (ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL())) {
|
|
||||||
|
|
||||||
var response = client.toBlocking().retrieve(HttpRequest.GET("/api/v1/main/usages/all"), Usage.class);
|
assertThat(response.getUuid()).isNotNull();
|
||||||
|
assertThat(response.getVersion()).isNotNull();
|
||||||
assertThat(response.getUuid()).isNotNull();
|
assertThat(response.getStartTime()).isNotNull();
|
||||||
assertThat(response.getVersion()).isNotNull();
|
assertThat(response.getEnvironments()).contains("test");
|
||||||
assertThat(response.getStartTime()).isNotNull();
|
assertThat(response.getStartTime()).isNotNull();
|
||||||
assertThat(response.getEnvironments()).contains("test");
|
assertThat(response.getHost().getUuid()).isNotNull();
|
||||||
assertThat(response.getStartTime()).isNotNull();
|
assertThat(response.getHost().getHardware().getLogicalProcessorCount()).isNotNull();
|
||||||
assertThat(response.getHost().getUuid()).isNotNull();
|
assertThat(response.getHost().getJvm().getName()).isNotNull();
|
||||||
assertThat(response.getHost().getHardware().getLogicalProcessorCount()).isNotNull();
|
assertThat(response.getHost().getOs().getFamily()).isNotNull();
|
||||||
assertThat(response.getHost().getJvm().getName()).isNotNull();
|
assertThat(response.getConfigurations().getRepositoryType()).isEqualTo("h2");
|
||||||
assertThat(response.getHost().getOs().getFamily()).isNotNull();
|
assertThat(response.getConfigurations().getQueueType()).isEqualTo("h2");
|
||||||
assertThat(response.getConfigurations().getRepositoryType()).isEqualTo("h2");
|
|
||||||
assertThat(response.getConfigurations().getQueueType()).isEqualTo("h2");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,23 +5,30 @@ import io.kestra.core.docs.DocumentationWithSchema;
|
|||||||
import io.kestra.core.docs.InputType;
|
import io.kestra.core.docs.InputType;
|
||||||
import io.kestra.core.docs.Plugin;
|
import io.kestra.core.docs.Plugin;
|
||||||
import io.kestra.core.docs.PluginIcon;
|
import io.kestra.core.docs.PluginIcon;
|
||||||
|
import io.kestra.core.junit.annotations.KestraTest;
|
||||||
import io.kestra.core.models.annotations.PluginSubGroup;
|
import io.kestra.core.models.annotations.PluginSubGroup;
|
||||||
import io.kestra.plugin.core.debug.Return;
|
import io.kestra.plugin.core.debug.Return;
|
||||||
import io.kestra.plugin.core.log.Log;
|
import io.kestra.plugin.core.log.Log;
|
||||||
import io.micronaut.core.type.Argument;
|
import io.micronaut.core.type.Argument;
|
||||||
import io.micronaut.http.HttpRequest;
|
import io.micronaut.http.HttpRequest;
|
||||||
|
import io.micronaut.http.client.annotation.Client;
|
||||||
import io.micronaut.reactor.http.client.ReactorHttpClient;
|
import io.micronaut.reactor.http.client.ReactorHttpClient;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@KestraTest
|
||||||
class PluginControllerTest {
|
class PluginControllerTest {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@Client("/")
|
||||||
|
ReactorHttpClient client;
|
||||||
|
|
||||||
public static final String PATH = "/api/v1/plugins";
|
public static final String PATH = "/api/v1/plugins";
|
||||||
|
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
@@ -30,205 +37,170 @@ class PluginControllerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void plugins() throws URISyntaxException {
|
void plugins() {
|
||||||
Helpers.runApplicationContext((applicationContext, embeddedServer) -> {
|
List<Plugin> list = client.toBlocking().retrieve(
|
||||||
ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL());
|
HttpRequest.GET(PATH),
|
||||||
|
Argument.listOf(Plugin.class)
|
||||||
|
);
|
||||||
|
|
||||||
List<Plugin> list = client.toBlocking().retrieve(
|
assertThat(list.size()).isEqualTo(2);
|
||||||
HttpRequest.GET(PATH),
|
|
||||||
Argument.listOf(Plugin.class)
|
|
||||||
);
|
|
||||||
|
|
||||||
assertThat(list.size()).isEqualTo(2);
|
Plugin template = list.stream()
|
||||||
|
.filter(plugin -> plugin.getTitle().equals("plugin-template-test"))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
|
||||||
Plugin template = list.stream()
|
assertThat(template.getTitle()).isEqualTo("plugin-template-test");
|
||||||
.filter(plugin -> plugin.getTitle().equals("plugin-template-test"))
|
assertThat(template.getGroup()).isEqualTo("io.kestra.plugin.templates");
|
||||||
.findFirst()
|
assertThat(template.getDescription()).isEqualTo("Plugin template for Kestra");
|
||||||
.orElseThrow();
|
|
||||||
|
|
||||||
assertThat(template.getTitle()).isEqualTo("plugin-template-test");
|
assertThat(template.getTasks().size()).isEqualTo(1);
|
||||||
assertThat(template.getGroup()).isEqualTo("io.kestra.plugin.templates");
|
assertThat(template.getTasks().getFirst()).isEqualTo("io.kestra.plugin.templates.ExampleTask");
|
||||||
assertThat(template.getDescription()).isEqualTo("Plugin template for Kestra");
|
|
||||||
|
|
||||||
assertThat(template.getTasks().size()).isEqualTo(1);
|
assertThat(template.getGuides().size()).isEqualTo(2);
|
||||||
assertThat(template.getTasks().getFirst()).isEqualTo("io.kestra.plugin.templates.ExampleTask");
|
assertThat(template.getGuides().getFirst()).isEqualTo("authentication");
|
||||||
|
|
||||||
assertThat(template.getGuides().size()).isEqualTo(2);
|
Plugin core = list.stream()
|
||||||
assertThat(template.getGuides().getFirst()).isEqualTo("authentication");
|
.filter(plugin -> plugin.getTitle().equals("core"))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
|
||||||
Plugin core = list.stream()
|
assertThat(core.getCategories()).containsExactlyInAnyOrder(PluginSubGroup.PluginCategory.STORAGE, PluginSubGroup.PluginCategory.CORE);
|
||||||
.filter(plugin -> plugin.getTitle().equals("core"))
|
|
||||||
.findFirst()
|
|
||||||
.orElseThrow();
|
|
||||||
|
|
||||||
assertThat(core.getCategories()).containsExactlyInAnyOrder(PluginSubGroup.PluginCategory.STORAGE, PluginSubGroup.PluginCategory.CORE);
|
// classLoader can lead to duplicate plugins for the core, just verify that the response is still the same
|
||||||
|
list = client.toBlocking().retrieve(
|
||||||
|
HttpRequest.GET(PATH),
|
||||||
|
Argument.listOf(Plugin.class)
|
||||||
|
);
|
||||||
|
|
||||||
// classLoader can lead to duplicate plugins for the core, just verify that the response is still the same
|
assertThat(list.size()).isEqualTo(2);
|
||||||
list = client.toBlocking().retrieve(
|
|
||||||
HttpRequest.GET(PATH),
|
|
||||||
Argument.listOf(Plugin.class)
|
|
||||||
);
|
|
||||||
|
|
||||||
assertThat(list.size()).isEqualTo(2);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void icons() throws URISyntaxException {
|
void icons() {
|
||||||
Helpers.runApplicationContext((applicationContext, embeddedServer) -> {
|
Map<String, PluginIcon> list = client.toBlocking().retrieve(
|
||||||
ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL());
|
HttpRequest.GET(PATH + "/icons"),
|
||||||
|
Argument.mapOf(String.class, PluginIcon.class)
|
||||||
|
);
|
||||||
|
|
||||||
Map<String, PluginIcon> list = client.toBlocking().retrieve(
|
assertThat(list.entrySet().stream()
|
||||||
HttpRequest.GET(PATH + "/icons"),
|
.filter(e -> e.getKey().equals(Log.class.getName()))
|
||||||
Argument.mapOf(String.class, PluginIcon.class)
|
.findFirst().orElseThrow().getValue().getIcon()).isNotNull();
|
||||||
);
|
// test an alias
|
||||||
|
assertThat(list.entrySet().stream()
|
||||||
assertThat(list.entrySet().stream().filter(e -> e.getKey().equals(Log.class.getName())).findFirst().orElseThrow().getValue().getIcon()).isNotNull();
|
.filter(e -> e.getKey().equals("io.kestra.core.tasks.log.Log"))
|
||||||
// test an alias
|
.findFirst().orElseThrow().getValue().getIcon()).isNotNull();
|
||||||
assertThat(list.entrySet().stream().filter(e -> e.getKey().equals("io.kestra.core.tasks.log.Log")).findFirst().orElseThrow().getValue().getIcon()).isNotNull();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@Test
|
@Test
|
||||||
void returnTask() throws URISyntaxException {
|
void returnTask() {
|
||||||
Helpers.runApplicationContext((applicationContext, embeddedServer) -> {
|
DocumentationWithSchema doc = client.toBlocking().retrieve(
|
||||||
ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL());
|
HttpRequest.GET(PATH + "/" + Return.class.getName()),
|
||||||
|
DocumentationWithSchema.class
|
||||||
|
);
|
||||||
|
|
||||||
DocumentationWithSchema doc = client.toBlocking().retrieve(
|
assertThat(doc.getMarkdown()).contains("io.kestra.plugin.core.debug.Return");
|
||||||
HttpRequest.GET(PATH + "/" + Return.class.getName()),
|
assertThat(doc.getMarkdown()).contains("Return a value for debugging purposes.");
|
||||||
DocumentationWithSchema.class
|
assertThat(doc.getMarkdown()).contains("The templated string to render");
|
||||||
);
|
assertThat(doc.getMarkdown()).contains("The generated string");
|
||||||
|
assertThat(((Map<String, Object>) doc.getSchema().getProperties().get("properties")).size()).isEqualTo(1);
|
||||||
assertThat(doc.getMarkdown()).contains("io.kestra.plugin.core.debug.Return");
|
assertThat(((Map<String, Object>) doc.getSchema().getOutputs().get("properties")).size()).isEqualTo(1);
|
||||||
assertThat(doc.getMarkdown()).contains("Return a value for debugging purposes.");
|
|
||||||
assertThat(doc.getMarkdown()).contains("The templated string to render");
|
|
||||||
assertThat(doc.getMarkdown()).contains("The generated string");
|
|
||||||
assertThat(((Map<String, Object>) doc.getSchema().getProperties().get("properties")).size()).isEqualTo(1);
|
|
||||||
assertThat(((Map<String, Object>) doc.getSchema().getOutputs().get("properties")).size()).isEqualTo(1);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@Test
|
@Test
|
||||||
void docs() throws URISyntaxException {
|
void docs() {
|
||||||
Helpers.runApplicationContext((applicationContext, embeddedServer) -> {
|
DocumentationWithSchema doc = client.toBlocking().retrieve(
|
||||||
ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL());
|
HttpRequest.GET(PATH + "/io.kestra.plugin.templates.ExampleTask"),
|
||||||
|
DocumentationWithSchema.class
|
||||||
|
);
|
||||||
|
|
||||||
DocumentationWithSchema doc = client.toBlocking().retrieve(
|
assertThat(doc.getMarkdown()).contains("io.kestra.plugin.templates.ExampleTask");
|
||||||
HttpRequest.GET(PATH + "/io.kestra.plugin.templates.ExampleTask"),
|
assertThat(((Map<String, Object>) doc.getSchema().getProperties().get("properties")).size()).isEqualTo(5);
|
||||||
DocumentationWithSchema.class
|
assertThat(((Map<String, Object>) doc.getSchema().getOutputs().get("properties")).size()).isEqualTo(1);
|
||||||
);
|
|
||||||
|
|
||||||
assertThat(doc.getMarkdown()).contains("io.kestra.plugin.templates.ExampleTask");
|
|
||||||
assertThat(((Map<String, Object>) doc.getSchema().getProperties().get("properties")).size()).isEqualTo(5);
|
|
||||||
assertThat(((Map<String, Object>) doc.getSchema().getOutputs().get("properties")).size()).isEqualTo(1);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void docWithAlert() throws URISyntaxException {
|
void docWithAlert() {
|
||||||
Helpers.runApplicationContext((applicationContext, embeddedServer) -> {
|
DocumentationWithSchema doc = client.toBlocking().retrieve(
|
||||||
ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL());
|
HttpRequest.GET(PATH + "/io.kestra.plugin.core.state.Set"),
|
||||||
|
DocumentationWithSchema.class
|
||||||
|
);
|
||||||
|
|
||||||
DocumentationWithSchema doc = client.toBlocking().retrieve(
|
assertThat(doc.getMarkdown()).contains("io.kestra.plugin.core.state.Set");
|
||||||
HttpRequest.GET(PATH + "/io.kestra.plugin.core.state.Set"),
|
assertThat(doc.getMarkdown()).contains("::: warning\n");
|
||||||
DocumentationWithSchema.class
|
|
||||||
);
|
|
||||||
|
|
||||||
assertThat(doc.getMarkdown()).contains("io.kestra.plugin.core.state.Set");
|
|
||||||
assertThat(doc.getMarkdown()).contains("::: warning\n");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@Test
|
@Test
|
||||||
void taskWithBase() throws URISyntaxException {
|
void taskWithBase() {
|
||||||
Helpers.runApplicationContext((applicationContext, embeddedServer) -> {
|
DocumentationWithSchema doc = client.toBlocking().retrieve(
|
||||||
ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL());
|
HttpRequest.GET(PATH + "/io.kestra.plugin.templates.ExampleTask?all=true"),
|
||||||
|
DocumentationWithSchema.class
|
||||||
|
);
|
||||||
|
|
||||||
DocumentationWithSchema doc = client.toBlocking().retrieve(
|
Map<String, Map<String, Object>> properties = (Map<String, Map<String, Object>>) doc.getSchema().getProperties().get("properties");
|
||||||
HttpRequest.GET(PATH + "/io.kestra.plugin.templates.ExampleTask?all=true"),
|
|
||||||
DocumentationWithSchema.class
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, Map<String, Object>> properties = (Map<String, Map<String, Object>>) doc.getSchema().getProperties().get("properties");
|
assertThat(doc.getMarkdown()).contains("io.kestra.plugin.templates.ExampleTask");
|
||||||
|
assertThat(properties.size()).isEqualTo(17);
|
||||||
assertThat(doc.getMarkdown()).contains("io.kestra.plugin.templates.ExampleTask");
|
assertThat(properties.get("id").size()).isEqualTo(4);
|
||||||
assertThat(properties.size()).isEqualTo(17);
|
assertThat(((Map<String, Object>) doc.getSchema().getOutputs().get("properties")).size()).isEqualTo(1);
|
||||||
assertThat(properties.get("id").size()).isEqualTo(4);
|
|
||||||
assertThat(((Map<String, Object>) doc.getSchema().getOutputs().get("properties")).size()).isEqualTo(1);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void flow() throws URISyntaxException {
|
void flow() {
|
||||||
Helpers.runApplicationContext((applicationContext, embeddedServer) -> {
|
Map<String, Object> doc = client.toBlocking().retrieve(
|
||||||
ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL());
|
HttpRequest.GET(PATH + "/schemas/flow"),
|
||||||
Map<String, Object> doc = client.toBlocking().retrieve(
|
Argument.mapOf(String.class, Object.class)
|
||||||
HttpRequest.GET(PATH + "/schemas/flow"),
|
);
|
||||||
Argument.mapOf(String.class, Object.class)
|
|
||||||
);
|
|
||||||
|
|
||||||
assertThat(doc.get("$ref")).isEqualTo("#/definitions/io.kestra.core.models.flows.Flow");
|
assertThat(doc.get("$ref")).isEqualTo("#/definitions/io.kestra.core.models.flows.Flow");
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void template() throws URISyntaxException {
|
void template() {
|
||||||
Helpers.runApplicationContext((applicationContext, embeddedServer) -> {
|
Map<String, Object> doc = client.toBlocking().retrieve(
|
||||||
ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL());
|
HttpRequest.GET(PATH + "/schemas/template"),
|
||||||
Map<String, Object> doc = client.toBlocking().retrieve(
|
Argument.mapOf(String.class, Object.class)
|
||||||
HttpRequest.GET(PATH + "/schemas/template"),
|
);
|
||||||
Argument.mapOf(String.class, Object.class)
|
|
||||||
);
|
|
||||||
|
|
||||||
assertThat(doc.get("$ref")).isEqualTo("#/definitions/io.kestra.core.models.templates.Template");
|
assertThat(doc.get("$ref")).isEqualTo("#/definitions/io.kestra.core.models.templates.Template");
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void task() throws URISyntaxException {
|
void task() {
|
||||||
Helpers.runApplicationContext((applicationContext, embeddedServer) -> {
|
Map<String, Object> doc = client.toBlocking().retrieve(
|
||||||
ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL());
|
HttpRequest.GET(PATH + "/schemas/task"),
|
||||||
Map<String, Object> doc = client.toBlocking().retrieve(
|
Argument.mapOf(String.class, Object.class)
|
||||||
HttpRequest.GET(PATH + "/schemas/task"),
|
);
|
||||||
Argument.mapOf(String.class, Object.class)
|
|
||||||
);
|
|
||||||
|
|
||||||
assertThat(doc.get("$ref")).isEqualTo("#/definitions/io.kestra.core.models.tasks.Task");
|
assertThat(doc.get("$ref")).isEqualTo("#/definitions/io.kestra.core.models.tasks.Task");
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void inputs() throws URISyntaxException {
|
void inputs() {
|
||||||
Helpers.runApplicationContext((applicationContext, embeddedServer) -> {
|
List<InputType> doc = client.toBlocking().retrieve(
|
||||||
ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL());
|
HttpRequest.GET(PATH + "/inputs"),
|
||||||
List<InputType> doc = client.toBlocking().retrieve(
|
Argument.listOf(InputType.class)
|
||||||
HttpRequest.GET(PATH + "/inputs"),
|
);
|
||||||
Argument.listOf(InputType.class)
|
|
||||||
);
|
|
||||||
|
|
||||||
assertThat(doc.size()).isEqualTo(19);
|
assertThat(doc.size()).isEqualTo(19);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@Test
|
@Test
|
||||||
void input() throws URISyntaxException {
|
void input() {
|
||||||
Helpers.runApplicationContext((applicationContext, embeddedServer) -> {
|
DocumentationWithSchema doc = client.toBlocking().retrieve(
|
||||||
ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL());
|
HttpRequest.GET(PATH + "/inputs/STRING"),
|
||||||
DocumentationWithSchema doc = client.toBlocking().retrieve(
|
DocumentationWithSchema.class
|
||||||
HttpRequest.GET(PATH + "/inputs/STRING"),
|
);
|
||||||
DocumentationWithSchema.class
|
|
||||||
);
|
|
||||||
|
|
||||||
assertThat(doc.getSchema().getProperties().size()).isEqualTo(3);
|
assertThat(doc.getSchema().getProperties().size()).isEqualTo(3);
|
||||||
Map<String, Object> properties = (Map<String, Object>) doc.getSchema().getProperties().get("properties");
|
Map<String, Object> properties = (Map<String, Object>) doc.getSchema().getProperties().get("properties");
|
||||||
assertThat(properties.size()).isEqualTo(8);
|
assertThat(properties.size()).isEqualTo(8);
|
||||||
// assertThat(((Map<String, Object>) properties.get("name")).get("$deprecated"), is(true));
|
assertThat(((Map<String, Object>) properties.get("name")).get("$deprecated")).isEqualTo(true);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
package io.kestra.webserver.filter;
|
package io.kestra.webserver.filter;
|
||||||
|
|
||||||
import io.kestra.webserver.services.BasicAuthService;
|
import io.kestra.webserver.services.BasicAuthService;
|
||||||
import io.micronaut.context.annotation.Property;
|
|
||||||
import io.micronaut.context.annotation.Value;
|
|
||||||
import io.micronaut.http.HttpRequest;
|
import io.micronaut.http.HttpRequest;
|
||||||
import io.micronaut.http.HttpStatus;
|
import io.micronaut.http.HttpStatus;
|
||||||
|
import io.micronaut.http.MutableHttpResponse;
|
||||||
import io.micronaut.http.client.annotation.Client;
|
import io.micronaut.http.client.annotation.Client;
|
||||||
import io.micronaut.http.client.exceptions.HttpClientResponseException;
|
import io.micronaut.http.client.exceptions.HttpClientResponseException;
|
||||||
import io.micronaut.reactor.http.client.ReactorHttpClient;
|
import io.micronaut.reactor.http.client.ReactorHttpClient;
|
||||||
import io.kestra.core.junit.annotations.KestraTest;
|
import io.kestra.core.junit.annotations.KestraTest;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.reactivestreams.Publisher;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
@KestraTest
|
@KestraTest
|
||||||
@Property(name = "kestra.server.basic-auth.enabled", value = "true")
|
|
||||||
class AuthenticationFilterTest {
|
class AuthenticationFilterTest {
|
||||||
@Inject
|
@Inject
|
||||||
@Client("/")
|
@Client("/")
|
||||||
@@ -25,10 +26,11 @@ class AuthenticationFilterTest {
|
|||||||
@Inject
|
@Inject
|
||||||
private BasicAuthService.BasicAuthConfiguration basicAuthConfiguration;
|
private BasicAuthService.BasicAuthConfiguration basicAuthConfiguration;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private AuthenticationFilter filter;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testUnauthorized() {
|
void testUnauthorized() {
|
||||||
assertThrows(HttpClientResponseException.class, () -> client.toBlocking().exchange("/api/v1/configs"));
|
|
||||||
|
|
||||||
assertThrows(HttpClientResponseException.class, () -> client.toBlocking()
|
assertThrows(HttpClientResponseException.class, () -> client.toBlocking()
|
||||||
.exchange(HttpRequest.GET("/api/v1/configs").basicAuth("anonymous", "hacker")));
|
.exchange(HttpRequest.GET("/api/v1/configs").basicAuth("anonymous", "hacker")));
|
||||||
}
|
}
|
||||||
@@ -57,4 +59,35 @@ class AuthenticationFilterTest {
|
|||||||
|
|
||||||
assertThat(response.getStatus().getCode()).isEqualTo(HttpStatus.OK.getCode());
|
assertThat(response.getStatus().getCode()).isEqualTo(HttpStatus.OK.getCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void should_unauthorized_with_wrong_username() {
|
||||||
|
HttpClientResponseException e = assertThrows(HttpClientResponseException.class,
|
||||||
|
() -> client.toBlocking()
|
||||||
|
.exchange(HttpRequest.GET("/api/v1/configs").basicAuth(
|
||||||
|
"incorrect",
|
||||||
|
basicAuthConfiguration.getPassword()
|
||||||
|
)));
|
||||||
|
|
||||||
|
assertThat(e.getResponse().getStatus().getCode()).isEqualTo(HttpStatus.UNAUTHORIZED.getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void should_unauthorized_with_wrong_password() {
|
||||||
|
HttpClientResponseException e = assertThrows(HttpClientResponseException.class,
|
||||||
|
() -> client.toBlocking()
|
||||||
|
.exchange(HttpRequest.GET("/api/v1/configs").basicAuth(
|
||||||
|
basicAuthConfiguration.getUsername(),
|
||||||
|
"incorrect"
|
||||||
|
)));
|
||||||
|
|
||||||
|
assertThat(e.getResponse().getStatus().getCode()).isEqualTo(HttpStatus.UNAUTHORIZED.getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void should_unauthorized_without_token(){
|
||||||
|
MutableHttpResponse<?> response = Mono.from(filter.doFilter(
|
||||||
|
HttpRequest.GET("/api/v1/configs"), null)).block();
|
||||||
|
assertThat(response.getStatus().getCode()).isEqualTo(HttpStatus.UNAUTHORIZED.getCode());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package io.kestra.webserver.filter;
|
||||||
|
|
||||||
|
import io.kestra.webserver.services.BasicAuthService;
|
||||||
|
import io.kestra.webserver.services.BasicAuthService.BasicAuthConfiguration;
|
||||||
|
import io.micronaut.context.annotation.Requires;
|
||||||
|
import io.micronaut.context.env.Environment;
|
||||||
|
import io.micronaut.http.HttpHeaders;
|
||||||
|
import io.micronaut.http.HttpResponse;
|
||||||
|
import io.micronaut.http.MutableHttpRequest;
|
||||||
|
import io.micronaut.http.annotation.Filter;
|
||||||
|
import io.micronaut.http.filter.ClientFilterChain;
|
||||||
|
import io.micronaut.http.filter.HttpClientFilter;
|
||||||
|
import io.micronaut.http.filter.ServerFilterPhase;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import java.util.Base64;
|
||||||
|
import org.reactivestreams.Publisher;
|
||||||
|
|
||||||
|
@Filter("/**")
|
||||||
|
@Requires(env = Environment.TEST)
|
||||||
|
public class TestAuthFilter implements HttpClientFilter {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private BasicAuthConfiguration basicAuthConfiguration;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private BasicAuthService basicAuthService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Publisher<? extends HttpResponse<?>> doFilter(MutableHttpRequest<?> request,
|
||||||
|
ClientFilterChain chain) {
|
||||||
|
//Basic auth may be removed from the database by jdbcTestUtils.drop(); / jdbcTestUtils.migrate();
|
||||||
|
//We need it back to be able to run the tests and avoid NPE while checking the basic authorization
|
||||||
|
if (basicAuthService.configuration() == null){
|
||||||
|
basicAuthService.save(basicAuthConfiguration);
|
||||||
|
}
|
||||||
|
//Add basic authorization header if no header are present in the query
|
||||||
|
if (request.getHeaders().getAuthorization().isEmpty()) {
|
||||||
|
String token = "Basic " + Base64.getEncoder().encodeToString(
|
||||||
|
(basicAuthConfiguration.getUsername() + ":" + basicAuthConfiguration.getPassword()).getBytes());
|
||||||
|
request.getHeaders().add(HttpHeaders.AUTHORIZATION, token);
|
||||||
|
}
|
||||||
|
return chain.proceed(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return ServerFilterPhase.SECURITY.order() - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package io.kestra.webserver.services;
|
|||||||
|
|
||||||
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
|
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
|
||||||
import io.kestra.core.models.Setting;
|
import io.kestra.core.models.Setting;
|
||||||
import io.kestra.core.queues.QueueException;
|
|
||||||
import io.kestra.core.repositories.SettingRepositoryInterface;
|
import io.kestra.core.repositories.SettingRepositoryInterface;
|
||||||
import io.kestra.core.serializers.JacksonMapper;
|
import io.kestra.core.serializers.JacksonMapper;
|
||||||
import io.kestra.core.services.InstanceService;
|
import io.kestra.core.services.InstanceService;
|
||||||
@@ -41,7 +40,7 @@ class BasicAuthServiceTest {
|
|||||||
post(urlEqualTo("/v1/reports/events"))
|
post(urlEqualTo("/v1/reports/events"))
|
||||||
.willReturn(aResponse().withStatus(200))
|
.willReturn(aResponse().withStatus(200))
|
||||||
);
|
);
|
||||||
ctx = ApplicationContext.run(Map.of("kestra.server.basic-auth.enabled", "true"), Environment.TEST);
|
ctx = ApplicationContext.run(Map.of(), Environment.TEST);
|
||||||
|
|
||||||
basicAuthService = ctx.getBean(BasicAuthService.class);
|
basicAuthService = ctx.getBean(BasicAuthService.class);
|
||||||
basicAuthConfiguration = ctx.getBean(BasicAuthService.BasicAuthConfiguration.class);
|
basicAuthConfiguration = ctx.getBean(BasicAuthService.BasicAuthConfiguration.class);
|
||||||
@@ -57,16 +56,14 @@ class BasicAuthServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void initFromYamlConfig() throws TimeoutException, QueueException {
|
void initFromYamlConfig() throws TimeoutException {
|
||||||
assertThat(basicAuthService.isEnabled()).isTrue();
|
|
||||||
|
|
||||||
assertConfigurationMatchesApplicationYaml();
|
assertConfigurationMatchesApplicationYaml();
|
||||||
|
|
||||||
awaitOssAuthEventApiCall("admin@kestra.io");
|
awaitOssAuthEventApiCall("admin@kestra.io");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void secure() throws TimeoutException, QueueException {
|
void secure() throws TimeoutException {
|
||||||
IllegalArgumentException illegalArgumentException = Assertions.assertThrows(
|
IllegalArgumentException illegalArgumentException = Assertions.assertThrows(
|
||||||
IllegalArgumentException.class,
|
IllegalArgumentException.class,
|
||||||
() -> basicAuthService.save(basicAuthConfiguration.withUsernamePassword("not-an-email", "password"))
|
() -> basicAuthService.save(basicAuthConfiguration.withUsernamePassword("not-an-email", "password"))
|
||||||
@@ -80,24 +77,6 @@ class BasicAuthServiceTest {
|
|||||||
awaitOssAuthEventApiCall("some@email.com");
|
awaitOssAuthEventApiCall("some@email.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void unsecure() {
|
|
||||||
assertThat(basicAuthService.isEnabled()).isTrue();
|
|
||||||
BasicAuthService.SaltedBasicAuthConfiguration previousConfiguration = basicAuthService.configuration();
|
|
||||||
|
|
||||||
basicAuthService.unsecure();
|
|
||||||
|
|
||||||
assertThat(basicAuthService.isEnabled()).isFalse();
|
|
||||||
BasicAuthService.SaltedBasicAuthConfiguration newConfiguration = basicAuthService.configuration();
|
|
||||||
|
|
||||||
|
|
||||||
assertThat(newConfiguration.getEnabled()).isFalse();
|
|
||||||
assertThat(newConfiguration.getUsername()).isEqualTo(previousConfiguration.getUsername());
|
|
||||||
assertThat(newConfiguration.getPassword()).isEqualTo(previousConfiguration.getPassword());
|
|
||||||
assertThat(newConfiguration.getRealm()).isEqualTo(previousConfiguration.getRealm());
|
|
||||||
assertThat(newConfiguration.getOpenUrls()).isEqualTo(previousConfiguration.getOpenUrls());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void assertConfigurationMatchesApplicationYaml() {
|
private void assertConfigurationMatchesApplicationYaml() {
|
||||||
BasicAuthService.SaltedBasicAuthConfiguration actualConfiguration = basicAuthService.configuration();
|
BasicAuthService.SaltedBasicAuthConfiguration actualConfiguration = basicAuthService.configuration();
|
||||||
BasicAuthService.SaltedBasicAuthConfiguration applicationYamlConfiguration = new BasicAuthService.SaltedBasicAuthConfiguration(
|
BasicAuthService.SaltedBasicAuthConfiguration applicationYamlConfiguration = new BasicAuthService.SaltedBasicAuthConfiguration(
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ kestra:
|
|||||||
access-log:
|
access-log:
|
||||||
enabled: false
|
enabled: false
|
||||||
basic-auth:
|
basic-auth:
|
||||||
enabled: false
|
|
||||||
username: admin@kestra.io
|
username: admin@kestra.io
|
||||||
password: kestra
|
password: kestra
|
||||||
open-urls:
|
open-urls:
|
||||||
|
|||||||
Reference in New Issue
Block a user