Skip to content

Commit

Permalink
Merge branch 'main' into refactor/6722
Browse files Browse the repository at this point in the history
  • Loading branch information
ruibaby authored Oct 8, 2024
2 parents 9e80f82 + 9d01b62 commit b7fcbca
Show file tree
Hide file tree
Showing 68 changed files with 1,289 additions and 1,090 deletions.
32 changes: 32 additions & 0 deletions api-docs/openapi/v3_0/aggregated.json
Original file line number Diff line number Diff line change
Expand Up @@ -15117,6 +15117,38 @@
]
}
},
"/apis/uc.api.content.halo.run/v1alpha1/posts/{name}/recycle": {
"delete": {
"description": "Move my post to recycle bin.",
"operationId": "RecycleMyPost",
"parameters": [
{
"description": "Post name",
"in": "path",
"name": "name",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"default": {
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/Post"
}
}
},
"description": "default response"
}
},
"tags": [
"PostV1alpha1Uc"
]
}
},
"/apis/uc.api.content.halo.run/v1alpha1/posts/{name}/unpublish": {
"put": {
"description": "Unpublish my post.",
Expand Down
32 changes: 32 additions & 0 deletions api-docs/openapi/v3_0/apis_uc.api_v1alpha1.json
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,38 @@
]
}
},
"/apis/uc.api.content.halo.run/v1alpha1/posts/{name}/recycle": {
"delete": {
"description": "Move my post to recycle bin.",
"operationId": "RecycleMyPost",
"parameters": [
{
"description": "Post name",
"in": "path",
"name": "name",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"default": {
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/Post"
}
}
},
"description": "default response"
}
},
"tags": [
"PostV1alpha1Uc"
]
}
},
"/apis/uc.api.content.halo.run/v1alpha1/posts/{name}/unpublish": {
"put": {
"description": "Unpublish my post.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,6 @@ public interface PostService {
Mono<Post> revertToSpecifiedSnapshot(String postName, String snapshotName);

Mono<ContentWrapper> deleteContent(String postName, String snapshotName);

Mono<Post> recycleBy(String postName, String username);
}
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,15 @@ public Mono<ContentWrapper> deleteContent(String postName, String snapshotName)
});
}

@Override
public Mono<Post> recycleBy(String postName, String username) {
return getByUsername(postName, username)
.flatMap(post -> updatePostWithRetry(post, record -> {
record.getSpec().setDeleted(true);
return record;
}));
}

private Mono<Post> updatePostWithRetry(Post post, UnaryOperator<Post> func) {
return client.update(func.apply(post))
.onErrorResume(OptimisticLockingFailureException.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,27 @@ public RouterFunction<ServerResponse> endpoint() {
.operationId("UnpublishMyPost")
.description("Unpublish my post.")
.parameter(namePathParam)
.response(responseBuilder().implementation(Post.class)))
.response(responseBuilder().implementation(Post.class))
)
.DELETE("/{name}/recycle", this::recycleMyPost, builder -> builder.tag(tag)
.operationId("RecycleMyPost")
.description("Move my post to recycle bin.")
.parameter(namePathParam)
.response(responseBuilder().implementation(Post.class))
)
.build(),
builder -> {
})
.build();
}

private Mono<ServerResponse> recycleMyPost(ServerRequest request) {
final var name = request.pathVariable("name");
return getCurrentUser()
.flatMap(username -> postService.recycleBy(name, username))
.flatMap(post -> ServerResponse.ok().bodyValue(post));
}

private Mono<ServerResponse> getMyPostDraft(ServerRequest request) {
var name = request.pathVariable("name");
var patched = request.queryParam("patched").map(Boolean::valueOf).orElse(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import static org.springframework.security.web.server.authentication.ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.builder;
import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;

import java.util.Collections;
import java.util.concurrent.ConcurrentHashMap;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
Expand All @@ -12,25 +11,18 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.security.web.server.savedrequest.ServerRequestCache;
import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.session.MapSession;
import org.springframework.session.config.annotation.web.server.EnableSpringWebSession;
import reactor.core.publisher.Mono;
import run.halo.app.core.user.service.RoleService;
import run.halo.app.core.user.service.UserService;
import run.halo.app.extension.ReactiveExtensionClient;
Expand All @@ -44,8 +36,6 @@
import run.halo.app.security.authentication.pat.PatAuthenticationManager;
import run.halo.app.security.authentication.pat.PatServerWebExchangeMatcher;
import run.halo.app.security.authorization.AuthorityUtils;
import run.halo.app.security.authorization.NotAuthenticatedAuthorizationManager;
import run.halo.app.security.authorization.RequestInfoAuthorizationManager;
import run.halo.app.security.session.InMemoryReactiveIndexedSessionRepository;
import run.halo.app.security.session.ReactiveIndexedSessionRepository;

Expand Down Expand Up @@ -86,29 +76,6 @@ SecurityWebFilterChain filterChain(ServerHttpSecurity http,
new NegatedServerWebExchangeMatcher(staticResourcesMatcher));

http.securityMatcher(securityMatcher)
.authorizeExchange(spec -> spec.pathMatchers(
"/api/**",
"/apis/**",
"/actuator/**"
).access(new RequestInfoAuthorizationManager(roleService))
.pathMatchers(HttpMethod.GET, "/login", "/signup")
.access(new NotAuthenticatedAuthorizationManager())
.pathMatchers(
"/login/**",
"/challenges/**",
"/password-reset/**",
"/signup",
"/logout"
).permitAll()
.pathMatchers("/console/**", "/uc/**").authenticated()
.matchers(createHtmlMatcher()).access((authentication, context) ->
// we only need to check the authentication is authenticated
// because we treat anonymous user as authenticated
authentication.map(Authentication::isAuthenticated)
.map(AuthorizationDecision::new)
.switchIfEmpty(Mono.fromSupplier(() -> new AuthorizationDecision(false)))
)
.anyExchange().permitAll())
.anonymous(spec -> {
spec.authorities(AuthorityUtils.ROLE_PREFIX + AnonymousUserConst.Role);
spec.principal(AnonymousUserConst.PRINCIPAL);
Expand Down Expand Up @@ -190,14 +157,5 @@ CryptoService cryptoService(HaloProperties haloProperties) {
return new RsaKeyService(haloProperties.getWorkDir().resolve("keys"));
}

private static ServerWebExchangeMatcher createHtmlMatcher() {
ServerWebExchangeMatcher get =
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**");
ServerWebExchangeMatcher notFavicon = new NegatedServerWebExchangeMatcher(
ServerWebExchangeMatchers.pathMatchers("/favicon.*"));
MediaTypeServerWebExchangeMatcher html =
new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);
html.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
return new AndServerWebExchangeMatcher(get, notFavicon, html);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,14 @@ public Mono<Void> restore(Publisher<DataBuffer> content) {
return Mono.usingWhen(
createTempDir("halo-restore-", scheduler),
tempDir -> unpackBackup(content, tempDir)
.then(Mono.defer(() -> restoreExtensions(tempDir)))
.then(Mono.defer(() ->
// This step skips index verification such as unique index.
// In order to avoid index conflicts after recovery or
// OptimisticLockingFailureException when updating the same record,
// so we need to truncate all extension stores before saving(create or update).
repository.deleteAll()
.then(restoreExtensions(tempDir)))
)
.then(Mono.defer(() -> restoreWorkdir(tempDir))),
tempDir -> deleteRecursivelyAndSilently(tempDir, scheduler)
);
Expand Down Expand Up @@ -241,13 +248,9 @@ private Mono<Void> restoreExtensions(Path backupRoot) {
sink.complete();
})
// reset version
.doOnNext(extensionStore -> extensionStore.setVersion(null)).buffer(100)
// We might encounter OptimisticLockingFailureException when saving extension
// store,
// So we have to delete all extension stores before saving.
.flatMap(extensionStores -> repository.deleteAll(extensionStores)
.thenMany(repository.saveAll(extensionStores))
)
.doOnNext(extensionStore -> extensionStore.setVersion(null))
.buffer(100)
.flatMap(repository::saveAll)
.doOnNext(extensionStore -> log.info("Restored extension store: {}",
extensionStore.getName()))
.then(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package run.halo.app.security.authorization;

import java.util.Collections;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import run.halo.app.core.user.service.RoleService;
import run.halo.app.security.authentication.SecurityConfigurer;

/**
* Authorization exchange configurers.
*
* @author johnniang
* @since 2.20.0
*/
@Component
class AuthorizationExchangeConfigurers {

private final AuthenticationTrustResolver authenticationTrustResolver =
new AuthenticationTrustResolverImpl();

@Bean
@Order(0)
SecurityConfigurer apiAuthorizationConfigurer(RoleService roleService) {
return http -> http.authorizeExchange(
spec -> spec.pathMatchers("/api/**", "/apis/**", "/actuator/**")
.access(new RequestInfoAuthorizationManager(roleService)));
}

@Bean
@Order(100)
SecurityConfigurer unauthenticatedAuthorizationConfigurer() {
return http -> http.authorizeExchange(spec -> {
spec.pathMatchers(HttpMethod.GET, "/login", "/signup")
.access((authentication, context) -> authentication.map(
a -> !authenticationTrustResolver.isAuthenticated(a)
)
.defaultIfEmpty(true)
.map(AuthorizationDecision::new));
});
}

@Bean
@Order(200)
SecurityConfigurer preAuthenticationAuthorizationConfigurer() {
return http -> http.authorizeExchange(spec -> spec.pathMatchers(
"/login/**",
"/challenges/**",
"/password-reset/**",
"/signup",
"/logout"
).permitAll());
}

@Bean
@Order(300)
SecurityConfigurer authenticatedAuthorizationConfigurer() {
// Anonymous user is not allowed
return http -> http.authorizeExchange(
spec -> spec.pathMatchers("/console/**", "/uc/**").authenticated()
);
}

@Bean
@Order(400)
SecurityConfigurer anonymousOrAuthenticatedAuthorizationConfigurer() {
return http -> http.authorizeExchange(
spec -> spec.matchers(createHtmlMatcher()).access((authentication, context) ->
// we only need to check the authentication is authenticated
// because we treat anonymous user as authenticated
authentication.map(Authentication::isAuthenticated)
.map(AuthorizationDecision::new)
.switchIfEmpty(Mono.fromSupplier(() -> new AuthorizationDecision(false)))
)
);
}

@Bean
@Order
SecurityConfigurer permitAllAuthorizationConfigurer() {
return http -> http.authorizeExchange(spec -> spec.anyExchange().permitAll());
}

private static ServerWebExchangeMatcher createHtmlMatcher() {
ServerWebExchangeMatcher get =
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**");
ServerWebExchangeMatcher notFavicon = new NegatedServerWebExchangeMatcher(
ServerWebExchangeMatchers.pathMatchers("/favicon.*"));
MediaTypeServerWebExchangeMatcher html =
new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);
html.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
return new AndServerWebExchangeMatcher(get, notFavicon, html);
}

}

This file was deleted.

Loading

0 comments on commit b7fcbca

Please sign in to comment.