Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: add system initialization check and redirect to console if not initialized #3892

Merged
merged 1 commit into from May 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
96 changes: 96 additions & 0 deletions application/src/main/java/run/halo/app/infra/SetupStateCache.java
@@ -0,0 +1,96 @@
package run.halo.app.infra;

import io.micrometer.common.util.StringUtils;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import lombok.Data;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import run.halo.app.extension.ConfigMap;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.controller.Controller;
import run.halo.app.extension.controller.ControllerBuilder;
import run.halo.app.extension.controller.Reconciler;
import run.halo.app.infra.utils.JsonUtils;

/**
* <p>A cache that caches system setup state.</p>
* when setUp state changed, the cache will be updated.
*
* @author guqing
* @since 2.5.2
*/
@Component
public class SetupStateCache implements Reconciler<Reconciler.Request>, Supplier<Boolean> {
public static final String SYSTEM_STATES_CONFIGMAP = "system-states";
private final ExtensionClient client;

private final InternalValueCache valueCache = new InternalValueCache();

public SetupStateCache(ExtensionClient client) {
this.client = client;
}

/**
* <p>Gets system setup state.</p>
* Never return null.
*
* @return <code>true</code> if system is initialized, <code>false</code> otherwise.
*/
@NonNull
@Override
public Boolean get() {
return valueCache.get();
}

@Override
public Result reconcile(Request request) {
if (!SYSTEM_STATES_CONFIGMAP.equals(request.name())) {
return Result.doNotRetry();
}
valueCache.cache(isInitialized());
return Result.doNotRetry();
}

@Override
public Controller setupWith(ControllerBuilder builder) {
return builder
.extension(new ConfigMap())
.build();
}

/**
* Check if system is initialized.
*
* @return <code>true</code> if system is initialized, <code>false</code> otherwise.
*/
private boolean isInitialized() {
return client.fetch(ConfigMap.class, SYSTEM_STATES_CONFIGMAP)
.filter(configMap -> configMap.getData() != null)
.map(ConfigMap::getData)
.flatMap(map -> Optional.ofNullable(map.get(SystemStates.GROUP))
.filter(StringUtils::isNotBlank)
.map(value -> JsonUtils.jsonToObject(value, SystemStates.class).getIsSetup())
)
.orElse(false);
}

@Data
static class SystemStates {
static final String GROUP = "states";
Boolean isSetup;
}

static class InternalValueCache {
private final AtomicBoolean value = new AtomicBoolean(false);

public boolean cache(boolean newValue) {
return value.getAndSet(newValue);
}

public boolean get() {
return value.get();
}
}
}
@@ -0,0 +1,57 @@
package run.halo.app.security;

import java.net.URI;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpMethod;
import org.springframework.lang.NonNull;
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
import org.springframework.security.web.server.ServerRedirectStrategy;
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import run.halo.app.infra.SetupStateCache;

/**
* A web filter that will redirect user to set up page if system is not initialized.
*
* @author guqing
* @since 2.5.2
*/
@Component
@RequiredArgsConstructor
public class InitializeRedirectionWebFilter implements WebFilter {
private final URI location = URI.create("/console");
private final ServerWebExchangeMatcher redirectMatcher =
new PathPatternParserServerWebExchangeMatcher("/", HttpMethod.GET);

private final SetupStateCache setupStateCache;

private ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();

@Override
@NonNull
public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) {
return redirectMatcher.matches(exchange)
.flatMap(matched -> {
if (!matched.isMatch() || setupStateCache.get()) {
return chain.filter(exchange);
}
// Redirect to set up page if system is not initialized.
return redirectStrategy.sendRedirect(exchange, location);
});
}

public ServerRedirectStrategy getRedirectStrategy() {
return redirectStrategy;
}

public void setRedirectStrategy(ServerRedirectStrategy redirectStrategy) {
Assert.notNull(redirectStrategy, "redirectStrategy cannot be null");
this.redirectStrategy = redirectStrategy;
}
}
@@ -0,0 +1,109 @@
package run.halo.app.security;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.net.URI;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.security.web.server.ServerRedirectStrategy;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import run.halo.app.infra.SetupStateCache;

/**
* Tests for {@link InitializeRedirectionWebFilter}.
*
* @author guqing
* @since 2.5.2
*/
@ExtendWith(MockitoExtension.class)
class InitializeRedirectionWebFilterTest {

@Mock
private SetupStateCache setupStateCache;

@Mock
private ServerRedirectStrategy serverRedirectStrategy;

@InjectMocks
private InitializeRedirectionWebFilter filter;

@BeforeEach
void setUp() {
filter.setRedirectStrategy(serverRedirectStrategy);
}

@Test
void shouldRedirectWhenSystemNotInitialized() {
when(setupStateCache.get()).thenReturn(false);

WebFilterChain chain = mock(WebFilterChain.class);

MockServerHttpRequest request = MockServerHttpRequest.get("/").build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);

when(serverRedirectStrategy.sendRedirect(any(), any())).thenReturn(Mono.empty().then());

Mono<Void> result = filter.filter(exchange, chain);

StepVerifier.create(result)
.expectNextCount(0)
.expectComplete()
.verify();

verify(serverRedirectStrategy).sendRedirect(eq(exchange), eq(URI.create("/console")));
verify(chain, never()).filter(eq(exchange));
}

@Test
void shouldNotRedirectWhenSystemInitialized() {
when(setupStateCache.get()).thenReturn(true);

WebFilterChain chain = mock(WebFilterChain.class);

MockServerHttpRequest request = MockServerHttpRequest.get("/").build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);
when(chain.filter(any())).thenReturn(Mono.empty().then());
Mono<Void> result = filter.filter(exchange, chain);

StepVerifier.create(result)
.expectNextCount(0)
.expectComplete()
.verify();

verify(serverRedirectStrategy, never()).sendRedirect(eq(exchange),
eq(URI.create("/console")));
verify(chain).filter(eq(exchange));
}

@Test
void shouldNotRedirectWhenNotHomePage() {
WebFilterChain chain = mock(WebFilterChain.class);

MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);
when(chain.filter(any())).thenReturn(Mono.empty().then());
Mono<Void> result = filter.filter(exchange, chain);

StepVerifier.create(result)
.expectNextCount(0)
.expectComplete()
.verify();

verify(serverRedirectStrategy, never()).sendRedirect(eq(exchange),
eq(URI.create("/console")));
verify(chain).filter(eq(exchange));
}
}
Expand Up @@ -24,6 +24,7 @@
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import run.halo.app.infra.SetupStateCache;
import run.halo.app.theme.ThemeContext;
import run.halo.app.theme.ThemeResolver;

Expand All @@ -44,11 +45,15 @@ public class ThemeMessageResolverIntegrationTest {

private URL otherThemeUrl;

@SpyBean
private SetupStateCache setupStateCache;

@Autowired
private WebTestClient webTestClient;

@BeforeEach
void setUp() throws FileNotFoundException, URISyntaxException {
when(setupStateCache.get()).thenReturn(true);
defaultThemeUrl = ResourceUtils.getURL("classpath:themes/default");
otherThemeUrl = ResourceUtils.getURL("classpath:themes/other");

Expand Down