feat: add some APIs to separate plugin-related Setting and ConfigMap permissions (#3142)

#### What type of PR is this?
/kind improvement
/area core
/kind api-change
/milestone 2.2.x

#### What this PR does / why we need it:
添加自定义 APIs 以分离插件对 Setting 和 ConfigMap 权限的依赖

⚠️ 此 PR 新增了 APIs,需要 Console 对其进行适配,插件查询 Setting、ConfigMap 的 APIs 需要更改。

#### Which issue(s) this PR fixes:

A part of https://github.com/halo-dev/halo/issues/3069

Fixes https://github.com/halo-dev/halo/issues/3069

#### Special notes for your reviewer:
how to test it?
- 测试 GET /plugins/{name}/setting 是否能正确获取到插件名称对应的主题的 Setting。
- 测试 GET /plugins/{name}/config 是否能正确获取到插件名称对应的主题的 ConfigMap。
- 测试 PUT /plugins/{name}/config 是否能更新插件的 ConfigMap,如果名称不匹配则抛出异常。
切换用户为其分配主题的查看权限可以有权限调用以上描述的 GET 请求的 endpoints,管理权限可以调用 PUT /plugins/{name}/config。
/cc @halo-dev/sig-halo 
#### Does this PR introduce a user-facing change?

```release-note
添加自定义 APIs 以分离插件对 Setting 和 ConfigMap 权限的依赖
```
pull/3182/head^2
guqing 2023-01-30 14:28:11 +08:00 committed by GitHub
parent 7f4b3a1330
commit 1d4f65c0cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 210 additions and 2 deletions

View File

@ -104,6 +104,23 @@ public class PluginEndpoint implements CustomEndpoint {
.content(contentBuilder().mediaType(MediaType.MULTIPART_FORM_DATA_VALUE)
.schema(schemaBuilder().implementation(InstallRequest.class))))
)
.PUT("plugins/{name}/config", this::updatePluginConfig,
builder -> builder.operationId("updatePluginConfig")
.description("Update the configMap of plugin setting.")
.tag(tag)
.parameter(parameterBuilder()
.name("name")
.in(ParameterIn.PATH)
.required(true)
.implementation(String.class)
)
.requestBody(requestBodyBuilder()
.required(true)
.content(contentBuilder().mediaType(MediaType.APPLICATION_JSON_VALUE)
.schema(schemaBuilder().implementation(ConfigMap.class))))
.response(responseBuilder()
.implementation(ConfigMap.class))
)
.PUT("plugins/{name}/reset-config", this::resetSettingConfig,
builder -> builder.operationId("ResetPluginConfig")
.description("Reset the configMap of plugin setting.")
@ -124,9 +141,88 @@ public class PluginEndpoint implements CustomEndpoint {
.response(responseBuilder().implementation(generateGenericClass(Plugin.class)));
buildParametersFromType(builder, ListRequest.class);
})
.GET("plugins/{name}/setting", this::fetchPluginSetting,
builder -> builder.operationId("fetchPluginSetting")
.description("Fetch setting of plugin.")
.tag(tag)
.parameter(parameterBuilder()
.name("name")
.in(ParameterIn.PATH)
.required(true)
.implementation(String.class)
)
.response(responseBuilder()
.implementation(Setting.class))
)
.GET("plugins/{name}/config", this::fetchPluginConfig,
builder -> builder.operationId("fetchPluginConfig")
.description("Fetch configMap of plugin by configured configMapName.")
.tag(tag)
.parameter(parameterBuilder()
.name("name")
.in(ParameterIn.PATH)
.required(true)
.implementation(String.class)
)
.response(responseBuilder()
.implementation(ConfigMap.class))
)
.build();
}
private Mono<ServerResponse> fetchPluginConfig(ServerRequest request) {
final var name = request.pathVariable("name");
return client.fetch(Plugin.class, name)
.mapNotNull(plugin -> plugin.getSpec().getConfigMapName())
.flatMap(configMapName -> client.fetch(ConfigMap.class, configMapName))
.flatMap(configMap -> ServerResponse.ok().bodyValue(configMap));
}
private Mono<ServerResponse> fetchPluginSetting(ServerRequest request) {
final var name = request.pathVariable("name");
return client.fetch(Plugin.class, name)
.mapNotNull(plugin -> plugin.getSpec().getSettingName())
.flatMap(settingName -> client.fetch(Setting.class, settingName))
.flatMap(setting -> ServerResponse.ok().bodyValue(setting));
}
private Mono<ServerResponse> updatePluginConfig(ServerRequest request) {
final var pluginName = request.pathVariable("name");
return client.fetch(Plugin.class, pluginName)
.doOnNext(plugin -> {
String configMapName = plugin.getSpec().getConfigMapName();
if (!StringUtils.hasText(configMapName)) {
throw new ServerWebInputException(
"Unable to complete the request because the plugin configMapName is blank");
}
})
.flatMap(plugin -> {
final String configMapName = plugin.getSpec().getConfigMapName();
return request.bodyToMono(ConfigMap.class)
.doOnNext(configMapToUpdate -> {
var configMapNameToUpdate = configMapToUpdate.getMetadata().getName();
if (!configMapName.equals(configMapNameToUpdate)) {
throw new ServerWebInputException(
"The name from the request body does not match the plugin "
+ "configMapName name.");
}
})
.flatMap(configMapToUpdate -> client.fetch(ConfigMap.class, configMapName)
.map(persisted -> {
configMapToUpdate.getMetadata()
.setVersion(persisted.getMetadata().getVersion());
return configMapToUpdate;
})
.switchIfEmpty(client.create(configMapToUpdate))
)
.flatMap(client::update)
.retryWhen(Retry.backoff(5, Duration.ofMillis(300))
.filter(OptimisticLockingFailureException.class::isInstance)
);
})
.flatMap(configMap -> ServerResponse.ok().bodyValue(configMap));
}
private Mono<ServerResponse> resetSettingConfig(ServerRequest request) {
String name = request.pathVariable("name");
return client.fetch(Plugin.class, name)

View File

@ -16,7 +16,7 @@ rules:
resources: [ "plugins" ]
verbs: [ "create", "patch", "update", "delete", "deletecollection" ]
- apiGroups: [ "api.console.halo.run" ]
resources: [ "plugins/upgrade", "plugins/resetconfig" ]
resources: [ "plugins/upgrade", "plugins/resetconfig", "plugins/config" ]
verbs: [ "*" ]
- nonResourceURLs: [ "/apis/api.console.halo.run/v1alpha1/plugins/*" ]
verbs: [ "create" ]
@ -37,5 +37,5 @@ rules:
resources: [ "plugins" ]
verbs: [ "get", "list" ]
- apiGroups: [ "api.console.halo.run" ]
resources: [ "plugins" ]
resources: [ "plugins", "plugins/setting", "plugins/config" ]
verbs: [ "get", "list" ]

View File

@ -4,6 +4,7 @@ import static java.util.Objects.requireNonNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
@ -38,6 +39,8 @@ import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Plugin;
import run.halo.app.core.extension.Setting;
import run.halo.app.extension.ConfigMap;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.ReactiveExtensionClient;
@ -306,6 +309,115 @@ class PluginEndpointTest {
}
@Nested
class UpdatePluginConfigTest {
WebTestClient webClient;
@BeforeEach
void setUp() {
webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint())
.build();
}
@Test
void updateWhenConfigMapNameIsNull() {
Plugin plugin = createPlugin("fake-plugin");
plugin.getSpec().setConfigMapName(null);
when(client.fetch(eq(Plugin.class), eq("fake-plugin"))).thenReturn(Mono.just(plugin));
webClient.put()
.uri("/plugins/fake-plugin/config")
.exchange()
.expectStatus().isBadRequest();
}
@Test
void updateWhenConfigMapNameNotMatch() {
Plugin plugin = createPlugin("fake-plugin");
plugin.getSpec().setConfigMapName("fake-config-map");
when(client.fetch(eq(Plugin.class), eq("fake-plugin"))).thenReturn(Mono.just(plugin));
webClient.put()
.uri("/plugins/fake-plugin/config")
.body(Mono.fromSupplier(() -> {
ConfigMap configMap = new ConfigMap();
configMap.setMetadata(new Metadata());
configMap.getMetadata().setName("not-match");
return configMap;
}), ConfigMap.class)
.exchange()
.expectStatus().isBadRequest();
}
@Test
void updateWhenConfigMapNameMatch() {
Plugin plugin = createPlugin("fake-plugin");
plugin.getSpec().setConfigMapName("fake-config-map");
when(client.fetch(eq(Plugin.class), eq("fake-plugin"))).thenReturn(Mono.just(plugin));
when(client.fetch(eq(ConfigMap.class), eq("fake-config-map"))).thenReturn(Mono.empty());
when(client.create(any(ConfigMap.class))).thenReturn(Mono.empty());
webClient.put()
.uri("/plugins/fake-plugin/config")
.body(Mono.fromSupplier(() -> {
ConfigMap configMap = new ConfigMap();
configMap.setMetadata(new Metadata());
configMap.getMetadata().setName("fake-config-map");
return configMap;
}), ConfigMap.class)
.exchange()
.expectStatus().isOk();
}
}
@Nested
class PluginConfigAndSettingFetchTest {
WebTestClient webClient;
@BeforeEach
void setUp() {
webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint())
.build();
}
@Test
void fetchSetting() {
Plugin plugin = createPlugin("fake");
plugin.getSpec().setSettingName("fake-setting");
when(client.fetch(eq(Setting.class), eq("fake-setting")))
.thenReturn(Mono.just(new Setting()));
when(client.fetch(eq(Plugin.class), eq("fake"))).thenReturn(Mono.just(plugin));
webClient.get()
.uri("/plugins/fake/setting")
.exchange()
.expectStatus().isOk();
verify(client).fetch(eq(Setting.class), eq("fake-setting"));
verify(client).fetch(eq(Plugin.class), eq("fake"));
}
@Test
void fetchConfig() {
Plugin plugin = createPlugin("fake");
plugin.getSpec().setConfigMapName("fake-config");
when(client.fetch(eq(ConfigMap.class), eq("fake-config")))
.thenReturn(Mono.just(new ConfigMap()));
when(client.fetch(eq(Plugin.class), eq("fake"))).thenReturn(Mono.just(plugin));
webClient.get()
.uri("/plugins/fake/config")
.exchange()
.expectStatus().isOk();
verify(client).fetch(eq(ConfigMap.class), eq("fake-config"));
verify(client).fetch(eq(Plugin.class), eq("fake"));
}
}
Plugin createPlugin(String name) {
return createPlugin(name, "fake display name", "fake description", null);
}