Skip to content

Commit 19871a7

Browse files
authored
[Feature][Zeta] Support basic authentication for web ui (#9171)
1 parent 9da2d17 commit 19871a7

File tree

10 files changed

+517
-2
lines changed

10 files changed

+517
-2
lines changed

Diff for: config/seatunnel.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,7 @@ seatunnel:
4444
enable-http: true
4545
port: 8080
4646
enable-dynamic-port: false
47+
# Uncomment the following lines to enable basic authentication for web UI
48+
# enable-basic-auth: true
49+
# basic-auth-username: admin
50+
# basic-auth-password: admin

Diff for: docs/en/seatunnel-engine/security.md

+21
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
# Security
22

3+
## Basic Authentication
4+
5+
You can secure your Web UI by enabling basic authentication. This will require users to enter a username and password when accessing the web interface.
6+
7+
| Parameter Name | Required | Description |
8+
|----------------|----------|-------------|
9+
| `enable-basic-auth` | No | Whether to enable basic authentication, default is `false` |
10+
| `basic-auth-username` | No | The username for basic authentication, default is `admin` |
11+
| `basic-auth-password` | No | The password for basic authentication, default is `admin` |
12+
13+
```yaml
14+
seatunnel:
15+
engine:
16+
http:
17+
enable-http: true
18+
port: 8080
19+
enable-basic-auth: true
20+
basic-auth-username: "your_username"
21+
basic-auth-password: "your_password"
22+
```
23+
324
## HTTPS Configuration
425
526
You can secure your REST-API-V2 service by enabling HTTPS. Both HTTP and HTTPS can be enabled simultaneously, or only one of them can be enabled.

Diff for: docs/zh/seatunnel-engine/security.md

+21
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,27 @@ sidebar_position: 16
44

55
# Security
66

7+
## Basic 认证
8+
9+
您可以通过开启 Basic 认证来保护您的 Web UI。这将要求用户在访问 Web 界面时输入用户名和密码。
10+
11+
| 参数名称 | 是否必填 | 参数描述 |
12+
|--------|---------|--------|
13+
| `enable-basic-auth` || 是否开启Basic 认证,默认为 `false` |
14+
| `basic-auth-username` || Basic 认证的用户名,默认为 `admin` |
15+
| `basic-auth-password` || Basic 认证的密码,默认为 `admin` |
16+
17+
```yaml
18+
seatunnel:
19+
engine:
20+
http:
21+
enable-http: true
22+
port: 8080
23+
enable-basic-auth: true
24+
basic-auth-username: "your_username"
25+
basic-auth-password: "your_password"
26+
```
27+
728
## HTTPS 配置
829
930
您可以通过开启 HTTPS 来保护您的 API 服务。HTTP 和 HTTPS 可同时开启,也可以只开启其中一个。
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.seatunnel.engine.e2e;
19+
20+
import org.apache.seatunnel.engine.server.rest.RestConstant;
21+
22+
import org.awaitility.Awaitility;
23+
import org.junit.jupiter.api.AfterEach;
24+
import org.junit.jupiter.api.BeforeEach;
25+
import org.junit.jupiter.api.Test;
26+
import org.testcontainers.containers.GenericContainer;
27+
28+
import io.restassured.http.ContentType;
29+
import io.restassured.response.Response;
30+
31+
import java.io.IOException;
32+
import java.util.Base64;
33+
import java.util.concurrent.TimeUnit;
34+
35+
import static io.restassured.RestAssured.given;
36+
import static org.apache.seatunnel.e2e.common.util.ContainerUtil.PROJECT_ROOT_PATH;
37+
import static org.hamcrest.Matchers.containsString;
38+
import static org.hamcrest.Matchers.notNullValue;
39+
40+
/** Integration test for basic authentication in SeaTunnel Engine. */
41+
public class BasicAuthenticationIT extends SeaTunnelEngineContainer {
42+
43+
private static final String HTTP = "http://";
44+
private static final String COLON = ":";
45+
private static final String USERNAME = "testuser";
46+
private static final String PASSWORD = "testpassword";
47+
private static final String BASIC_AUTH_HEADER = "Authorization";
48+
private static final String BASIC_AUTH_PREFIX = "Basic ";
49+
50+
@Override
51+
@BeforeEach
52+
public void startUp() throws Exception {
53+
// Create server with basic authentication enabled
54+
55+
server = createSeaTunnelContainerWithBasicAuth();
56+
// Wait for server to be ready
57+
Awaitility.await()
58+
.atMost(2, TimeUnit.MINUTES)
59+
.until(
60+
() -> {
61+
try {
62+
// Try to access with correct credentials
63+
String credentials = USERNAME + ":" + PASSWORD;
64+
String encodedCredentials =
65+
Base64.getEncoder().encodeToString(credentials.getBytes());
66+
67+
given().header(
68+
BASIC_AUTH_HEADER,
69+
BASIC_AUTH_PREFIX + encodedCredentials)
70+
.get(
71+
HTTP
72+
+ server.getHost()
73+
+ COLON
74+
+ server.getMappedPort(8080)
75+
+ "/")
76+
.then()
77+
.statusCode(200);
78+
return true;
79+
} catch (Exception e) {
80+
return false;
81+
}
82+
});
83+
}
84+
85+
@Override
86+
@AfterEach
87+
public void tearDown() throws Exception {
88+
super.tearDown();
89+
}
90+
91+
/**
92+
* Test that accessing the web UI without authentication credentials returns 401 Unauthorized.
93+
*/
94+
@Test
95+
public void testAccessWithoutCredentials() {
96+
given().get(HTTP + server.getHost() + COLON + server.getMappedPort(8080) + "/")
97+
.then()
98+
.statusCode(401);
99+
}
100+
101+
/** Test that accessing the web UI with incorrect credentials returns 401 Unauthorized. */
102+
@Test
103+
public void testAccessWithIncorrectCredentials() {
104+
String credentials = "wronguser:wrongpassword";
105+
String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes());
106+
107+
given().header(BASIC_AUTH_HEADER, BASIC_AUTH_PREFIX + encodedCredentials)
108+
.get(HTTP + server.getHost() + COLON + server.getMappedPort(8080) + "/")
109+
.then()
110+
.statusCode(401);
111+
}
112+
113+
/** Test that accessing the web UI with correct credentials returns 200 OK. */
114+
@Test
115+
public void testAccessWithCorrectCredentials() {
116+
String credentials = USERNAME + ":" + PASSWORD;
117+
String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes());
118+
119+
given().header(BASIC_AUTH_HEADER, BASIC_AUTH_PREFIX + encodedCredentials)
120+
.get(HTTP + server.getHost() + COLON + server.getMappedPort(8080) + "/")
121+
.then()
122+
.statusCode(200)
123+
.contentType(containsString("text/html"))
124+
.body(containsString("<title>Seatunnel Engine UI</title>"));
125+
}
126+
127+
/** Test that accessing the REST API with correct credentials returns 200 OK. */
128+
@Test
129+
public void testRestApiAccessWithCorrectCredentials() {
130+
String credentials = USERNAME + ":" + PASSWORD;
131+
String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes());
132+
133+
given().header(BASIC_AUTH_HEADER, BASIC_AUTH_PREFIX + encodedCredentials)
134+
.get(
135+
HTTP
136+
+ server.getHost()
137+
+ COLON
138+
+ server.getMappedPort(8080)
139+
+ RestConstant.REST_URL_OVERVIEW)
140+
.then()
141+
.statusCode(200)
142+
.body("projectVersion", notNullValue());
143+
}
144+
145+
/** Test that accessing the REST API with Incorrect credentials returns 200 OK. */
146+
@Test
147+
public void testRestApiAccessWithIncorrectCredentials() {
148+
String credentials = "wronguser:wrongpassword";
149+
String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes());
150+
151+
given().header(BASIC_AUTH_HEADER, BASIC_AUTH_PREFIX + encodedCredentials)
152+
.get(
153+
HTTP
154+
+ server.getHost()
155+
+ COLON
156+
+ server.getMappedPort(8080)
157+
+ RestConstant.REST_URL_OVERVIEW)
158+
.then()
159+
.statusCode(401);
160+
}
161+
162+
/** Test submitting a job via REST API with correct credentials. */
163+
@Test
164+
public void testSubmitJobWithCorrectCredentials() {
165+
String credentials = USERNAME + ":" + PASSWORD;
166+
String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes());
167+
168+
// Simple batch job configuration
169+
String jobConfig =
170+
"{\n"
171+
+ " \"env\": {\n"
172+
+ " \"job.mode\": \"batch\"\n"
173+
+ " },\n"
174+
+ " \"source\": [\n"
175+
+ " {\n"
176+
+ " \"plugin_name\": \"FakeSource\",\n"
177+
+ " \"plugin_output\": \"fake\",\n"
178+
+ " \"row.num\": 100,\n"
179+
+ " \"schema\": {\n"
180+
+ " \"fields\": {\n"
181+
+ " \"name\": \"string\",\n"
182+
+ " \"age\": \"int\",\n"
183+
+ " \"card\": \"int\"\n"
184+
+ " }\n"
185+
+ " }\n"
186+
+ " }\n"
187+
+ " ],\n"
188+
+ " \"transform\": [\n"
189+
+ " ],\n"
190+
+ " \"sink\": [\n"
191+
+ " {\n"
192+
+ " \"plugin_name\": \"InMemory\",\n"
193+
+ " \"plugin_input\": \"fake\",\n"
194+
+ " \"throw_exception\": true\n"
195+
+ " }\n"
196+
+ " ]\n"
197+
+ "}";
198+
199+
Response response =
200+
given().header(BASIC_AUTH_HEADER, BASIC_AUTH_PREFIX + encodedCredentials)
201+
.contentType(ContentType.JSON)
202+
.body(jobConfig)
203+
.post(
204+
HTTP
205+
+ server.getHost()
206+
+ COLON
207+
+ server.getMappedPort(8080)
208+
+ RestConstant.REST_URL_SUBMIT_JOB);
209+
210+
response.then().statusCode(200).body("jobId", notNullValue());
211+
}
212+
213+
/** Test submitting a job via REST API with incorrect credentials. */
214+
@Test
215+
public void testSubmitJobWithIncorrectCredentials() {
216+
String credentials = "wronguser:wrongpassword";
217+
String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes());
218+
219+
// Simple batch job configuration
220+
String jobConfig =
221+
"{\n"
222+
+ " \"env\": {\n"
223+
+ " \"job.mode\": \"BATCH\"\n"
224+
+ " },\n"
225+
+ " \"source\": {\n"
226+
+ " \"FakeSource\": {\n"
227+
+ " \"plugin_output\": \"fake\",\n"
228+
+ " \"row.num\": 100,\n"
229+
+ " \"schema\": {\n"
230+
+ " \"fields\": {\n"
231+
+ " \"id\": \"int\",\n"
232+
+ " \"name\": \"string\"\n"
233+
+ " }\n"
234+
+ " }\n"
235+
+ " }\n"
236+
+ " },\n"
237+
+ " \"sink\": {\n"
238+
+ " \"Console\": {\n"
239+
+ " \"plugin_input\": \"fake\"\n"
240+
+ " }\n"
241+
+ " }\n"
242+
+ "}";
243+
244+
given().header(BASIC_AUTH_HEADER, BASIC_AUTH_PREFIX + encodedCredentials)
245+
.contentType(ContentType.JSON)
246+
.body(jobConfig)
247+
.post(
248+
HTTP
249+
+ server.getHost()
250+
+ COLON
251+
+ server.getMappedPort(8080)
252+
+ RestConstant.REST_URL_SUBMIT_JOB)
253+
.then()
254+
.statusCode(401);
255+
}
256+
257+
/** Create a SeaTunnel container with basic authentication enabled. */
258+
private GenericContainer<?> createSeaTunnelContainerWithBasicAuth()
259+
throws IOException, InterruptedException {
260+
String configPath =
261+
PROJECT_ROOT_PATH
262+
+ "/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/resources/basic-auth/seatunnel.yaml";
263+
264+
return createSeaTunnelContainerWithFakeSourceAndInMemorySink(configPath);
265+
}
266+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to You under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
seatunnel:
19+
engine:
20+
history-job-expire-minutes: 1
21+
backup-count: 2
22+
queue-type: blockingqueue
23+
print-execution-info-interval: 10
24+
classloader-cache-mode: false
25+
slot-service:
26+
dynamic-slot: true
27+
checkpoint:
28+
interval: 300000
29+
timeout: 100000
30+
storage:
31+
type: localfile
32+
max-retained: 3
33+
plugin-config:
34+
namespace: /tmp/seatunnel/checkpoint_snapshot/
35+
http:
36+
enable-http: true
37+
port: 8080
38+
enable-dynamic-port: false
39+
enable-basic-auth: true
40+
basic-auth-username: "testuser"
41+
basic-auth-password: "testpassword"
42+
telemetry:
43+
metric:
44+
enabled: false
45+
logs:
46+
scheduled-deletion-enable: false

Diff for: seatunnel-engine/seatunnel-engine-common/src/main/java/org/apache/seatunnel/engine/common/config/YamlSeaTunnelDomConfigProcessor.java

+12
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,18 @@ private HttpConfig parseHttpConfig(Node httpNode) {
549549
.key()
550550
.equals(name)) {
551551
httpConfig.setTrustStorePassword(getTextContent(node));
552+
} else if (ServerConfigOptions.MasterServerConfigOptions.ENABLE_BASIC_AUTH
553+
.key()
554+
.equals(name)) {
555+
httpConfig.setEnableBasicAuth(getBooleanValue(getTextContent(node)));
556+
} else if (ServerConfigOptions.MasterServerConfigOptions.BASIC_AUTH_USERNAME
557+
.key()
558+
.equals(name)) {
559+
httpConfig.setBasicAuthUsername(getTextContent(node));
560+
} else if (ServerConfigOptions.MasterServerConfigOptions.BASIC_AUTH_PASSWORD
561+
.key()
562+
.equals(name)) {
563+
httpConfig.setBasicAuthPassword(getTextContent(node));
552564
} else {
553565
LOGGER.warning("Unrecognized element: " + name);
554566
}

0 commit comments

Comments
 (0)