From ca3cff277ab32872aeddf0cd5902f8a4f88f46fd Mon Sep 17 00:00:00 2001 From: John Niang Date: Fri, 15 Jul 2022 14:43:09 +0800 Subject: [PATCH] Create super admin initializer and run extension controllers conditionally (#2248) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind improvement /kind failing-test /area core /milestone 2.0 #### What this PR does / why we need it: Before this PR, our unit tests were flaky to run. After my inspection, I found that extension controllers will run asynchronously at every unit test that is annotated `@SpringBootTest` annotation. Please see the log of failing test: ```java ExtensionConfigurationTest > shouldReturnNotFoundWhenSchemeNotRegistered() FAILED java.lang.AssertionError at ExtensionConfigurationTest.java:72 ``` So this PR makes Halo create super admin initializer and run extension controllers conditionally, especially in tests. You can configure the following property to disable super admin initialization and extension controllers running: ```yaml halo: security: initializer: disabled: true extension: controller: disabled: true ``` BTW, we can configure the initial username and password for super administrator: ```yaml halo: security: initializer: super-admin-username: admin super-admin-password: P@88w0rd ``` #### Which issue(s) this PR fixes: Fixes # #### Special notes for your reviewer: #### Does this PR introduce a user-facing change? ```release-note None ``` --- .../app/config/ExtensionConfiguration.java | 75 +++++++++++-------- .../app/config/WebServerSecurityConfig.java | 13 ++++ .../controller/ControllerManager.java | 16 ++-- .../infra/properties/ExtensionProperties.java | 17 +++++ .../app/infra/properties/HaloProperties.java | 8 +- .../infra/properties/SecurityProperties.java | 21 ++++++ .../app/security/SuperAdminInitializer.java | 39 +++++++--- .../security/SuperAdminInitializerTest.java | 13 +++- src/test/resources/application.yaml | 5 ++ 9 files changed, 153 insertions(+), 54 deletions(-) create mode 100644 src/main/java/run/halo/app/infra/properties/ExtensionProperties.java create mode 100644 src/main/java/run/halo/app/infra/properties/SecurityProperties.java diff --git a/src/main/java/run/halo/app/config/ExtensionConfiguration.java b/src/main/java/run/halo/app/config/ExtensionConfiguration.java index df4bd429b..6be89d6ea 100644 --- a/src/main/java/run/halo/app/config/ExtensionConfiguration.java +++ b/src/main/java/run/halo/app/config/ExtensionConfiguration.java @@ -1,6 +1,7 @@ package run.halo.app.config; import java.util.List; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.server.RouterFunction; @@ -25,6 +26,7 @@ import run.halo.app.extension.SchemeWatcherManager; import run.halo.app.extension.SchemeWatcherManager.SchemeWatcher; import run.halo.app.extension.controller.Controller; import run.halo.app.extension.controller.ControllerBuilder; +import run.halo.app.extension.controller.ControllerManager; import run.halo.app.extension.store.ExtensionStoreClient; import run.halo.app.plugin.HaloPluginManager; import run.halo.app.plugin.resources.JsBundleRuleProvider; @@ -54,36 +56,49 @@ public class ExtensionConfiguration { return new DefaultSchemeWatcherManager(); } - @Bean - Controller userController(ExtensionClient client) { - return new ControllerBuilder("user-controller", client) - .reconciler(new UserReconciler(client)) - .extension(new User()) - .build(); + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(name = "halo.extension.controller.disabled", + havingValue = "false", + matchIfMissing = true) + static class ExtensionControllerConfiguration { + + @Bean + ControllerManager controllerManager() { + return new ControllerManager(); + } + + @Bean + Controller userController(ExtensionClient client) { + return new ControllerBuilder("user-controller", client) + .reconciler(new UserReconciler(client)) + .extension(new User()) + .build(); + } + + @Bean + Controller roleController(ExtensionClient client, RoleService roleService) { + return new ControllerBuilder("role-controller", client) + .reconciler(new RoleReconciler(client, roleService)) + .extension(new Role()) + .build(); + } + + @Bean + Controller roleBindingController(ExtensionClient client) { + return new ControllerBuilder("role-binding-controller", client) + .reconciler(new RoleBindingReconciler(client)) + .extension(new RoleBinding()) + .build(); + } + + @Bean + Controller pluginController(ExtensionClient client, HaloPluginManager haloPluginManager, + JsBundleRuleProvider jsBundleRule) { + return new ControllerBuilder("plugin-controller", client) + .reconciler(new PluginReconciler(client, haloPluginManager, jsBundleRule)) + .extension(new Plugin()) + .build(); + } } - @Bean - Controller roleController(ExtensionClient client, RoleService roleService) { - return new ControllerBuilder("role-controller", client) - .reconciler(new RoleReconciler(client, roleService)) - .extension(new Role()) - .build(); - } - - @Bean - Controller roleBindingController(ExtensionClient client) { - return new ControllerBuilder("role-binding-controller", client) - .reconciler(new RoleBindingReconciler(client)) - .extension(new RoleBinding()) - .build(); - } - - @Bean - Controller pluginController(ExtensionClient client, HaloPluginManager haloPluginManager, - JsBundleRuleProvider jsBundleRule) { - return new ControllerBuilder("plugin-controller", client) - .reconciler(new PluginReconciler(client, haloPluginManager, jsBundleRule)) - .extension(new Plugin()) - .build(); - } } diff --git a/src/main/java/run/halo/app/config/WebServerSecurityConfig.java b/src/main/java/run/halo/app/config/WebServerSecurityConfig.java index 0ec8a8223..025d7b15e 100644 --- a/src/main/java/run/halo/app/config/WebServerSecurityConfig.java +++ b/src/main/java/run/halo/app/config/WebServerSecurityConfig.java @@ -9,6 +9,7 @@ import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.jwk.source.ImmutableJWKSet; import java.util.Arrays; import java.util.List; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -33,8 +34,11 @@ import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; import org.springframework.web.reactive.function.server.ServerResponse; import run.halo.app.core.extension.service.RoleService; import run.halo.app.core.extension.service.UserService; +import run.halo.app.extension.ExtensionClient; +import run.halo.app.infra.properties.HaloProperties; import run.halo.app.infra.properties.JwtProperties; import run.halo.app.security.DefaultUserDetailService; +import run.halo.app.security.SuperAdminInitializer; import run.halo.app.security.authentication.jwt.LoginAuthenticationFilter; import run.halo.app.security.authentication.jwt.LoginAuthenticationManager; import run.halo.app.security.authorization.RequestInfoAuthorizationManager; @@ -143,4 +147,13 @@ public class WebServerSecurityConfig { return new NimbusJwtEncoder(jwks); } + @Bean + @ConditionalOnProperty(name = "halo.security.initializer.disabled", + havingValue = "false", + matchIfMissing = true) + SuperAdminInitializer superAdminInitializer(ExtensionClient client, HaloProperties halo) { + return new SuperAdminInitializer(client, + passwordEncoder(), + halo.getSecurity().getInitializer()); + } } diff --git a/src/main/java/run/halo/app/extension/controller/ControllerManager.java b/src/main/java/run/halo/app/extension/controller/ControllerManager.java index 776cd4600..ee6f42523 100644 --- a/src/main/java/run/halo/app/extension/controller/ControllerManager.java +++ b/src/main/java/run/halo/app/extension/controller/ControllerManager.java @@ -1,22 +1,18 @@ package run.halo.app.extension.controller; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.DisposableBean; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; -import org.springframework.stereotype.Component; @Slf4j -@Component public class ControllerManager implements ApplicationListener, - DisposableBean { + ApplicationContextAware, DisposableBean { - private final ApplicationContext applicationContext; - - public ControllerManager(ApplicationContext applicationContext) { - this.applicationContext = applicationContext; - } + private ApplicationContext applicationContext; @Override public void onApplicationEvent(ApplicationReadyEvent event) { @@ -39,4 +35,8 @@ public class ControllerManager implements ApplicationListener initialExtensionLocations; + private Set initialExtensionLocations = new HashSet<>(); + + private final ExtensionProperties extension = new ExtensionProperties(); + + private final SecurityProperties security = new SecurityProperties(); + } diff --git a/src/main/java/run/halo/app/infra/properties/SecurityProperties.java b/src/main/java/run/halo/app/infra/properties/SecurityProperties.java new file mode 100644 index 000000000..04828b82d --- /dev/null +++ b/src/main/java/run/halo/app/infra/properties/SecurityProperties.java @@ -0,0 +1,21 @@ +package run.halo.app.infra.properties; + +import lombok.Data; + +@Data +public class SecurityProperties { + + private final Initializer initializer = new Initializer(); + + @Data + public static class Initializer { + + private boolean disabled; + + private String superAdminUsername = "admin"; + + private String superAdminPassword; + + } + +} diff --git a/src/main/java/run/halo/app/security/SuperAdminInitializer.java b/src/main/java/run/halo/app/security/SuperAdminInitializer.java index fd462a65a..80043b5d8 100644 --- a/src/main/java/run/halo/app/security/SuperAdminInitializer.java +++ b/src/main/java/run/halo/app/security/SuperAdminInitializer.java @@ -9,7 +9,7 @@ import org.apache.commons.lang3.RandomStringUtils; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationListener; import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; import run.halo.app.core.extension.Role; import run.halo.app.core.extension.Role.PolicyRule; import run.halo.app.core.extension.RoleBinding; @@ -19,22 +19,28 @@ import run.halo.app.core.extension.User; import run.halo.app.core.extension.User.UserSpec; import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.Metadata; +import run.halo.app.infra.properties.SecurityProperties.Initializer; @Slf4j -@Component public class SuperAdminInitializer implements ApplicationListener { + private static final String SUPER_ROLE_NAME = "super-role"; + private final ExtensionClient client; private final PasswordEncoder passwordEncoder; - public SuperAdminInitializer(ExtensionClient client, PasswordEncoder passwordEncoder) { + private final Initializer initializer; + + public SuperAdminInitializer(ExtensionClient client, PasswordEncoder passwordEncoder, + Initializer initializer) { this.client = client; this.passwordEncoder = passwordEncoder; + this.initializer = initializer; } @Override public void onApplicationEvent(ApplicationReadyEvent event) { - client.fetch(User.class, "admin").ifPresentOrElse(user -> { + client.fetch(User.class, initializer.getSuperAdminUsername()).ifPresentOrElse(user -> { // do nothing if admin has been initialized }, () -> { var admin = createAdmin(); @@ -48,7 +54,9 @@ public class SuperAdminInitializer implements ApplicationListener annotations = new HashMap<>(); annotations.put(Role.UI_PERMISSIONS_ANNO, "[\"*\"]"); metadata.setAnnotations(annotations); @@ -89,7 +97,7 @@ public class SuperAdminInitializer implements ApplicationListener { if (extension instanceof User user) { - return "admin".equals(user.getMetadata().getName()); + return "fake-admin".equals(user.getMetadata().getName()) + && encoder.matches("fake-password", user.getSpec().getPassword()); } return false; })); @@ -44,7 +51,7 @@ class SuperAdminInitializerTest { })); verify(client, times(1)).create(argThat(extension -> { if (extension instanceof RoleBinding roleBinding) { - return "admin-super-role-binding".equals(roleBinding.getMetadata().getName()); + return "fake-admin-super-role-binding".equals(roleBinding.getMetadata().getName()); } return false; })); diff --git a/src/test/resources/application.yaml b/src/test/resources/application.yaml index 2544cacd0..84350717c 100644 --- a/src/test/resources/application.yaml +++ b/src/test/resources/application.yaml @@ -19,10 +19,15 @@ spring: halo: security: + initializer: + disabled: true oauth2: jwt: public-key-location: classpath:app.pub private-key-location: classpath:app.key + extension: + controller: + disabled: true springdoc: api-docs: