mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-19 18:05:41 -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
|
||||
anonymous-usage-report:
|
||||
enabled: false
|
||||
server:
|
||||
basic-auth:
|
||||
enabled: false
|
||||
|
||||
datasources:
|
||||
postgres:
|
||||
|
||||
3
Makefile
3
Makefile
@@ -130,9 +130,6 @@ datasources:
|
||||
username: kestra
|
||||
password: k3str4
|
||||
kestra:
|
||||
server:
|
||||
basic-auth:
|
||||
enabled: false
|
||||
encryption:
|
||||
secret-key: 3ywuDa/Ec61VHkOX3RlI9gYq7CaD0mv0Pf3DHtAXA6U=
|
||||
repository:
|
||||
|
||||
@@ -184,7 +184,6 @@ kestra:
|
||||
|
||||
server:
|
||||
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
|
||||
open-urls:
|
||||
- "/ping"
|
||||
|
||||
@@ -57,7 +57,6 @@ services:
|
||||
kestra:
|
||||
server:
|
||||
basic-auth:
|
||||
enabled: false
|
||||
username: "admin@kestra.io" # it must be a valid email address
|
||||
password: kestra
|
||||
repository:
|
||||
|
||||
@@ -14,9 +14,6 @@ kestra:
|
||||
secret-key: not_really_a_secret_only_for_unit_test
|
||||
anonymous-usage-report:
|
||||
enabled: false
|
||||
server:
|
||||
basic-auth:
|
||||
enabled: false
|
||||
datasources:
|
||||
postgres:
|
||||
url: jdbc:postgresql://postgres:5432/kestra_unit
|
||||
|
||||
@@ -90,7 +90,7 @@ public class MiscController {
|
||||
@Get("/configs")
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@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
|
||||
.builder()
|
||||
.uuid(instanceService.fetch())
|
||||
@@ -104,8 +104,7 @@ public class MiscController {
|
||||
.preview(Preview.builder()
|
||||
.initial(this.initialPreviewRows)
|
||||
.max(this.maxPreviewRows)
|
||||
.build()
|
||||
).isBasicAuthEnabled(basicAuthService.isEnabled())
|
||||
.build())
|
||||
.isAiEnabled(applicationContext.containsBean(AiController.class))
|
||||
.systemNamespace(namespaceUtils.getSystemFlowNamespace())
|
||||
.resourceToFilters(QueryFilter.Resource.asResourceList())
|
||||
@@ -174,8 +173,6 @@ public class MiscController {
|
||||
|
||||
Preview preview;
|
||||
|
||||
Boolean isBasicAuthEnabled;
|
||||
|
||||
String systemNamespace;
|
||||
|
||||
List<String> hiddenLabelsPrefixes;
|
||||
|
||||
@@ -17,12 +17,12 @@ import io.micronaut.web.router.RouteMatchUtils;
|
||||
import jakarta.inject.Inject;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
@Filter("/**")
|
||||
@Requires(property = "kestra.server-type", pattern = "(WEBSERVER|STANDALONE)")
|
||||
@@ -42,15 +42,10 @@ public class AuthenticationFilter implements HttpServerFilter {
|
||||
|
||||
@Override
|
||||
public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, ServerFilterChain chain) {
|
||||
return Mono.fromCallable(() -> basicAuthService.isEnabled())
|
||||
return Mono.fromCallable(basicAuthService::configuration)
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.flux()
|
||||
.switchMap(enabled -> {
|
||||
if (!enabled) {
|
||||
return chain.proceed(request);
|
||||
}
|
||||
|
||||
BasicAuthService.SaltedBasicAuthConfiguration basicAuthConfiguration = this.basicAuthService.configuration();
|
||||
.flatMap(basicAuthConfiguration -> {
|
||||
boolean isOpenUrl = Optional.ofNullable(basicAuthConfiguration.getOpenUrls())
|
||||
.map(Collection::stream)
|
||||
.map(stream -> stream.anyMatch(s -> request.getPath().startsWith(s)))
|
||||
@@ -68,13 +63,14 @@ public class AuthenticationFilter implements HttpServerFilter {
|
||||
|
||||
if (basicAuth.isEmpty() ||
|
||||
!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);
|
||||
});
|
||||
}) ;
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
|
||||
@@ -50,22 +50,21 @@ public class BasicAuthService {
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
if (Boolean.TRUE.equals(this.basicAuthConfiguration.getEnabled())) {
|
||||
this.save(this.basicAuthConfiguration);
|
||||
} else if (Boolean.FALSE.equals(this.basicAuthConfiguration.getEnabled())) {
|
||||
this.unsecure();
|
||||
if (basicAuthConfiguration == null){
|
||||
settingRepository.save(Setting.builder()
|
||||
.key(BASIC_AUTH_SETTINGS_KEY)
|
||||
.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) {
|
||||
save(null, basicAuthConfiguration);
|
||||
}
|
||||
@@ -75,6 +74,10 @@ public class BasicAuthService {
|
||||
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) {
|
||||
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() {
|
||||
return settingRepository.findByKey(BASIC_AUTH_SETTINGS_KEY)
|
||||
.map(Setting::getValue)
|
||||
@@ -138,8 +129,6 @@ public class BasicAuthService {
|
||||
@EqualsAndHashCode
|
||||
@ConfigurationProperties("kestra.server.basic-auth")
|
||||
public static class BasicAuthConfiguration {
|
||||
@With
|
||||
private Boolean enabled;
|
||||
private String username;
|
||||
protected String password;
|
||||
private String realm;
|
||||
@@ -148,13 +137,11 @@ public class BasicAuthService {
|
||||
@SuppressWarnings("MnInjectionPoints")
|
||||
@ConfigurationInject
|
||||
public BasicAuthConfiguration(
|
||||
@Nullable Boolean enabled,
|
||||
@Nullable String username,
|
||||
@Nullable String password,
|
||||
@Nullable String realm,
|
||||
@Nullable List<String> openUrls
|
||||
) {
|
||||
this.enabled = enabled;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.realm = Optional.ofNullable(realm).orElse("Kestra");
|
||||
@@ -165,12 +152,11 @@ public class BasicAuthService {
|
||||
String username,
|
||||
String password
|
||||
) {
|
||||
this(true, username, password, null, null);
|
||||
this(username, password, null, null);
|
||||
}
|
||||
|
||||
public BasicAuthConfiguration(BasicAuthConfiguration basicAuthConfiguration) {
|
||||
if (basicAuthConfiguration != null) {
|
||||
this.enabled = basicAuthConfiguration.getEnabled();
|
||||
this.username = basicAuthConfiguration.getUsername();
|
||||
this.password = basicAuthConfiguration.getPassword();
|
||||
this.realm = basicAuthConfiguration.getRealm();
|
||||
@@ -181,7 +167,6 @@ public class BasicAuthService {
|
||||
@VisibleForTesting
|
||||
BasicAuthConfiguration withUsernamePassword(String username, String password) {
|
||||
return new BasicAuthConfiguration(
|
||||
this.enabled,
|
||||
username,
|
||||
password,
|
||||
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;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import io.kestra.core.junit.annotations.KestraTest;
|
||||
import io.kestra.webserver.services.BasicAuthService;
|
||||
import io.kestra.webserver.services.BasicAuthService.BasicAuthConfiguration;
|
||||
import io.micronaut.context.annotation.Property;
|
||||
import io.micronaut.http.HttpRequest;
|
||||
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.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@KestraTest
|
||||
@Property(name = "kestra.system-flows.namespace", value = "some.system.ns")
|
||||
class MiscControllerTest {
|
||||
@@ -23,6 +24,9 @@ class MiscControllerTest {
|
||||
@Inject
|
||||
BasicAuthService basicAuthService;
|
||||
|
||||
@Inject
|
||||
BasicAuthConfiguration basicAuthConfiguration;
|
||||
|
||||
@Test
|
||||
void ping() {
|
||||
var response = client.toBlocking().retrieve("/ping", String.class);
|
||||
@@ -38,7 +42,6 @@ class MiscControllerTest {
|
||||
assertThat(response.getUuid()).isNotNull();
|
||||
assertThat(response.getIsTaskRunEnabled()).isFalse();
|
||||
assertThat(response.getIsAnonymousUsageEnabled()).isTrue();
|
||||
assertThat(response.getIsBasicAuthEnabled()).isFalse();
|
||||
assertThat(response.getIsAiEnabled()).isFalse();
|
||||
assertThat(response.getSystemNamespace()).isEqualTo("some.system.ns");
|
||||
}
|
||||
@@ -70,7 +73,7 @@ class MiscControllerTest {
|
||||
MiscController.Configuration.class)
|
||||
);
|
||||
} finally {
|
||||
basicAuthService.unsecure();
|
||||
basicAuthService.save(basicAuthConfiguration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
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 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 {
|
||||
|
||||
@Inject
|
||||
@Client("/")
|
||||
ReactorHttpClient client;
|
||||
|
||||
@Test
|
||||
void usages() throws URISyntaxException {
|
||||
Helpers.runApplicationContext(new String[]{"test"}, Map.of("kestra.server-type", "STANDALONE"), (applicationContext, embeddedServer) -> {
|
||||
try (ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL())) {
|
||||
void usages() {
|
||||
var response = client.toBlocking().retrieve(HttpRequest.GET("/api/v1/main/usages/all"), Usage.class);
|
||||
|
||||
var response = client.toBlocking().retrieve(HttpRequest.GET("/api/v1/main/usages/all"), Usage.class);
|
||||
|
||||
assertThat(response.getUuid()).isNotNull();
|
||||
assertThat(response.getVersion()).isNotNull();
|
||||
assertThat(response.getStartTime()).isNotNull();
|
||||
assertThat(response.getEnvironments()).contains("test");
|
||||
assertThat(response.getStartTime()).isNotNull();
|
||||
assertThat(response.getHost().getUuid()).isNotNull();
|
||||
assertThat(response.getHost().getHardware().getLogicalProcessorCount()).isNotNull();
|
||||
assertThat(response.getHost().getJvm().getName()).isNotNull();
|
||||
assertThat(response.getHost().getOs().getFamily()).isNotNull();
|
||||
assertThat(response.getConfigurations().getRepositoryType()).isEqualTo("h2");
|
||||
assertThat(response.getConfigurations().getQueueType()).isEqualTo("h2");
|
||||
}
|
||||
});
|
||||
assertThat(response.getUuid()).isNotNull();
|
||||
assertThat(response.getVersion()).isNotNull();
|
||||
assertThat(response.getStartTime()).isNotNull();
|
||||
assertThat(response.getEnvironments()).contains("test");
|
||||
assertThat(response.getStartTime()).isNotNull();
|
||||
assertThat(response.getHost().getUuid()).isNotNull();
|
||||
assertThat(response.getHost().getHardware().getLogicalProcessorCount()).isNotNull();
|
||||
assertThat(response.getHost().getJvm().getName()).isNotNull();
|
||||
assertThat(response.getHost().getOs().getFamily()).isNotNull();
|
||||
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.Plugin;
|
||||
import io.kestra.core.docs.PluginIcon;
|
||||
import io.kestra.core.junit.annotations.KestraTest;
|
||||
import io.kestra.core.models.annotations.PluginSubGroup;
|
||||
import io.kestra.plugin.core.debug.Return;
|
||||
import io.kestra.plugin.core.log.Log;
|
||||
import io.micronaut.core.type.Argument;
|
||||
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.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@KestraTest
|
||||
class PluginControllerTest {
|
||||
|
||||
@Inject
|
||||
@Client("/")
|
||||
ReactorHttpClient client;
|
||||
|
||||
public static final String PATH = "/api/v1/plugins";
|
||||
|
||||
@BeforeAll
|
||||
@@ -30,205 +37,170 @@ class PluginControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void plugins() throws URISyntaxException {
|
||||
Helpers.runApplicationContext((applicationContext, embeddedServer) -> {
|
||||
ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL());
|
||||
void plugins() {
|
||||
List<Plugin> list = client.toBlocking().retrieve(
|
||||
HttpRequest.GET(PATH),
|
||||
Argument.listOf(Plugin.class)
|
||||
);
|
||||
|
||||
List<Plugin> list = client.toBlocking().retrieve(
|
||||
HttpRequest.GET(PATH),
|
||||
Argument.listOf(Plugin.class)
|
||||
);
|
||||
assertThat(list.size()).isEqualTo(2);
|
||||
|
||||
assertThat(list.size()).isEqualTo(2);
|
||||
Plugin template = list.stream()
|
||||
.filter(plugin -> plugin.getTitle().equals("plugin-template-test"))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
Plugin template = list.stream()
|
||||
.filter(plugin -> plugin.getTitle().equals("plugin-template-test"))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
assertThat(template.getTitle()).isEqualTo("plugin-template-test");
|
||||
assertThat(template.getGroup()).isEqualTo("io.kestra.plugin.templates");
|
||||
assertThat(template.getDescription()).isEqualTo("Plugin template for Kestra");
|
||||
|
||||
assertThat(template.getTitle()).isEqualTo("plugin-template-test");
|
||||
assertThat(template.getGroup()).isEqualTo("io.kestra.plugin.templates");
|
||||
assertThat(template.getDescription()).isEqualTo("Plugin template for Kestra");
|
||||
assertThat(template.getTasks().size()).isEqualTo(1);
|
||||
assertThat(template.getTasks().getFirst()).isEqualTo("io.kestra.plugin.templates.ExampleTask");
|
||||
|
||||
assertThat(template.getTasks().size()).isEqualTo(1);
|
||||
assertThat(template.getTasks().getFirst()).isEqualTo("io.kestra.plugin.templates.ExampleTask");
|
||||
assertThat(template.getGuides().size()).isEqualTo(2);
|
||||
assertThat(template.getGuides().getFirst()).isEqualTo("authentication");
|
||||
|
||||
assertThat(template.getGuides().size()).isEqualTo(2);
|
||||
assertThat(template.getGuides().getFirst()).isEqualTo("authentication");
|
||||
Plugin core = list.stream()
|
||||
.filter(plugin -> plugin.getTitle().equals("core"))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
Plugin core = list.stream()
|
||||
.filter(plugin -> plugin.getTitle().equals("core"))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
assertThat(core.getCategories()).containsExactlyInAnyOrder(PluginSubGroup.PluginCategory.STORAGE, PluginSubGroup.PluginCategory.CORE);
|
||||
|
||||
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
|
||||
list = client.toBlocking().retrieve(
|
||||
HttpRequest.GET(PATH),
|
||||
Argument.listOf(Plugin.class)
|
||||
);
|
||||
|
||||
assertThat(list.size()).isEqualTo(2);
|
||||
});
|
||||
assertThat(list.size()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void icons() throws URISyntaxException {
|
||||
Helpers.runApplicationContext((applicationContext, embeddedServer) -> {
|
||||
ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL());
|
||||
void icons() {
|
||||
Map<String, PluginIcon> list = client.toBlocking().retrieve(
|
||||
HttpRequest.GET(PATH + "/icons"),
|
||||
Argument.mapOf(String.class, PluginIcon.class)
|
||||
);
|
||||
|
||||
Map<String, PluginIcon> list = client.toBlocking().retrieve(
|
||||
HttpRequest.GET(PATH + "/icons"),
|
||||
Argument.mapOf(String.class, PluginIcon.class)
|
||||
);
|
||||
|
||||
assertThat(list.entrySet().stream().filter(e -> e.getKey().equals(Log.class.getName())).findFirst().orElseThrow().getValue().getIcon()).isNotNull();
|
||||
// test an alias
|
||||
assertThat(list.entrySet().stream().filter(e -> e.getKey().equals("io.kestra.core.tasks.log.Log")).findFirst().orElseThrow().getValue().getIcon()).isNotNull();
|
||||
});
|
||||
assertThat(list.entrySet().stream()
|
||||
.filter(e -> e.getKey().equals(Log.class.getName()))
|
||||
.findFirst().orElseThrow().getValue().getIcon()).isNotNull();
|
||||
// test an alias
|
||||
assertThat(list.entrySet().stream()
|
||||
.filter(e -> e.getKey().equals("io.kestra.core.tasks.log.Log"))
|
||||
.findFirst().orElseThrow().getValue().getIcon()).isNotNull();
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void returnTask() throws URISyntaxException {
|
||||
Helpers.runApplicationContext((applicationContext, embeddedServer) -> {
|
||||
ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL());
|
||||
void returnTask() {
|
||||
DocumentationWithSchema doc = client.toBlocking().retrieve(
|
||||
HttpRequest.GET(PATH + "/" + Return.class.getName()),
|
||||
DocumentationWithSchema.class
|
||||
);
|
||||
|
||||
DocumentationWithSchema doc = client.toBlocking().retrieve(
|
||||
HttpRequest.GET(PATH + "/" + Return.class.getName()),
|
||||
DocumentationWithSchema.class
|
||||
);
|
||||
|
||||
assertThat(doc.getMarkdown()).contains("io.kestra.plugin.core.debug.Return");
|
||||
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);
|
||||
});
|
||||
assertThat(doc.getMarkdown()).contains("io.kestra.plugin.core.debug.Return");
|
||||
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")
|
||||
@Test
|
||||
void docs() throws URISyntaxException {
|
||||
Helpers.runApplicationContext((applicationContext, embeddedServer) -> {
|
||||
ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL());
|
||||
void docs() {
|
||||
DocumentationWithSchema doc = client.toBlocking().retrieve(
|
||||
HttpRequest.GET(PATH + "/io.kestra.plugin.templates.ExampleTask"),
|
||||
DocumentationWithSchema.class
|
||||
);
|
||||
|
||||
DocumentationWithSchema doc = client.toBlocking().retrieve(
|
||||
HttpRequest.GET(PATH + "/io.kestra.plugin.templates.ExampleTask"),
|
||||
DocumentationWithSchema.class
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
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
|
||||
void docWithAlert() throws URISyntaxException {
|
||||
Helpers.runApplicationContext((applicationContext, embeddedServer) -> {
|
||||
ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL());
|
||||
void docWithAlert() {
|
||||
DocumentationWithSchema doc = client.toBlocking().retrieve(
|
||||
HttpRequest.GET(PATH + "/io.kestra.plugin.core.state.Set"),
|
||||
DocumentationWithSchema.class
|
||||
);
|
||||
|
||||
DocumentationWithSchema doc = client.toBlocking().retrieve(
|
||||
HttpRequest.GET(PATH + "/io.kestra.plugin.core.state.Set"),
|
||||
DocumentationWithSchema.class
|
||||
);
|
||||
|
||||
assertThat(doc.getMarkdown()).contains("io.kestra.plugin.core.state.Set");
|
||||
assertThat(doc.getMarkdown()).contains("::: warning\n");
|
||||
});
|
||||
assertThat(doc.getMarkdown()).contains("io.kestra.plugin.core.state.Set");
|
||||
assertThat(doc.getMarkdown()).contains("::: warning\n");
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void taskWithBase() throws URISyntaxException {
|
||||
Helpers.runApplicationContext((applicationContext, embeddedServer) -> {
|
||||
ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL());
|
||||
void taskWithBase() {
|
||||
DocumentationWithSchema doc = client.toBlocking().retrieve(
|
||||
HttpRequest.GET(PATH + "/io.kestra.plugin.templates.ExampleTask?all=true"),
|
||||
DocumentationWithSchema.class
|
||||
);
|
||||
|
||||
DocumentationWithSchema doc = client.toBlocking().retrieve(
|
||||
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");
|
||||
|
||||
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(properties.get("id").size()).isEqualTo(4);
|
||||
assertThat(((Map<String, Object>) doc.getSchema().getOutputs().get("properties")).size()).isEqualTo(1);
|
||||
});
|
||||
assertThat(doc.getMarkdown()).contains("io.kestra.plugin.templates.ExampleTask");
|
||||
assertThat(properties.size()).isEqualTo(17);
|
||||
assertThat(properties.get("id").size()).isEqualTo(4);
|
||||
assertThat(((Map<String, Object>) doc.getSchema().getOutputs().get("properties")).size()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void flow() throws URISyntaxException {
|
||||
Helpers.runApplicationContext((applicationContext, embeddedServer) -> {
|
||||
ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL());
|
||||
Map<String, Object> doc = client.toBlocking().retrieve(
|
||||
HttpRequest.GET(PATH + "/schemas/flow"),
|
||||
Argument.mapOf(String.class, Object.class)
|
||||
);
|
||||
void flow() {
|
||||
Map<String, Object> doc = client.toBlocking().retrieve(
|
||||
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
|
||||
void template() throws URISyntaxException {
|
||||
Helpers.runApplicationContext((applicationContext, embeddedServer) -> {
|
||||
ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL());
|
||||
Map<String, Object> doc = client.toBlocking().retrieve(
|
||||
HttpRequest.GET(PATH + "/schemas/template"),
|
||||
Argument.mapOf(String.class, Object.class)
|
||||
);
|
||||
void template() {
|
||||
Map<String, Object> doc = client.toBlocking().retrieve(
|
||||
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
|
||||
void task() throws URISyntaxException {
|
||||
Helpers.runApplicationContext((applicationContext, embeddedServer) -> {
|
||||
ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL());
|
||||
Map<String, Object> doc = client.toBlocking().retrieve(
|
||||
HttpRequest.GET(PATH + "/schemas/task"),
|
||||
Argument.mapOf(String.class, Object.class)
|
||||
);
|
||||
void task() {
|
||||
Map<String, Object> doc = client.toBlocking().retrieve(
|
||||
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
|
||||
void inputs() throws URISyntaxException {
|
||||
Helpers.runApplicationContext((applicationContext, embeddedServer) -> {
|
||||
ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL());
|
||||
List<InputType> doc = client.toBlocking().retrieve(
|
||||
HttpRequest.GET(PATH + "/inputs"),
|
||||
Argument.listOf(InputType.class)
|
||||
);
|
||||
void inputs() {
|
||||
List<InputType> doc = client.toBlocking().retrieve(
|
||||
HttpRequest.GET(PATH + "/inputs"),
|
||||
Argument.listOf(InputType.class)
|
||||
);
|
||||
|
||||
assertThat(doc.size()).isEqualTo(19);
|
||||
});
|
||||
assertThat(doc.size()).isEqualTo(19);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void input() throws URISyntaxException {
|
||||
Helpers.runApplicationContext((applicationContext, embeddedServer) -> {
|
||||
ReactorHttpClient client = ReactorHttpClient.create(embeddedServer.getURL());
|
||||
DocumentationWithSchema doc = client.toBlocking().retrieve(
|
||||
HttpRequest.GET(PATH + "/inputs/STRING"),
|
||||
DocumentationWithSchema.class
|
||||
);
|
||||
void input() {
|
||||
DocumentationWithSchema doc = client.toBlocking().retrieve(
|
||||
HttpRequest.GET(PATH + "/inputs/STRING"),
|
||||
DocumentationWithSchema.class
|
||||
);
|
||||
|
||||
assertThat(doc.getSchema().getProperties().size()).isEqualTo(3);
|
||||
Map<String, Object> properties = (Map<String, Object>) doc.getSchema().getProperties().get("properties");
|
||||
assertThat(properties.size()).isEqualTo(8);
|
||||
// assertThat(((Map<String, Object>) properties.get("name")).get("$deprecated"), is(true));
|
||||
});
|
||||
assertThat(doc.getSchema().getProperties().size()).isEqualTo(3);
|
||||
Map<String, Object> properties = (Map<String, Object>) doc.getSchema().getProperties().get("properties");
|
||||
assertThat(properties.size()).isEqualTo(8);
|
||||
assertThat(((Map<String, Object>) properties.get("name")).get("$deprecated")).isEqualTo(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
package io.kestra.webserver.filter;
|
||||
|
||||
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.HttpStatus;
|
||||
import io.micronaut.http.MutableHttpResponse;
|
||||
import io.micronaut.http.client.annotation.Client;
|
||||
import io.micronaut.http.client.exceptions.HttpClientResponseException;
|
||||
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 org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
@KestraTest
|
||||
@Property(name = "kestra.server.basic-auth.enabled", value = "true")
|
||||
class AuthenticationFilterTest {
|
||||
@Inject
|
||||
@Client("/")
|
||||
@@ -25,10 +26,11 @@ class AuthenticationFilterTest {
|
||||
@Inject
|
||||
private BasicAuthService.BasicAuthConfiguration basicAuthConfiguration;
|
||||
|
||||
@Inject
|
||||
private AuthenticationFilter filter;
|
||||
|
||||
@Test
|
||||
void testUnauthorized() {
|
||||
assertThrows(HttpClientResponseException.class, () -> client.toBlocking().exchange("/api/v1/configs"));
|
||||
|
||||
assertThrows(HttpClientResponseException.class, () -> client.toBlocking()
|
||||
.exchange(HttpRequest.GET("/api/v1/configs").basicAuth("anonymous", "hacker")));
|
||||
}
|
||||
@@ -57,4 +59,35 @@ class AuthenticationFilterTest {
|
||||
|
||||
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 io.kestra.core.models.Setting;
|
||||
import io.kestra.core.queues.QueueException;
|
||||
import io.kestra.core.repositories.SettingRepositoryInterface;
|
||||
import io.kestra.core.serializers.JacksonMapper;
|
||||
import io.kestra.core.services.InstanceService;
|
||||
@@ -41,7 +40,7 @@ class BasicAuthServiceTest {
|
||||
post(urlEqualTo("/v1/reports/events"))
|
||||
.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);
|
||||
basicAuthConfiguration = ctx.getBean(BasicAuthService.BasicAuthConfiguration.class);
|
||||
@@ -57,16 +56,14 @@ class BasicAuthServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void initFromYamlConfig() throws TimeoutException, QueueException {
|
||||
assertThat(basicAuthService.isEnabled()).isTrue();
|
||||
|
||||
void initFromYamlConfig() throws TimeoutException {
|
||||
assertConfigurationMatchesApplicationYaml();
|
||||
|
||||
awaitOssAuthEventApiCall("admin@kestra.io");
|
||||
}
|
||||
|
||||
@Test
|
||||
void secure() throws TimeoutException, QueueException {
|
||||
void secure() throws TimeoutException {
|
||||
IllegalArgumentException illegalArgumentException = Assertions.assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> basicAuthService.save(basicAuthConfiguration.withUsernamePassword("not-an-email", "password"))
|
||||
@@ -80,24 +77,6 @@ class BasicAuthServiceTest {
|
||||
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() {
|
||||
BasicAuthService.SaltedBasicAuthConfiguration actualConfiguration = basicAuthService.configuration();
|
||||
BasicAuthService.SaltedBasicAuthConfiguration applicationYamlConfiguration = new BasicAuthService.SaltedBasicAuthConfiguration(
|
||||
|
||||
@@ -47,7 +47,6 @@ kestra:
|
||||
access-log:
|
||||
enabled: false
|
||||
basic-auth:
|
||||
enabled: false
|
||||
username: admin@kestra.io
|
||||
password: kestra
|
||||
open-urls:
|
||||
|
||||
Reference in New Issue
Block a user