Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.kestra.core.models.QueryFilter;
import io.kestra.core.models.collectors.ExecutionUsage;
import io.kestra.core.models.collectors.FlowUsage;
import io.kestra.core.plugins.PluginRegistry;
Expand Down Expand Up @@ -157,7 +156,7 @@ public ApiUsage getUsages() {
public HttpResponse<Void> createBasicAuth(
@RequestBody @Body BasicAuthCredentials basicAuthCredentials
) {
basicAuthService.save(basicAuthCredentials.getUid(), new BasicAuthService.BasicAuthConfiguration(basicAuthCredentials.getUsername(), basicAuthCredentials.getPassword()));
basicAuthService.createBasicAuthCredentials(basicAuthCredentials);

return HttpResponse.noContent();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io.kestra.core.serializers.JacksonMapper;
import io.kestra.core.services.InstanceService;
import io.kestra.core.utils.AuthUtils;
import io.kestra.webserver.controllers.api.MiscController;
import io.kestra.webserver.models.events.OssAuthEvent;
import io.micronaut.context.annotation.ConfigurationInject;
import io.micronaut.context.annotation.ConfigurationProperties;
Expand Down Expand Up @@ -70,6 +71,13 @@ protected void init() {
}
}

public void createBasicAuthCredentials(MiscController.BasicAuthCredentials basicAuthCredentials){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe make a pojo for this MiscController.BasicAuthCredentials

save(
basicAuthCredentials.getUid(),
basicAuthConfiguration.updateWithUsernamePassword(basicAuthCredentials.getUsername(), basicAuthCredentials.getPassword())
);
}

public void save(BasicAuthConfiguration basicAuthConfiguration) {
save(null, basicAuthConfiguration);
}
Expand Down Expand Up @@ -195,7 +203,7 @@ public BasicAuthConfiguration(BasicAuthConfiguration basicAuthConfiguration) {
}

@VisibleForTesting
BasicAuthConfiguration withUsernamePassword(String username, String password) {
BasicAuthConfiguration updateWithUsernamePassword(String username, String password) {
return new BasicAuthConfiguration(
username,
password,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
package io.kestra.webserver.controllers.api;

import static io.kestra.webserver.services.BasicAuthService.BASIC_AUTH_ERROR_CONFIG;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

import io.kestra.core.junit.annotations.KestraTest;
import io.kestra.core.models.Setting;
import io.kestra.core.models.flows.FlowWithSource;
import io.kestra.core.repositories.FlowRepositoryInterface;
import io.kestra.core.repositories.SettingRepositoryInterface;
import io.kestra.core.utils.IdUtils;
import io.kestra.webserver.controllers.api.MiscController.BasicAuthCredentials;
import io.kestra.webserver.services.BasicAuthService;
import io.kestra.webserver.services.BasicAuthService.BasicAuthConfiguration;
import io.micronaut.context.annotation.Property;
import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.http.hateoas.JsonError;
import io.micronaut.reactor.http.client.ReactorHttpClient;
import jakarta.inject.Inject;
import java.util.List;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.List;

import static io.kestra.webserver.services.BasicAuthService.BASIC_AUTH_ERROR_CONFIG;
import static io.micronaut.http.HttpRequest.GET;
import static io.micronaut.http.HttpRequest.POST;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertThrows;

@KestraTest
@Property(name = "kestra.system-flows.namespace", value = "some.system.ns")
class MiscControllerTest {
Expand All @@ -39,6 +45,9 @@ class MiscControllerTest {
@Inject
private SettingRepositoryInterface settingRepository;

@Inject
private FlowRepositoryInterface flowRepository;

@Test
void ping() {
var response = client.toBlocking().retrieve("/ping", String.class);
Expand All @@ -59,7 +68,7 @@ void getConfiguration() {

@Test
void getEmptyValidationErrors() {
List<String> response = client.toBlocking().retrieve(HttpRequest.GET("/api/v1/basicAuthValidationErrors"), Argument.LIST_OF_STRING);
List<String> response = client.toBlocking().retrieve(GET("/api/v1/basicAuthValidationErrors"), Argument.LIST_OF_STRING);

assertThat(response).isNotNull();
}
Expand All @@ -68,7 +77,7 @@ void getEmptyValidationErrors() {
void getValidationErrors() {
settingRepository.save(Setting.builder().key(BASIC_AUTH_ERROR_CONFIG).value(List.of("error1", "error2")).build());
try {
List<String> response = client.toBlocking().retrieve(HttpRequest.GET("/api/v1/basicAuthValidationErrors"), Argument.LIST_OF_STRING);
List<String> response = client.toBlocking().retrieve(GET("/api/v1/basicAuthValidationErrors"), Argument.LIST_OF_STRING);

assertThat(response).containsExactly("error1", "error2");
} finally {
Expand All @@ -92,30 +101,84 @@ void saveInvalidBasicAuthConfig(){

@Test
void basicAuth() {
Assertions.assertDoesNotThrow(() -> client.toBlocking().retrieve("/api/v1/configs", MiscController.Configuration.class));
assertThatCode(() -> client.toBlocking().retrieve("/api/v1/configs", MiscController.Configuration.class)).doesNotThrowAnyException();

String uid = "someUid";
String username = "[email protected]";
String password = "myPassword1";
client.toBlocking().exchange(HttpRequest.POST("/api/v1/main/basicAuth", new MiscController.BasicAuthCredentials(uid, username, password)));
try {
assertThrows(
HttpClientResponseException.class,
assertThatThrownBy(
() -> client.toBlocking().retrieve("/api/v1/main/dashboards", MiscController.Configuration.class)
);
assertThrows(
HttpClientResponseException.class,
)
.as("expect 401 for unauthenticated GET /api/v1/main/dashboards")
.isInstanceOfSatisfying(HttpClientResponseException.class, ex ->
assertThat((CharSequence) ex.getStatus()).isEqualTo(HttpStatus.UNAUTHORIZED)
);

assertThatThrownBy(
() -> client.toBlocking().retrieve(
HttpRequest.GET("/api/v1/main/dashboards")
GET("/api/v1/main/dashboards")
.basicAuth("[email protected]", "badPassword"),
MiscController.Configuration.class
)
);
Assertions.assertDoesNotThrow(() -> client.toBlocking().retrieve(
HttpRequest.GET("/api/v1/main/dashboards")
).as("expect 401 for GET /api/v1/main/dashboards with wrong password")
.isInstanceOfSatisfying(HttpClientResponseException.class, ex ->
assertThat((CharSequence) ex.getStatus()).isEqualTo(HttpStatus.UNAUTHORIZED)
);

assertThatCode(() -> client.toBlocking().retrieve(
GET("/api/v1/main/dashboards")
.basicAuth(username, password),
MiscController.Configuration.class)
);
).as("expect success GET /api/v1/main/dashboards with good password")
.doesNotThrowAnyException();
} finally {
basicAuthService.save(basicAuthConfiguration);
}
}

@Test
void canTriggerAWebhookWithoutBasicAuth() {
String uid = "someUid2";
String username = "[email protected]";
String password = "myPassword2";
client.toBlocking().exchange(HttpRequest.POST("/api/v1/main/basicAuth", new MiscController.BasicAuthCredentials(uid, username, password)));

try {
var namespace = "namespace1";
var flowId = "flowWithWebhook" + IdUtils.create();
var key = "1KERKzRQZSMtLdMdNI7Nkr";
var flowWithWebhook = """
id: %s
namespace: %s
tasks:
- id: out
type: io.kestra.plugin.core.debug.Return
format: "output1"
triggers:
- id: webhook_trigger
type: io.kestra.plugin.core.trigger.Webhook
key: %s
disabled: false
deleted: false
""".formatted(flowId, namespace, key);

assertThatCode(() -> client.toBlocking().retrieve(
POST("/api/v1/main/flows", flowWithWebhook)
.contentType(MediaType.APPLICATION_YAML)
.basicAuth(username, password),
FlowWithSource.class)
).as("can create a Flow with webhook when authenticated")
.doesNotThrowAnyException();

assertThatCode(() -> client.toBlocking().retrieve(POST("/api/v1/main/executions/webhook/{namespace}/{flowId}/{key}"
.replace("{namespace}", namespace)
.replace("{flowId}", flowId)
.replace("{key}", key)
, flowWithWebhook), FlowWithSource.class)
).as("can trigger this Flow webhook when not authenticated")
.doesNotThrowAnyException();
} finally {
basicAuthService.save(basicAuthConfiguration);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,5 @@
package io.kestra.webserver.services;

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.and;
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
import static com.github.tomakehurst.wiremock.client.WireMock.matchingJsonPath;
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
import static io.kestra.webserver.services.BasicAuthService.BASIC_AUTH_ERROR_CONFIG;
import static io.kestra.webserver.services.BasicAuthService.BASIC_AUTH_SETTINGS_KEY;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import io.kestra.core.exceptions.ValidationErrorException;
import io.kestra.core.junit.annotations.KestraTest;
Expand All @@ -24,21 +8,29 @@
import io.kestra.core.serializers.JacksonMapper;
import io.kestra.core.services.InstanceService;
import io.kestra.core.utils.Await;
import io.kestra.webserver.controllers.api.MiscController;
import io.kestra.webserver.models.events.Event;
import io.kestra.webserver.services.BasicAuthService.BasicAuthConfiguration;
import io.micronaut.context.env.Environment;
import jakarta.inject.Inject;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeoutException;
import java.util.stream.Stream;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeoutException;
import java.util.stream.Stream;

import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static io.kestra.webserver.services.BasicAuthService.BASIC_AUTH_ERROR_CONFIG;
import static io.kestra.webserver.services.BasicAuthService.BASIC_AUTH_SETTINGS_KEY;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

@WireMockTest(httpPort = 28181)
@KestraTest(environments = Environment.TEST)
class BasicAuthServiceTest {
Expand Down Expand Up @@ -91,6 +83,40 @@ void isBasicAuthInitialized(){
assertFalse(basicAuthService.isBasicAuthInitialized());
}

@Test
void basicAuthAPICreation_shouldNot_discardYamlConfiguration(){
// simulate starting Kestra for the first time
deleteSetting();
var defaultConfigWithoutBasicAuthCreds = new ConfigWrapper(
new BasicAuthConfiguration(null, null, "Kestra2", List.of("/api/v1/main/executions/webhook/"))
);
basicAuthService.basicAuthConfiguration = defaultConfigWithoutBasicAuthCreds.config;
basicAuthService.init();
assertFalse(basicAuthService.isBasicAuthInitialized());

/**
* simulate basic auth UI onboarding (createBasicAuth)
* {@link io.kestra.webserver.controllers.api.MiscController#createBasicAuth(MiscController.BasicAuthCredentials)}
*/
basicAuthService.createBasicAuthCredentials(
new MiscController.BasicAuthCredentials(
BASIC_AUTH_SETTINGS_KEY,
"[email protected]",
"Password1"
)
);
assertTrue(basicAuthService.isBasicAuthInitialized());

assertThat(basicAuthService.configuration())
.as("Default configured realm and openUrls should not have been discarded after creating the basic auth user")
.satisfies(configuration -> {
assertThat(configuration.getUsername()).isEqualTo("[email protected]");
assertThat(configuration.getPassword()).isNotBlank();
assertThat(configuration.getRealm()).isEqualTo("Kestra2");
assertThat(configuration.getOpenUrls()).isEqualTo(List.of("/api/v1/main/executions/webhook/"));
});
}

@Test
void initFromYamlConfig() throws TimeoutException {
basicAuthService.basicAuthConfiguration = basicAuthConfiguration;
Expand Down
1 change: 1 addition & 0 deletions webserver/src/test/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ kestra:
open-urls:
- "/ping"
- "/api/v1/executions/webhook/"
- "/api/v1/main/executions/webhook/"
liveness:
enabled: false
service:
Expand Down
Loading