Skip to content

Commit b6ad972

Browse files
committed
Merge branch 'main' into improve-component-config
2 parents 4d09adf + 8660eab commit b6ad972

File tree

14 files changed

+246
-35
lines changed

14 files changed

+246
-35
lines changed

checkstyle.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
<property name="ignorePattern" value="^package.*|^import.*|a href|http://|https://|ftp://|//.*"/>
3737
</module>
3838
<module name="TreeWalker">
39+
<property name="javaParseExceptionSeverity" value="info"/>
40+
<property name="skipFileOnJavaParseException" value="true"/>
3941
<module name="SuppressionCommentFilter">
4042
<property name="offCommentFormat" value="CHECKSTYLE.OFF\: ([\w\|]+)"/>
4143
<property name="onCommentFormat" value="CHECKSTYLE.ON"/>

cloudnet-rest-module/build.gradle.kts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,31 @@ dependencies {
5757
compileOnly("eu.cloudnetservice.cloudnet:bridge:4.0.0-RC11-SNAPSHOT")
5858
}
5959

60+
tasks.withType<Test> {
61+
jvmArgs("--enable-preview")
62+
}
63+
64+
tasks.withType<JavaCompile> {
65+
sourceCompatibility = JavaVersion.VERSION_22.toString()
66+
targetCompatibility = JavaVersion.VERSION_22.toString()
67+
68+
options.compilerArgs.add("-Xlint:-preview")
69+
options.compilerArgs.add("--enable-preview")
70+
}
71+
72+
tasks.withType<Javadoc> {
73+
val options = options as? StandardJavadocDocletOptions ?: return@withType
74+
options.addStringOption("-release", "22")
75+
options.addBooleanOption("-enable-preview", true)
76+
}
77+
78+
extensions.configure<JavaPluginExtension> {
79+
toolchain {
80+
vendor = JvmVendorSpec.AZUL
81+
languageVersion = JavaLanguageVersion.of(22)
82+
}
83+
}
84+
6085
tasks.withType<Jar> {
6186
archiveFileName.set("cloudnet-rest.jar")
6287
}

cloudnet-rest-module/src/main/java/eu/cloudnetservice/ext/modules/rest/CloudNetRestModule.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ public void loadLanguageFile() {
6969
@ModuleTask(order = 127, lifecycle = ModuleLifeCycle.STARTED)
7070
public void initHttpServer(@Named("module") @NonNull InjectionLayer<?> injectionLayer) {
7171
var restConfig = this.readConfig(RestConfiguration.class, () -> RestConfiguration.DEFAULT, DocumentFactory.json());
72+
restConfig.validate();
73+
RestConfiguration.setInstance(restConfig);
7274

7375
// construct the http server component
7476
var componentFactory = HttpComponentFactoryLoader.getFirstComponentFactory(HttpServer.class);

cloudnet-rest-module/src/main/java/eu/cloudnetservice/ext/modules/rest/RestCommand.java

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,6 @@
1616

1717
package eu.cloudnetservice.ext.modules.rest;
1818

19-
import cloud.commandframework.annotations.Argument;
20-
import cloud.commandframework.annotations.CommandMethod;
21-
import cloud.commandframework.annotations.CommandPermission;
22-
import cloud.commandframework.annotations.Regex;
23-
import cloud.commandframework.annotations.parsers.Parser;
24-
import cloud.commandframework.context.CommandContext;
2519
import eu.cloudnetservice.common.language.I18n;
2620
import eu.cloudnetservice.ext.modules.rest.auth.DefaultRestUser;
2721
import eu.cloudnetservice.ext.rest.api.auth.AuthProvider;
@@ -36,13 +30,18 @@
3630
import jakarta.inject.Singleton;
3731
import java.nio.charset.StandardCharsets;
3832
import java.time.OffsetDateTime;
39-
import java.util.Queue;
4033
import java.util.Set;
4134
import java.util.function.Consumer;
4235
import lombok.NonNull;
36+
import org.incendo.cloud.annotations.Argument;
37+
import org.incendo.cloud.annotations.Command;
38+
import org.incendo.cloud.annotations.Permission;
39+
import org.incendo.cloud.annotations.Regex;
40+
import org.incendo.cloud.annotations.parser.Parser;
41+
import org.incendo.cloud.context.CommandInput;
4342

4443
@Singleton
45-
@CommandPermission("cloudnet.command.rest")
44+
@Permission("cloudnet.command.rest")
4645
@Description("module-rest-command-description")
4746
public final class RestCommand {
4847

@@ -56,8 +55,8 @@ public RestCommand(@NonNull RestUserManagement restUserManagement) {
5655
}
5756

5857
@Parser
59-
public @NonNull DefaultRestUser defaultRestUserParser(@NonNull CommandContext<?> $, @NonNull Queue<String> input) {
60-
var username = input.remove();
58+
public @NonNull DefaultRestUser defaultRestUserParser(@NonNull CommandInput input) {
59+
var username = input.readString();
6160
var user = this.restUserManagement.restUserByUsername(username);
6261
if (user == null) {
6362
throw new ArgumentNotAvailableException(I18n.trans("module-rest-user-not-found", username));
@@ -74,8 +73,8 @@ public RestCommand(@NonNull RestUserManagement restUserManagement) {
7473
}
7574

7675
@Parser(name = "restUserScope")
77-
public @NonNull String restUserScopeParser(@NonNull CommandContext<?> $, @NonNull Queue<String> input) {
78-
var scope = input.remove();
76+
public @NonNull String restUserScopeParser(@NonNull CommandInput input) {
77+
var scope = input.readString();
7978
if (RestUser.SCOPE_NAMING_PATTERN.matcher(scope).matches()) {
8079
return scope;
8180
}
@@ -86,7 +85,7 @@ public RestCommand(@NonNull RestUserManagement restUserManagement) {
8685
scope));
8786
}
8887

89-
@CommandMethod("rest user create <username> <password>")
88+
@Command("rest user create <username> <password>")
9089
public void createRestUser(
9190
@NonNull CommandSource source,
9291
@Argument("username") @Regex(DefaultRestUser.USER_NAMING_REGEX) @NonNull String username,
@@ -109,13 +108,13 @@ public void createRestUser(
109108
source.sendMessage(I18n.trans("module-rest-user-create-successful", username));
110109
}
111110

112-
@CommandMethod("rest user delete <username>")
111+
@Command("rest user delete <username>")
113112
public void deleteRestUser(@NonNull CommandSource source, @Argument("username") @NonNull DefaultRestUser restUser) {
114113
this.restUserManagement.deleteRestUser(restUser.id());
115114
source.sendMessage(I18n.trans("module-rest-user-delete-successful", restUser.username()));
116115
}
117116

118-
@CommandMethod("rest user <username>")
117+
@Command("rest user <username>")
119118
public void displayUser(@NonNull CommandSource source, @Argument("username") @NonNull DefaultRestUser restUser) {
120119
source.sendMessage("RestUser " + restUser.id() + ":" + restUser.username());
121120
source.sendMessage("Scopes:");
@@ -124,7 +123,7 @@ public void displayUser(@NonNull CommandSource source, @Argument("username") @No
124123
}
125124
}
126125

127-
@CommandMethod("rest user <username> add scope <scope>")
126+
@Command("rest user <username> add scope <scope>")
128127
public void addScope(
129128
@NonNull CommandSource source,
130129
@Argument("username") @NonNull DefaultRestUser restUser,
@@ -134,13 +133,13 @@ public void addScope(
134133
source.sendMessage(I18n.trans("module-rest-user-add-scope-successful", restUser.username(), scope));
135134
}
136135

137-
@CommandMethod("rest user <username> clear scopes")
136+
@Command("rest user <username> clear scopes")
138137
public void clearScopes(@NonNull CommandSource source, @Argument("username") @NonNull DefaultRestUser restUser) {
139138
this.updateRestUser(source, restUser, builder -> builder.scopes(Set.of()));
140139
source.sendMessage(I18n.trans("module-rest-user-clear-scopes-successful", restUser.username()));
141140
}
142141

143-
@CommandMethod("rest user <username> remove scope <scope>")
142+
@Command("rest user <username> remove scope <scope>")
144143
public void removeScope(
145144
@NonNull CommandSource source,
146145
@Argument("username") @NonNull DefaultRestUser restUser,
@@ -150,7 +149,7 @@ public void removeScope(
150149
source.sendMessage(I18n.trans("module-rest-user-remove-scope-successful", restUser.username(), scope));
151150
}
152151

153-
@CommandMethod("rest user <username> set password <password>")
152+
@Command("rest user <username> set password <password>")
154153
public void setPassword(
155154
@NonNull CommandSource source,
156155
@Argument("username") @NonNull DefaultRestUser restUser,
@@ -160,7 +159,7 @@ public void setPassword(
160159
source.sendMessage(I18n.trans("module-rest-user-password-changed", restUser.username()));
161160
}
162161

163-
@CommandMethod("rest user <username> verifyPassword <password>")
162+
@Command("rest user <username> verifyPassword <password>")
164163
public void verifyPassword(
165164
@NonNull CommandSource source,
166165
@Argument("username") @NonNull DefaultRestUser restUser,

cloudnet-rest-module/src/main/java/eu/cloudnetservice/ext/modules/rest/auth/provider/CloudNetJwtAuthProvider.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@
2020
import eu.cloudnetservice.driver.inject.InjectionLayer;
2121
import eu.cloudnetservice.ext.modules.rest.CloudNetRestModule;
2222
import eu.cloudnetservice.ext.modules.rest.auth.util.KeySecurityUtil;
23+
import eu.cloudnetservice.ext.modules.rest.config.RestConfiguration;
2324
import eu.cloudnetservice.ext.rest.api.auth.AuthProvider;
2425
import eu.cloudnetservice.ext.rest.jwt.JwtAuthProvider;
2526
import java.io.IOException;
2627
import java.nio.file.Files;
2728
import java.nio.file.Path;
2829
import java.security.KeyPair;
29-
import java.time.Duration;
3030
import lombok.NonNull;
3131

3232
public final class CloudNetJwtAuthProvider extends JwtAuthProvider {
@@ -35,10 +35,12 @@ public final class CloudNetJwtAuthProvider extends JwtAuthProvider {
3535
private static final Path PUBLIC_KEY_PATH = Path.of("jwt_sign_key.pub");
3636

3737
public CloudNetJwtAuthProvider() {
38-
super("CloudNet Rest",
38+
var authConfig = RestConfiguration.get().authConfig();
39+
super(
40+
"CloudNet Rest",
3941
readOrGenerateJwtKeyPair(),
40-
Duration.ofHours(12),
41-
Duration.ofDays(3));
42+
authConfig.jwtTokenLifetime(),
43+
authConfig.jwtRefreshTokenLifetime());
4244
}
4345

4446
private static @NonNull KeyPair readOrGenerateJwtKeyPair() {

cloudnet-rest-module/src/main/java/eu/cloudnetservice/ext/modules/rest/auth/provider/CloudNetTicketAuthProvider.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,22 @@
2020
import eu.cloudnetservice.driver.inject.InjectionLayer;
2121
import eu.cloudnetservice.ext.modules.rest.CloudNetRestModule;
2222
import eu.cloudnetservice.ext.modules.rest.auth.util.KeySecurityUtil;
23+
import eu.cloudnetservice.ext.modules.rest.config.RestConfiguration;
2324
import eu.cloudnetservice.ext.rest.ticket.TicketAuthProvider;
2425
import java.io.IOException;
2526
import java.nio.file.Files;
2627
import java.nio.file.Path;
2728
import java.security.InvalidKeyException;
2829
import java.security.NoSuchAlgorithmException;
29-
import java.time.Duration;
3030
import javax.crypto.Mac;
3131
import lombok.NonNull;
3232

3333
public final class CloudNetTicketAuthProvider extends TicketAuthProvider {
3434

3535
private static final Path HMAC_KEY_PATH = Path.of("ticket_sign_key");
36-
private static final Duration TICKET_EXPIRATION_DURATION = Duration.ofSeconds(15);
3736

3837
public CloudNetTicketAuthProvider() {
39-
super(TICKET_EXPIRATION_DURATION, readOrGenenerateMAC());
38+
super(RestConfiguration.get().authConfig().ticketLifetime(), readOrGenenerateMAC());
4039
}
4140

4241
private static @NonNull Mac readOrGenenerateMAC() {
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2019-2024 CloudNetService team & contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package eu.cloudnetservice.ext.modules.rest.config;
18+
19+
import java.time.Duration;
20+
import lombok.NonNull;
21+
22+
public record AuthConfiguration(
23+
int jwtTokenLifetimeSeconds,
24+
int jwtRefreshTokenLifetimeSeconds,
25+
int ticketLifetimeSeconds
26+
) {
27+
28+
public static final AuthConfiguration DEFAULT_CONFIGURATION = new AuthConfiguration(
29+
12 * 60 * 60, // 12h
30+
3 * 24 * 60 * 60, // 3d
31+
15 // 15s
32+
);
33+
34+
public void validate() {
35+
if (this.jwtTokenLifetimeSeconds <= 0
36+
|| this.jwtRefreshTokenLifetimeSeconds <= 0
37+
|| this.ticketLifetimeSeconds <= 0) {
38+
throw new IllegalStateException("invalid authentication configuration: one lifetime is less or equal to zero");
39+
}
40+
}
41+
42+
public @NonNull Duration jwtTokenLifetime() {
43+
return Duration.ofSeconds(this.jwtTokenLifetimeSeconds);
44+
}
45+
46+
public @NonNull Duration jwtRefreshTokenLifetime() {
47+
return Duration.ofSeconds(this.jwtRefreshTokenLifetimeSeconds);
48+
}
49+
50+
public @NonNull Duration ticketLifetime() {
51+
return Duration.ofSeconds(this.ticketLifetimeSeconds);
52+
}
53+
}

cloudnet-rest-module/src/main/java/eu/cloudnetservice/ext/modules/rest/config/RestConfiguration.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package eu.cloudnetservice.ext.modules.rest.config;
1818

19+
import com.google.common.base.Preconditions;
1920
import eu.cloudnetservice.ext.rest.api.config.ComponentConfig;
2021
import eu.cloudnetservice.ext.rest.api.config.CorsConfig;
2122
import eu.cloudnetservice.ext.rest.api.config.HttpProxyMode;
@@ -31,8 +32,9 @@
3132
public record RestConfiguration(
3233
int maxContentLength,
3334
boolean disableNativeTransport,
34-
@NonNull CorsConfig cors,
35+
@NonNull CorsConfig corsConfig,
3536
@NonNull HttpProxyMode proxyMode,
37+
@NonNull AuthConfiguration authConfig,
3638
@NonNull List<HostAndPort> httpListeners,
3739
@NonNull List<ConnectionInfoResolverConfiguration> connectionInfoResolver,
3840
@Nullable SslConfiguration sslConfiguration
@@ -47,13 +49,29 @@ public record RestConfiguration(
4749
.allowCredentials(true)
4850
.build(),
4951
HttpProxyMode.DISABLED,
52+
AuthConfiguration.DEFAULT_CONFIGURATION,
5053
List.of(new HostAndPort("127.0.0.1", 2812)),
5154
List.of(),
5255
null);
5356

57+
private static RestConfiguration instance;
58+
59+
public static @NonNull RestConfiguration get() {
60+
Preconditions.checkState(instance != null, "rest configuration has not been initialized");
61+
return instance;
62+
}
63+
64+
public static void setInstance(@NonNull RestConfiguration config) {
65+
instance = config;
66+
}
67+
68+
public void validate() {
69+
this.authConfig.validate();
70+
}
71+
5472
public @NonNull ComponentConfig toComponentConfig() {
5573
return ComponentConfig.builder()
56-
.corsConfig(this.cors)
74+
.corsConfig(this.corsConfig)
5775
.haProxyMode(this.proxyMode)
5876
.maxContentLength(this.maxContentLength)
5977
.sslConfiguration(this.sslConfiguration)

cloudnet-rest-module/src/main/java/eu/cloudnetservice/ext/modules/rest/v3/V3HttpHandlerCluster.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818

1919
import eu.cloudnetservice.driver.document.Document;
2020
import eu.cloudnetservice.ext.modules.rest.dto.NetworkClusterNodeDto;
21+
import eu.cloudnetservice.ext.modules.rest.validation.TrueFalse;
2122
import eu.cloudnetservice.ext.rest.api.HttpMethod;
2223
import eu.cloudnetservice.ext.rest.api.HttpResponseCode;
2324
import eu.cloudnetservice.ext.rest.api.annotation.Authentication;
25+
import eu.cloudnetservice.ext.rest.api.annotation.FirstRequestQueryParam;
2426
import eu.cloudnetservice.ext.rest.api.annotation.RequestHandler;
2527
import eu.cloudnetservice.ext.rest.api.annotation.RequestPathParam;
2628
import eu.cloudnetservice.ext.rest.api.annotation.RequestTypedBody;
@@ -77,6 +79,24 @@ public V3HttpHandlerCluster(@NonNull Configuration configuration, @NonNull NodeS
7779
return this.nodeServerNotFound(node);
7880
}
7981

82+
@EnableValidation
83+
@RequestHandler(path = "/api/v3/cluster/{node}/drain", method = HttpMethod.PATCH)
84+
@Authentication(
85+
providers = "jwt",
86+
scopes = {"cloudnet_rest:cluster_write", "cloudnet_rest:cluster_node_change_draining"})
87+
public @NonNull IntoResponse<?> handleNodeDrainRequest(
88+
@NonNull @RequestPathParam("node") String node,
89+
@Valid @TrueFalse @NonNull @FirstRequestQueryParam("draining") String drainingParam
90+
) {
91+
var server = this.nodeServerProvider.node(node);
92+
if (server == null) {
93+
return this.nodeServerNotFound(node);
94+
}
95+
96+
server.drain(Boolean.parseBoolean(drainingParam));
97+
return HttpResponseCode.ACCEPTED;
98+
}
99+
80100
@RequestHandler(path = "/api/v3/cluster/{node}/command", method = HttpMethod.POST)
81101
@Authentication(providers = "jwt", scopes = {"cloudnet_rest:cluster_write", "cloudnet_rest:cluster_node_command"})
82102
public @NonNull IntoResponse<?> handleNodeCommandRequest(

0 commit comments

Comments
 (0)