Suppress compilation warnings and remove deprecated method and classes (#4308)

#### What type of PR is this?

/kind cleanup
/area core
/milestone 2.8.x

#### What this PR does / why we need it:

- Suppress compilation warnings.
- Remove deprecated methods and classes.
- Remove unused methods.

- Before
    ```bash
    ❯ ./gradlew compileJava compileTestJava
    
    > Task :application:compileJava
    /Users/johnniang/workspaces/halo-dev/halo/application/src/main/java/run/halo/app/core/extension/reconciler/ThemeReconciler.java:48: warning: [removal] ThemePathPolicy in run.halo.app.theme has been deprecated and marked for removal
        private final ThemePathPolicy themePathPolicy;
                      ^
    /Users/johnniang/workspaces/halo-dev/halo/application/src/main/java/run/halo/app/core/extension/reconciler/ThemeReconciler.java:48: warning: [removal] ThemePathPolicy in run.halo.app.theme has been deprecated and marked for removal
        private final ThemePathPolicy themePathPolicy;
                      ^
    /Users/johnniang/workspaces/halo-dev/halo/application/src/main/java/run/halo/app/core/extension/reconciler/ThemeReconciler.java:48: warning: [removal] ThemePathPolicy in run.halo.app.theme has been deprecated and marked for removal
        private final ThemePathPolicy themePathPolicy;
                      ^
    /Users/johnniang/workspaces/halo-dev/halo/application/src/main/java/run/halo/app/core/extension/reconciler/ThemeReconciler.java:48: warning: [removal] ThemePathPolicy in run.halo.app.theme has been deprecated and marked for removal
        private final ThemePathPolicy themePathPolicy;
                      ^
    /Users/johnniang/workspaces/halo-dev/halo/application/src/main/java/run/halo/app/core/extension/reconciler/ThemeReconciler.java:48: warning: [removal] ThemePathPolicy in run.halo.app.theme has been deprecated and marked for removal
        private final ThemePathPolicy themePathPolicy;
                      ^
    /Users/johnniang/workspaces/halo-dev/halo/application/src/main/java/run/halo/app/core/extension/reconciler/ThemeReconciler.java:60: warning: [removal] ThemePathPolicy in run.halo.app.theme has been deprecated and marked for removal
            themePathPolicy = new ThemePathPolicy(haloProperties.getWorkDir());
                                  ^
    /Users/johnniang/workspaces/halo-dev/halo/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java:64: warning: [removal] authorizeExchange() in ServerHttpSecurity has been deprecated and marked for removal
                .authorizeExchange().anyExchange()
                ^
    /Users/johnniang/workspaces/halo-dev/halo/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java:65: warning: [removal] and() in ServerHttpSecurity.AuthorizeExchangeSpec has been deprecated and marked for removal
                .access(new RequestInfoAuthorizationManager(roleService)).and()
                                                                         ^
    /Users/johnniang/workspaces/halo-dev/halo/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java:88: warning: [removal] authorizeExchange() in ServerHttpSecurity has been deprecated and marked for removal
                .authorizeExchange().anyExchange().permitAll().and()
                ^
    /Users/johnniang/workspaces/halo-dev/halo/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java:88: warning: [removal] and() in ServerHttpSecurity.AuthorizeExchangeSpec has been deprecated and marked for removal
                .authorizeExchange().anyExchange().permitAll().and()
                                                              ^
    /Users/johnniang/workspaces/halo-dev/halo/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java:90: warning: [removal] headers() in ServerHttpSecurity has been deprecated and marked for removal
                .headers()
                ^
    /Users/johnniang/workspaces/halo-dev/halo/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java💯 warning: [removal] cache() in ServerHttpSecurity.HeaderSpec has been deprecated and marked for removal
                .cache().disable().and()
                ^
    /Users/johnniang/workspaces/halo-dev/halo/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java💯 warning: [removal] and() in ServerHttpSecurity.HeaderSpec has been deprecated and marked for removal
                .cache().disable().and()
                                  ^
    /Users/johnniang/workspaces/halo-dev/halo/application/src/main/java/run/halo/app/security/CsrfConfigurer.java:24: warning: [removal] csrf() in ServerHttpSecurity has been deprecated and marked for removal
            http.csrf().csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())
                ^
    /Users/johnniang/workspaces/halo-dev/halo/application/src/main/java/run/halo/app/security/authorization/DefaultRuleResolver.java:58: warning: [removal] visitRulesFor(UserDetails,RuleAccumulator) in AuthorizationRuleResolver has been deprecated and marked for removal
        public void visitRulesFor(UserDetails user, RuleAccumulator visitor) {
                    ^
    /Users/johnniang/workspaces/halo-dev/halo/application/src/main/java/run/halo/app/security/authorization/DefaultRuleResolver.java:43: warning: [removal] rulesFor(UserDetails) in AuthorizationRuleResolver has been deprecated and marked for removal
        public PolicyRuleList rulesFor(UserDetails user) {
                              ^
    Note: Some input files use or override a deprecated API.
    Note: Recompile with -Xlint:deprecation for details.
    Note: /Users/johnniang/workspaces/halo-dev/halo/application/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java uses unchecked or unsafe operations.
    Note: Recompile with -Xlint:unchecked for details.
    16 warnings
    
    > Task :application:compileTestJava
    /Users/johnniang/workspaces/halo-dev/halo/application/src/test/java/run/halo/app/core/extension/reconciler/ThemeReconcilerTest.java:90: warning: [removal] ThemePathPolicy in run.halo.app.theme has been deprecated and marked for removal
            final ThemePathPolicy themePathPolicy = new ThemePathPolicy(testWorkDir);
                  ^
    /Users/johnniang/workspaces/halo-dev/halo/application/src/test/java/run/halo/app/core/extension/reconciler/ThemeReconcilerTest.java:90: warning: [removal] ThemePathPolicy in run.halo.app.theme has been deprecated and marked for removal
            final ThemePathPolicy themePathPolicy = new ThemePathPolicy(testWorkDir);
                                                        ^
    Note: /Users/johnniang/workspaces/halo-dev/halo/application/src/test/java/run/halo/app/security/authorization/RequestInfoResolverTest.java uses or overrides a deprecated API.
    Note: Recompile with -Xlint:deprecation for details.
    Note: /Users/johnniang/workspaces/halo-dev/halo/application/src/test/java/run/halo/app/migration/BackupReconcilerTest.java uses unchecked or unsafe operations.
    Note: Recompile with -Xlint:unchecked for details.
    2 warnings
    ```
- After

    ```bash
    ❯ ./gradlew clean compileJava compileTestJava
    
    > Task :api:compileJava
    /Users/johnniang/workspaces/halo-dev/halo/api/src/main/java/run/halo/app/extension/Unstructured.java:69: warning: This field does not exist, or would have been excluded anyway.
        @EqualsAndHashCode(exclude = "version")
                                     ^
    Note: /Users/johnniang/workspaces/halo-dev/halo/api/src/main/java/run/halo/app/extension/Unstructured.java uses unchecked or unsafe operations.
    Note: Recompile with -Xlint:unchecked for details.
    1 warning
    
    > Task :application:compileJava
    Note: /Users/johnniang/workspaces/halo-dev/halo/application/src/main/java/run/halo/app/plugin/SpringExtensionFactory.java uses or overrides a deprecated API.
    Note: Recompile with -Xlint:deprecation for details.
    
    > Task :api:compileTestJava
    Note: /Users/johnniang/workspaces/halo-dev/halo/api/src/test/java/run/halo/app/infra/utils/JsonUtilsTest.java uses unchecked or unsafe operations.
    Note: Recompile with -Xlint:unchecked for details.
    
    BUILD SUCCESSFUL in 7s
    22 actionable tasks: 15 executed, 7 up-to-date
    ```

#### Does this PR introduce a user-facing change?

```release-note
None
```
pull/4322/head
John Niang 2023-07-27 16:59:19 +08:00 committed by GitHub
parent f3d7e856ac
commit 150e9975ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 367 additions and 1086 deletions

View File

@ -1,5 +1,7 @@
package run.halo.app.core.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.LinkedHashSet;
@ -15,14 +17,14 @@ import run.halo.app.extension.GVK;
@GVK(group = "", version = "v1alpha1", kind = "Menu", plural = "menus", singular = "menu")
public class Menu extends AbstractExtension {
@Schema(description = "The spec of menu.", required = true)
@Schema(description = "The spec of menu.", requiredMode = REQUIRED)
private Spec spec;
@Data
@Schema(name = "MenuSpec")
public static class Spec {
@Schema(description = "The display name of the menu.", required = true)
@Schema(description = "The display name of the menu.", requiredMode = REQUIRED)
private String displayName;
@Schema(description = "Names of menu children below this menu.")

View File

@ -1,5 +1,7 @@
package run.halo.app.core.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import io.swagger.v3.oas.annotations.media.ArraySchema;
@ -19,7 +21,7 @@ import run.halo.app.extension.Ref;
plural = "menuitems", singular = "menuitem")
public class MenuItem extends AbstractExtension {
@Schema(description = "The spec of menu item.", required = true)
@Schema(description = "The spec of menu item.", requiredMode = REQUIRED)
private MenuItemSpec spec;
@Schema(description = "The status of menu item.")

View File

@ -1,5 +1,6 @@
package run.halo.app.core.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static java.util.Arrays.compare;
import static run.halo.app.core.extension.Role.GROUP;
import static run.halo.app.core.extension.Role.KIND;
@ -44,7 +45,7 @@ public class Role extends AbstractExtension {
public static final String VERSION = "v1alpha1";
public static final String KIND = "Role";
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
List<PolicyRule> rules;
/**

View File

@ -1,5 +1,7 @@
package run.halo.app.core.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import lombok.Data;
@ -21,25 +23,25 @@ public class Setting extends AbstractExtension {
public static final String KIND = "Setting";
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private SettingSpec spec;
@Data
public static class SettingSpec {
@Schema(required = true, minLength = 1)
@Schema(requiredMode = REQUIRED, minLength = 1)
private List<SettingForm> forms;
}
@Data
public static class SettingForm {
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private String group;
private String label;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private List<Object> formSchema;
}
}

View File

@ -34,7 +34,7 @@ public class Theme extends AbstractExtension {
public static final String THEME_NAME_LABEL = "theme.halo.run/theme-name";
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private ThemeSpec spec;
private ThemeStatus status;

View File

@ -1,5 +1,6 @@
package run.halo.app.core.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static run.halo.app.core.extension.User.GROUP;
import static run.halo.app.core.extension.User.KIND;
import static run.halo.app.core.extension.User.VERSION;
@ -41,7 +42,7 @@ public class User extends AbstractExtension {
public static final String HIDDEN_USER_LABEL = "halo.run/hidden-user";
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private UserSpec spec;
private UserStatus status;
@ -49,12 +50,12 @@ public class User extends AbstractExtension {
@Data
public static class UserSpec {
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private String displayName;
private String avatar;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private String email;
private String phone;
@ -87,16 +88,16 @@ public class User extends AbstractExtension {
@Data
public static class LoginHistory {
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private Instant loginAt;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private String sourceIp;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private String userAgent;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private Boolean successful;
private String reason;

View File

@ -1,5 +1,6 @@
package run.halo.app.core.extension.attachment;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static run.halo.app.core.extension.attachment.Attachment.KIND;
import io.swagger.v3.oas.annotations.media.ArraySchema;
@ -20,7 +21,7 @@ public class Attachment extends AbstractExtension {
public static final String KIND = "Attachment";
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private AttachmentSpec spec;
private AttachmentStatus status;

View File

@ -20,8 +20,6 @@ public enum Constant {
* Do not use this key to set external link. You could implement
* {@link AttachmentHandler#getPermalink} by your self.
* <p>
*
* @deprecated Use your own group instead.
*/
public static final String EXTERNAL_LINK_ANNO_KEY = GROUP + "/external-link";

View File

@ -1,5 +1,6 @@
package run.halo.app.core.extension.attachment;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static run.halo.app.core.extension.attachment.Group.KIND;
import io.swagger.v3.oas.annotations.media.Schema;
@ -20,7 +21,7 @@ public class Group extends AbstractExtension {
public static final String KIND = "Group";
public static final String HIDDEN_LABEL = "halo.run/hidden";
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private GroupSpec spec;
private GroupStatus status;
@ -28,7 +29,7 @@ public class Group extends AbstractExtension {
@Data
public static class GroupSpec {
@Schema(required = true, description = "Display name of group")
@Schema(requiredMode = REQUIRED, description = "Display name of group")
private String displayName;
}

View File

@ -1,5 +1,6 @@
package run.halo.app.core.extension.content;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static run.halo.app.core.extension.content.Category.KIND;
import com.fasterxml.jackson.annotation.JsonIgnore;
@ -28,7 +29,7 @@ public class Category extends AbstractExtension {
public static final GroupVersionKind GVK = GroupVersionKind.fromExtension(Category.class);
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private CategorySpec spec;
@Schema
@ -42,10 +43,10 @@ public class Category extends AbstractExtension {
@Data
public static class CategorySpec {
@Schema(required = true, minLength = 1)
@Schema(requiredMode = REQUIRED, minLength = 1)
private String displayName;
@Schema(required = true, minLength = 1)
@Schema(requiredMode = REQUIRED, minLength = 1)
private String slug;
private String description;
@ -54,7 +55,7 @@ public class Category extends AbstractExtension {
private String template;
@Schema(required = true, defaultValue = "0")
@Schema(requiredMode = REQUIRED, defaultValue = "0")
private Integer priority;
private List<String> children;

View File

@ -1,5 +1,6 @@
package run.halo.app.core.extension.content;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import com.fasterxml.jackson.annotation.JsonIgnore;
@ -30,7 +31,7 @@ public class Comment extends AbstractExtension {
public static final String KIND = "Comment";
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private CommentSpec spec;
@Schema
@ -49,7 +50,7 @@ public class Comment extends AbstractExtension {
@EqualsAndHashCode(callSuper = true)
public static class CommentSpec extends BaseCommentSpec {
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private Ref subjectRef;
private Instant lastReadTime;
@ -58,13 +59,13 @@ public class Comment extends AbstractExtension {
@Data
public static class BaseCommentSpec {
@Schema(required = true, minLength = 1)
@Schema(requiredMode = REQUIRED, minLength = 1)
private String raw;
@Schema(required = true, minLength = 1)
@Schema(requiredMode = REQUIRED, minLength = 1)
private String content;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private CommentOwner owner;
private String userAgent;
@ -78,19 +79,19 @@ public class Comment extends AbstractExtension {
*/
private Instant creationTime;
@Schema(required = true, defaultValue = "0")
@Schema(requiredMode = REQUIRED, defaultValue = "0")
private Integer priority;
@Schema(required = true, defaultValue = "false")
@Schema(requiredMode = REQUIRED, defaultValue = "false")
private Boolean top;
@Schema(required = true, defaultValue = "true")
@Schema(requiredMode = REQUIRED, defaultValue = "true")
private Boolean allowNotification;
@Schema(required = true, defaultValue = "false")
@Schema(requiredMode = REQUIRED, defaultValue = "false")
private Boolean approved;
@Schema(required = true, defaultValue = "false")
@Schema(requiredMode = REQUIRED, defaultValue = "false")
private Boolean hidden;
}
@ -100,10 +101,10 @@ public class Comment extends AbstractExtension {
public static final String AVATAR_ANNO = "avatar";
public static final String WEBSITE_ANNO = "website";
@Schema(required = true, minLength = 1)
@Schema(requiredMode = REQUIRED, minLength = 1)
private String kind;
@Schema(required = true, maxLength = 64)
@Schema(requiredMode = REQUIRED, maxLength = 64)
private String name;
private String displayName;

View File

@ -1,5 +1,7 @@
package run.halo.app.core.extension.content;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -21,14 +23,14 @@ public class Reply extends AbstractExtension {
public static final String KIND = "Reply";
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private ReplySpec spec;
@Data
@EqualsAndHashCode(callSuper = true)
public static class ReplySpec extends Comment.BaseCommentSpec {
@Schema(required = true, minLength = 1)
@Schema(requiredMode = REQUIRED, minLength = 1)
private String commentName;
private String quoteReply;

View File

@ -1,5 +1,7 @@
package run.halo.app.core.extension.content;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;
@ -36,7 +38,7 @@ public class SinglePage extends AbstractExtension {
public static final String OWNER_LABEL = "content.halo.run/owner";
public static final String VISIBLE_LABEL = "content.halo.run/visible";
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private SinglePageSpec spec;
@Schema
@ -58,10 +60,10 @@ public class SinglePage extends AbstractExtension {
@Data
public static class SinglePageSpec {
@Schema(required = true, minLength = 1)
@Schema(requiredMode = REQUIRED, minLength = 1)
private String title;
@Schema(required = true, minLength = 1)
@Schema(requiredMode = REQUIRED, minLength = 1)
private String slug;
/**
@ -79,27 +81,27 @@ public class SinglePage extends AbstractExtension {
private String cover;
@Schema(required = true, defaultValue = "false")
@Schema(requiredMode = REQUIRED, defaultValue = "false")
private Boolean deleted;
@Schema(required = true, defaultValue = "false")
@Schema(requiredMode = REQUIRED, defaultValue = "false")
private Boolean publish;
private Instant publishTime;
@Schema(required = true, defaultValue = "false")
@Schema(requiredMode = REQUIRED, defaultValue = "false")
private Boolean pinned;
@Schema(required = true, defaultValue = "true")
@Schema(requiredMode = REQUIRED, defaultValue = "true")
private Boolean allowComment;
@Schema(required = true, defaultValue = "PUBLIC")
@Schema(requiredMode = REQUIRED, defaultValue = "PUBLIC")
private Post.VisibleEnum visible;
@Schema(required = true, defaultValue = "0")
@Schema(requiredMode = REQUIRED, defaultValue = "0")
private Integer priority;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private Post.Excerpt excerpt;
private List<Map<String, String>> htmlMetas;

View File

@ -1,5 +1,7 @@
package run.halo.app.core.extension.content;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;
import java.util.LinkedHashSet;
@ -26,19 +28,19 @@ public class Snapshot extends AbstractExtension {
public static final String KIND = "Snapshot";
public static final String KEEP_RAW_ANNO = "content.halo.run/keep-raw";
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private SnapShotSpec spec;
@Data
public static class SnapShotSpec {
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private Ref subjectRef;
/**
* such as: markdown | html | json | asciidoc | latex.
*/
@Schema(required = true, minLength = 1, maxLength = 50)
@Schema(requiredMode = REQUIRED, minLength = 1, maxLength = 50)
private String rawType;
private String rawPatch;
@ -49,7 +51,7 @@ public class Snapshot extends AbstractExtension {
private Instant lastModifyTime;
@Schema(required = true, minLength = 1)
@Schema(requiredMode = REQUIRED, minLength = 1)
private String owner;
private Set<String> contributors;

View File

@ -1,5 +1,7 @@
package run.halo.app.core.extension.content;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@ -25,7 +27,7 @@ public class Tag extends AbstractExtension {
public static final GroupVersionKind GVK = GroupVersionKind.fromExtension(Tag.class);
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private TagSpec spec;
@Schema
@ -34,10 +36,10 @@ public class Tag extends AbstractExtension {
@Data
public static class TagSpec {
@Schema(required = true, minLength = 1)
@Schema(requiredMode = REQUIRED, minLength = 1)
private String displayName;
@Schema(required = true, minLength = 1)
@Schema(requiredMode = REQUIRED, minLength = 1)
private String slug;
/**

View File

@ -1,5 +1,7 @@
package run.halo.app.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
@ -13,7 +15,7 @@ import org.springframework.util.StringUtils;
*/
public interface ExtensionOperator {
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
@JsonProperty("apiVersion")
default String getApiVersion() {
final var gvk = getClass().getAnnotation(GVK.class);
@ -27,7 +29,7 @@ public interface ExtensionOperator {
return gvk.version();
}
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
@JsonProperty("kind")
default String getKind() {
final var gvk = getClass().getAnnotation(GVK.class);
@ -38,7 +40,7 @@ public interface ExtensionOperator {
return gvk.kind();
}
@Schema(required = true, implementation = Metadata.class)
@Schema(requiredMode = REQUIRED, implementation = Metadata.class)
@JsonProperty("metadata")
MetadataOperator getMetadata();
@ -48,30 +50,6 @@ public interface ExtensionOperator {
void setMetadata(MetadataOperator metadata);
/**
* This method is only for backward compatibility. Same as {@link #getMetadata()}.
*
* @return Extension metadata.
* @see #getMetadata()
*/
@JsonIgnore
@Deprecated(forRemoval = true)
default MetadataOperator metadata() {
return getMetadata();
}
/**
* This method is only for backward compatibility. Same as
* {@link #setMetadata(MetadataOperator)}.
*
* @param metadata is Extension metadata.
* @see #setMetadata(MetadataOperator)
*/
@Deprecated(forRemoval = true)
default void metadata(MetadataOperator metadata) {
setMetadata(metadata);
}
/**
* Sets GroupVersionKind of the Extension.
*

View File

@ -1,5 +1,7 @@
package run.halo.app.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.ArrayList;
@ -16,17 +18,17 @@ import run.halo.app.infra.utils.GenericClassUtils;
public class ListResult<T> implements Iterable<T>, Supplier<Stream<T>> {
@Schema(description = "Page number, starts from 1. If not set or equal to 0, it means no "
+ "pagination.", required = true)
+ "pagination.", requiredMode = REQUIRED)
private final int page;
@Schema(description = "Size of each page. If not set or equal to 0, it means no pagination.",
required = true)
requiredMode = REQUIRED)
private final int size;
@Schema(description = "Total elements.", required = true)
@Schema(description = "Total elements.", requiredMode = REQUIRED)
private final long total;
@Schema(description = "A chunk of items.", required = true)
@Schema(description = "A chunk of items.", requiredMode = REQUIRED)
private final List<T> items;
public ListResult(int page, int size, long total, List<T> items) {
@ -50,17 +52,20 @@ public class ListResult<T> implements Iterable<T>, Supplier<Stream<T>> {
this(0, 0, items.size(), items);
}
@Schema(description = "Indicates whether current page is the first page.", required = true)
@Schema(description = "Indicates whether current page is the first page.",
requiredMode = REQUIRED)
public boolean isFirst() {
return !hasPrevious();
}
@Schema(description = "Indicates whether current page is the last page.", required = true)
@Schema(description = "Indicates whether current page is the last page.",
requiredMode = REQUIRED)
public boolean isLast() {
return !hasNext();
}
@Schema(description = "Indicates whether current page has previous page.", required = true)
@Schema(description = "Indicates whether current page has previous page.",
requiredMode = REQUIRED)
@JsonProperty("hasNext")
public boolean hasNext() {
if (page <= 0) {
@ -69,7 +74,8 @@ public class ListResult<T> implements Iterable<T>, Supplier<Stream<T>> {
return page < getTotalPages();
}
@Schema(description = "Indicates whether current page has previous page.", required = true)
@Schema(description = "Indicates whether current page has previous page.",
requiredMode = REQUIRED)
@JsonProperty("hasPrevious")
public boolean hasPrevious() {
return page > 1;
@ -80,7 +86,7 @@ public class ListResult<T> implements Iterable<T>, Supplier<Stream<T>> {
return items.iterator();
}
@Schema(description = "Indicates total pages.", required = true)
@Schema(description = "Indicates total pages.", requiredMode = REQUIRED)
@JsonProperty("totalPages")
public long getTotalPages() {
return size == 0 ? 1 : (total + size - 1) / size;

View File

@ -1,5 +1,7 @@
package run.halo.app.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import io.swagger.v3.oas.annotations.media.Schema;
@ -17,7 +19,7 @@ import java.util.Set;
@Schema(implementation = Metadata.class)
public interface MetadataOperator {
@Schema(name = "name", description = "Metadata name", required = true)
@Schema(name = "name", description = "Metadata name", requiredMode = REQUIRED)
@JsonProperty("name")
String getName();

View File

@ -1,5 +1,7 @@
package run.halo.app.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Objects;
import lombok.Data;
@ -17,7 +19,7 @@ public class Ref {
@Schema(description = "Extension kind")
private String kind;
@Schema(required = true, description = "Extension name. This field is mandatory")
@Schema(requiredMode = REQUIRED, description = "Extension name. This field is mandatory")
private String name;
public static Ref of(String name) {

View File

@ -1,5 +1,7 @@
package run.halo.app.infra;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;
import lombok.AllArgsConstructor;
@ -28,7 +30,7 @@ public class Condition {
* example: Ready, Initialized.
* maxLength: 316.
*/
@Schema(required = true, maxLength = 316,
@Schema(requiredMode = REQUIRED, maxLength = 316,
pattern = "^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?("
+ "([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$")
private String type;
@ -36,26 +38,26 @@ public class Condition {
/**
* Status is the status of the condition. Can be True, False, Unknown.
*/
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private ConditionStatus status;
/**
* Last time the condition transitioned from one status to another.
*/
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private Instant lastTransitionTime;
/**
* Human-readable message indicating details about last transition.
* This may be an empty string.
*/
@Schema(required = true, maxLength = 32768)
@Schema(requiredMode = REQUIRED, maxLength = 32768)
private String message;
/**
* Unique, one-word, CamelCase reason for the condition's last transition.
*/
@Schema(required = true, maxLength = 1024,
@Schema(requiredMode = REQUIRED, maxLength = 1024,
pattern = "^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$")
private String reason;
}

View File

@ -1,5 +1,7 @@
package run.halo.app.search;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
@ -17,7 +19,7 @@ public class SearchParam {
this.query = query;
}
@Schema(name = "keyword", required = true)
@Schema(name = "keyword", requiredMode = REQUIRED)
public String getKeyword() {
var keyword = query.getFirst("keyword");
if (!StringUtils.hasText(keyword)) {

View File

@ -61,8 +61,9 @@ public class WebServerSecurityConfig {
http.securityMatcher(pathMatchers("/api/**", "/apis/**", "/oauth2/**",
"/login/**", "/logout", "/actuator/**"))
.authorizeExchange().anyExchange()
.access(new RequestInfoAuthorizationManager(roleService)).and()
.authorizeExchange(spec -> {
spec.anyExchange().access(new RequestInfoAuthorizationManager(roleService));
})
.anonymous(spec -> {
spec.authorities(AnonymousUserConst.Role);
spec.principal(AnonymousUserConst.PRINCIPAL);
@ -85,19 +86,24 @@ public class WebServerSecurityConfig {
var mediaTypeMatcher = new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);
mediaTypeMatcher.setIgnoredMediaTypes(Set.of(MediaType.ALL));
http.securityMatcher(new AndServerWebExchangeMatcher(pathMatcher, mediaTypeMatcher))
.authorizeExchange().anyExchange().permitAll().and()
.securityContextRepository(securityContextRepository)
.headers()
.frameOptions(spec -> {
.authorizeExchange(spec -> {
spec.anyExchange().permitAll();
})
.headers(headerSpec -> headerSpec
.frameOptions(frameSpec -> {
var frameOptions = haloProperties.getSecurity().getFrameOptions();
spec.mode(frameOptions.getMode());
frameSpec.mode(frameOptions.getMode());
if (frameOptions.isDisabled()) {
spec.disable();
frameSpec.disable();
}
})
.referrerPolicy(
spec -> spec.policy(haloProperties.getSecurity().getReferrerOptions().getPolicy()))
.cache().disable().and()
.referrerPolicy(referrerPolicySpec -> {
referrerPolicySpec.policy(
haloProperties.getSecurity().getReferrerOptions().getPolicy());
})
.cache(ServerHttpSecurity.HeaderSpec.CacheSpec::disable)
)
.anonymous(spec -> spec.authenticationFilter(
new HaloAnonymousAuthenticationWebFilter("portal", AnonymousUserConst.PRINCIPAL,
AuthorityUtils.createAuthorityList(AnonymousUserConst.Role),

View File

@ -1,5 +1,7 @@
package run.halo.app.content;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.HashMap;
import org.apache.commons.lang3.StringUtils;
@ -11,11 +13,11 @@ import run.halo.app.extension.Ref;
* @author guqing
* @since 2.0.0
*/
public record ContentRequest(@Schema(required = true) Ref subjectRef,
public record ContentRequest(@Schema(requiredMode = REQUIRED) Ref subjectRef,
String headSnapshotName,
@Schema(required = true) String raw,
@Schema(required = true) String content,
@Schema(required = true) String rawType) {
@Schema(requiredMode = REQUIRED) String raw,
@Schema(requiredMode = REQUIRED) String content,
@Schema(requiredMode = REQUIRED) String rawType) {
public Snapshot toSnapshot() {
final Snapshot snapshot = new Snapshot();

View File

@ -1,5 +1,7 @@
package run.halo.app.content;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import lombok.Data;
@ -17,21 +19,21 @@ import run.halo.app.core.extension.content.Tag;
@Data
public class ListedPost {
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private Post post;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private List<Category> categories;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private List<Tag> tags;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private List<Contributor> contributors;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private Contributor owner;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private Stats stats;
}

View File

@ -1,5 +1,7 @@
package run.halo.app.content;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import lombok.Data;
@ -15,15 +17,15 @@ import run.halo.app.core.extension.content.SinglePage;
@Data
public class ListedSinglePage {
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private SinglePage page;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private List<Contributor> contributors;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private Contributor owner;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private Stats stats;
}

View File

@ -1,5 +1,7 @@
package run.halo.app.content;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import run.halo.app.core.extension.content.Post;
import run.halo.app.extension.Ref;
@ -8,8 +10,8 @@ import run.halo.app.extension.Ref;
* @author guqing
* @since 2.0.0
*/
public record PostRequest(@Schema(required = true) Post post,
@Schema(required = true) Content content) {
public record PostRequest(@Schema(requiredMode = REQUIRED) Post post,
@Schema(requiredMode = REQUIRED) Content content) {
public ContentRequest contentRequest() {
Ref subjectRef = Ref.of(post);

View File

@ -1,5 +1,7 @@
package run.halo.app.content;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import run.halo.app.core.extension.content.SinglePage;
import run.halo.app.extension.Ref;
@ -10,8 +12,8 @@ import run.halo.app.extension.Ref;
* @author guqing
* @since 2.0.0
*/
public record SinglePageRequest(@Schema(required = true) SinglePage page,
@Schema(required = true) Content content) {
public record SinglePageRequest(@Schema(requiredMode = REQUIRED) SinglePage page,
@Schema(requiredMode = REQUIRED) Content content) {
public ContentRequest contentRequest() {
Ref subjectRef = Ref.of(page);

View File

@ -1,5 +1,7 @@
package run.halo.app.content.comment;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.UUID;
import lombok.Data;
@ -16,15 +18,15 @@ import run.halo.app.extension.Ref;
@Data
public class CommentRequest {
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private Ref subjectRef;
private CommentEmailOwner owner;
@Schema(required = true, minLength = 1)
@Schema(requiredMode = REQUIRED, minLength = 1)
private String raw;
@Schema(required = true, minLength = 1)
@Schema(requiredMode = REQUIRED, minLength = 1)
private String content;
@Schema(defaultValue = "false")

View File

@ -1,5 +1,7 @@
package run.halo.app.content.comment;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
@ -16,14 +18,14 @@ import run.halo.app.extension.Extension;
@Builder
public class ListedComment {
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private Comment comment;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private OwnerInfo owner;
private Extension subject;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private CommentStats stats;
}

View File

@ -1,5 +1,7 @@
package run.halo.app.content.comment;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
@ -15,12 +17,12 @@ import run.halo.app.core.extension.content.Reply;
@Builder
public class ListedReply {
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private Reply reply;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private OwnerInfo owner;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private CommentStats stats;
}

View File

@ -1,5 +1,7 @@
package run.halo.app.content.comment;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.UUID;
import lombok.Data;
@ -15,10 +17,10 @@ import run.halo.app.extension.Metadata;
@Data
public class ReplyRequest {
@Schema(required = true, minLength = 1)
@Schema(requiredMode = REQUIRED, minLength = 1)
private String raw;
@Schema(required = true, minLength = 1)
@Schema(requiredMode = REQUIRED, minLength = 1)
private String content;
@Schema(defaultValue = "false")

View File

@ -1,5 +1,6 @@
package run.halo.app.core.extension.attachment.endpoint;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static java.util.Comparator.comparing;
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
@ -274,10 +275,10 @@ public class AttachmentEndpoint implements CustomEndpoint {
public interface IUploadRequest {
@Schema(required = true, description = "Attachment file")
@Schema(requiredMode = REQUIRED, description = "Attachment file")
FilePart getFile();
@Schema(required = true, description = "Storage policy name")
@Schema(requiredMode = REQUIRED, description = "Storage policy name")
String getPolicyName();
@Schema(description = "The name of the group to which the attachment belongs")

View File

@ -429,7 +429,7 @@ public class UserEndpoint implements CustomEndpoint {
}
record ChangePasswordRequest(
@Schema(description = "New password.", required = true, minLength = 6)
@Schema(description = "New password.", requiredMode = REQUIRED, minLength = 6)
String password) {
}
@ -522,8 +522,8 @@ public class UserEndpoint implements CustomEndpoint {
.flatMapIterable(Function.identity());
}
record UserPermission(@Schema(required = true) Set<Role> roles,
@Schema(required = true) Set<String> uiPermissions) {
record UserPermission(@Schema(requiredMode = REQUIRED) Set<Role> roles,
@Schema(requiredMode = REQUIRED) Set<String> uiPermissions) {
}
public class ListRequest extends IListRequest.QueryListRequest {

View File

@ -3,7 +3,6 @@ package run.halo.app.core.extension.reconciler;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Instant;
import java.util.HashSet;
import java.util.List;
@ -28,11 +27,10 @@ import run.halo.app.extension.controller.Reconciler.Request;
import run.halo.app.infra.Condition;
import run.halo.app.infra.ConditionStatus;
import run.halo.app.infra.SystemVersionSupplier;
import run.halo.app.infra.ThemeRootGetter;
import run.halo.app.infra.exception.ThemeUninstallException;
import run.halo.app.infra.properties.HaloProperties;
import run.halo.app.infra.utils.JsonUtils;
import run.halo.app.infra.utils.VersionUtils;
import run.halo.app.theme.ThemePathPolicy;
/**
* Reconciler for theme.
@ -45,7 +43,8 @@ public class ThemeReconciler implements Reconciler<Request> {
private static final String FINALIZER_NAME = "theme-protection";
private final ExtensionClient client;
private final ThemePathPolicy themePathPolicy;
private final ThemeRootGetter themeRoot;
private final SystemVersionSupplier systemVersionSupplier;
private final RetryTemplate retryTemplate = RetryTemplate.builder()
@ -54,10 +53,10 @@ public class ThemeReconciler implements Reconciler<Request> {
.retryOn(IllegalStateException.class)
.build();
public ThemeReconciler(ExtensionClient client, HaloProperties haloProperties,
public ThemeReconciler(ExtensionClient client, ThemeRootGetter themeRoot,
SystemVersionSupplier systemVersionSupplier) {
this.client = client;
themePathPolicy = new ThemePathPolicy(haloProperties.getWorkDir());
this.themeRoot = themeRoot;
this.systemVersionSupplier = systemVersionSupplier;
}
@ -90,7 +89,7 @@ public class ThemeReconciler implements Reconciler<Request> {
final Theme.ThemeStatus oldStatus = JsonUtils.deepCopy(status);
theme.setStatus(status);
Path themePath = themePathPolicy.generate(theme);
var themePath = themeRoot.get().resolve(name);
status.setLocation(themePath.toAbsolutePath().toString());
status.setPhase(Theme.ThemePhase.READY);
@ -216,7 +215,7 @@ public class ThemeReconciler implements Reconciler<Request> {
}
private void deleteThemeFiles(Theme theme) {
Path themeDir = themePathPolicy.generate(theme);
var themeDir = themeRoot.get().resolve(theme.getMetadata().getName());
try {
FileSystemUtils.deleteRecursively(themeDir);
} catch (IOException e) {

View File

@ -3,14 +3,10 @@ package run.halo.app.core.extension.service;
import static run.halo.app.extension.MetadataUtil.nullSafeLabels;
import com.fasterxml.jackson.core.type.TypeReference;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import lombok.extern.slf4j.Slf4j;
@ -19,7 +15,6 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Role;
import run.halo.app.core.extension.RoleBinding;
import run.halo.app.core.extension.RoleBinding.RoleRef;
@ -42,17 +37,6 @@ public class DefaultRoleService implements RoleService {
this.extensionClient = extensionClient;
}
@Override
@NonNull
public Role getRole(@NonNull String name) {
return extensionClient.fetch(Role.class, name).blockOptional().orElseThrow();
}
@Override
public Mono<Role> getMonoRole(String name) {
return extensionClient.get(Role.class, name);
}
@Override
public Flux<RoleRef> listRoleRefs(Subject subject) {
return extensionClient.list(RoleBinding.class,
@ -61,39 +45,6 @@ public class DefaultRoleService implements RoleService {
.map(RoleBinding::getRoleRef);
}
@Override
@NonNull
public List<Role> listDependencies(Set<String> names) {
List<Role> result = new ArrayList<>();
if (names == null) {
return result;
}
Set<String> visited = new HashSet<>();
Deque<String> queue = new ArrayDeque<>(names);
while (!queue.isEmpty()) {
String roleName = queue.poll();
// detecting cycle in role dependencies
if (visited.contains(roleName)) {
log.warn("Detected a cycle in role dependencies: {},and skipped automatically",
roleName);
continue;
}
visited.add(roleName);
extensionClient.fetch(Role.class, roleName)
.blockOptional()
.ifPresent(role -> {
result.add(role);
Map<String, String> annotations = role.getMetadata().getAnnotations();
if (annotations != null) {
String roleNameDependencies = annotations.get(Role.ROLE_DEPENDENCIES_ANNO);
List<String> roleDependencies = stringToList(roleNameDependencies);
queue.addAll(roleDependencies);
}
});
}
return result;
}
@Override
public Flux<Role> listDependenciesFlux(Set<String> names) {
if (names == null) {

View File

@ -1,10 +1,7 @@
package run.halo.app.core.extension.service;
import java.util.List;
import java.util.Set;
import org.springframework.lang.NonNull;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Role;
import run.halo.app.core.extension.RoleBinding.RoleRef;
import run.halo.app.core.extension.RoleBinding.Subject;
@ -15,16 +12,8 @@ import run.halo.app.core.extension.RoleBinding.Subject;
*/
public interface RoleService {
@NonNull
@Deprecated
Role getRole(String name);
Mono<Role> getMonoRole(String name);
Flux<RoleRef> listRoleRefs(Subject subject);
List<Role> listDependencies(Set<String> names);
Flux<Role> listDependenciesFlux(Set<String> names);
Flux<Role> list(Set<String> roleNames);

View File

@ -410,7 +410,7 @@ public class ThemeEndpoint implements CustomEndpoint {
public interface IUpgradeRequest {
@Schema(required = true, description = "Theme zip file.")
@Schema(requiredMode = REQUIRED, description = "Theme zip file.")
FilePart getFile();
}
@ -501,7 +501,7 @@ public class ThemeEndpoint implements CustomEndpoint {
@Schema(name = "ThemeInstallRequest")
public record InstallRequest(
@Schema(required = true, description = "Theme zip file.") FilePart file) {
@Schema(requiredMode = REQUIRED, description = "Theme zip file.") FilePart file) {
}
public record InstallFromUriRequest(@Schema(requiredMode = REQUIRED) URI uri) {

View File

@ -102,6 +102,7 @@ public class ReactiveExtensionClientImpl implements ReactiveExtensionClient {
}
@Override
@SuppressWarnings("unchecked")
public <E extends Extension> Mono<E> create(E extension) {
return Mono.just(extension)
.doOnNext(ext -> {
@ -133,6 +134,7 @@ public class ReactiveExtensionClientImpl implements ReactiveExtensionClient {
}
@Override
@SuppressWarnings("unchecked")
public <E extends Extension> Mono<E> update(E extension) {
// Refactor the atomic reference if we have a better solution.
final var statusChangeOnly = new AtomicBoolean(false);
@ -181,6 +183,7 @@ public class ReactiveExtensionClientImpl implements ReactiveExtensionClient {
}
@Override
@SuppressWarnings("unchecked")
public <E extends Extension> Mono<E> delete(E extension) {
// set deletionTimestamp
extension.getMetadata().setDeletionTimestamp(Instant.now());

View File

@ -1,169 +0,0 @@
package run.halo.app.metrics;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicInteger;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import reactor.core.Disposable;
import run.halo.app.infra.properties.HaloProperties;
import run.halo.app.infra.utils.FileUtils;
/**
* Visit log writer.
*
* @author guqing
* @since 2.0.0
*/
@Slf4j
@Deprecated
public class VisitLogWriter implements InitializingBean, DisposableBean {
private static final String LOG_FILE_NAME = "visits.log";
private static final String LOG_FILE_LOCATION = "logs";
private final AsyncLogWriter asyncLogWriter;
private volatile boolean interruptThread = false;
private volatile boolean started = false;
private final ExecutorService executorService;
private final Path logFilePath;
public VisitLogWriter(HaloProperties haloProperties) throws IOException {
Path logsPath = haloProperties.getWorkDir()
.resolve(LOG_FILE_LOCATION);
if (!Files.exists(logsPath)) {
Files.createDirectories(logsPath);
}
this.logFilePath = logsPath.resolve(LOG_FILE_NAME);
this.asyncLogWriter = new AsyncLogWriter(logFilePath);
this.executorService = Executors.newFixedThreadPool(1);
}
public void log(String logMsg) {
asyncLogWriter.put(logMsg);
}
public Path getLogFilePath() {
return logFilePath;
}
void start() {
if (started) {
return;
}
log.debug("Starting write visit log...");
this.started = true;
executorService.submit(() -> {
while (!interruptThread && !Thread.currentThread().isInterrupted()) {
try {
asyncLogWriter.writeLog();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("VisitLogWrite thread [{}] interrupted",
Thread.currentThread().getName());
}
}
});
}
@Override
public void afterPropertiesSet() throws Exception {
start();
}
public boolean isStarted() {
return started;
}
public long queuedSize() {
return asyncLogWriter.logQueue.size();
}
@Override
public void destroy() throws Exception {
this.started = false;
interruptThread = true;
asyncLogWriter.dispose();
executorService.shutdownNow();
}
static class AsyncLogWriter implements Disposable {
private static final int MAX_LOG_SIZE = 10000;
private static final int BATCH_SIZE = 10;
private final BufferedOutputStream writer;
private final BlockingQueue<String> logQueue;
private final AtomicInteger logBatch = new AtomicInteger(0);
private volatile boolean disposed = false;
public AsyncLogWriter(Path logFilePath) {
OutputStream outputStream;
try {
outputStream = Files.newOutputStream(logFilePath,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
} catch (IOException e) {
throw new RuntimeException(e);
}
this.writer = new BufferedOutputStream(outputStream);
this.logQueue = new LinkedBlockingDeque<>(MAX_LOG_SIZE);
}
public void writeLog() throws InterruptedException {
String logMessage = logQueue.take();
if (StringUtils.isBlank(logMessage)) {
return;
}
writeToDisk(logMessage);
log.debug("Consumption visit log message: [{}]", logMessage);
}
void writeToDisk(String logMsg) {
String format = String.format("%s %s\n", Instant.now(), logMsg);
try {
writer.write(format.getBytes(), 0, format.length());
int size = logBatch.incrementAndGet();
if (size >= BATCH_SIZE) {
writer.flush();
logBatch.set(0);
}
} catch (IOException e) {
log.warn("Record access log failure: ", ExceptionUtils.getRootCause(e));
}
}
public void put(String logMessage) {
// add log message to queue tail
logQueue.add(logMessage);
log.debug("Production a log messages [{}]", logMessage);
}
@Override
public void dispose() {
this.disposed = true;
if (writer != null) {
try {
writer.flush();
} catch (IOException e) {
// ignore this
}
FileUtils.closeQuietly(writer);
}
}
@Override
public boolean isDisposed() {
return this.disposed;
}
}
}

View File

@ -1,5 +1,7 @@
package run.halo.app.search.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -15,7 +17,7 @@ import run.halo.app.extension.Ref;
plural = "searchengines", singular = "searchengine")
public class SearchEngine extends AbstractExtension {
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private SearchEngineSpec spec;
@Data
@ -25,7 +27,7 @@ public class SearchEngine extends AbstractExtension {
private String website;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private String displayName;
private String description;

View File

@ -1,9 +1,9 @@
package run.halo.app.security;
import static org.springframework.security.web.server.csrf.CookieServerCsrfTokenRepository.withHttpOnlyFalse;
import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.csrf.CookieServerCsrfTokenRepository;
import org.springframework.security.web.server.csrf.CsrfWebFilter;
import org.springframework.security.web.server.csrf.ServerCsrfTokenRequestAttributeHandler;
import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
@ -20,12 +20,12 @@ public class CsrfConfigurer implements SecurityConfigurer {
CsrfWebFilter.DEFAULT_CSRF_MATCHER,
new NegatedServerWebExchangeMatcher(pathMatchers("/api/**", "/apis/**")
));
http.csrf().csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())
http.csrf(csrfSpec -> csrfSpec
.csrfTokenRepository(withHttpOnlyFalse())
// TODO Use XorServerCsrfTokenRequestAttributeHandler instead when console implements
// the algorithm
.csrfTokenRequestHandler(new ServerCsrfTokenRequestAttributeHandler())
.requireCsrfProtectionMatcher(csrfMatcher);
.requireCsrfProtectionMatcher(csrfMatcher));
}
}

View File

@ -9,28 +9,5 @@ import reactor.core.publisher.Mono;
*/
public interface AuthorizationRuleResolver {
/**
* rulesFor returns the list of rules that apply to a given user.
* If an error is returned, the slice of PolicyRules may not be complete,
* but it contains all retrievable rules.
* This is done because policy rules are purely additive and policy determinations
* can be made on the basis of those rules that are found.
*
* @param user authenticated user info
*/
@Deprecated(forRemoval = true, since = "2.0.0")
PolicyRuleList rulesFor(UserDetails user);
/**
* visitRulesFor invokes visitor() with each rule that applies to a given user
* and each error encountered resolving those rules. Rule may be null if err is non-nil.
* If visitor() returns false, visiting is short-circuited.
*
* @param user user info
* @param visitor visitor
*/
@Deprecated(forRemoval = true, since = "2.0.0")
void visitRulesFor(UserDetails user, RuleAccumulator visitor);
Mono<AuthorizingVisitor> visitRules(UserDetails user, RequestInfo requestInfo);
}

View File

@ -1,27 +1,17 @@
package run.halo.app.security.authorization;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Role;
import run.halo.app.core.extension.service.DefaultRoleBindingService;
import run.halo.app.core.extension.service.RoleBindingService;
import run.halo.app.core.extension.service.RoleService;
import run.halo.app.extension.MetadataOperator;
import run.halo.app.infra.AnonymousUserConst;
import run.halo.app.infra.utils.JsonUtils;
/**
* @author guqing
@ -33,57 +23,11 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
private static final String AUTHENTICATED_ROLE = "authenticated";
private RoleService roleService;
private RoleBindingService roleBindingService = new DefaultRoleBindingService();
private RoleBindingService roleBindingService;
public DefaultRuleResolver(RoleService roleService) {
this.roleService = roleService;
}
@Override
public PolicyRuleList rulesFor(UserDetails user) {
PolicyRuleList policyRules = new PolicyRuleList();
visitRulesFor(user, (source, rule, err) -> {
if (rule != null) {
policyRules.add(rule);
}
if (err != null) {
policyRules.addError(err);
}
return true;
});
return policyRules;
}
@Override
public void visitRulesFor(UserDetails user, RuleAccumulator visitor) {
Set<String> roleNamesImmutable =
roleBindingService.listBoundRoleNames(user.getAuthorities());
Set<String> roleNames = new HashSet<>(roleNamesImmutable);
if (!AnonymousUserConst.PRINCIPAL.equals(user.getUsername())) {
roleNames.add(AUTHENTICATED_ROLE);
roleNames.add(AnonymousUserConst.Role);
}
List<Role.PolicyRule> rules = Collections.emptyList();
for (String roleName : roleNames) {
try {
Role role = roleService.getRole(roleName);
// fetch rules from role
rules = fetchRules(role);
} catch (Exception e) {
if (visitor.visit(null, null, e)) {
// if visitor returns true, we continue visiting
continue;
}
}
String source = roleBindingDescriber(roleName, user.getUsername());
for (Role.PolicyRule rule : rules) {
if (!visitor.visit(source, rule, null)) {
return;
}
}
}
this.roleBindingService = new DefaultRoleBindingService();
}
@Override
@ -123,32 +67,6 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
.then(Mono.just(visitor));
}
private List<Role.PolicyRule> fetchRules(Role role) {
MetadataOperator metadata = role.getMetadata();
if (metadata == null || metadata.getAnnotations() == null) {
return role.getRules();
}
// merge policy rules
String roleDependencyRules = metadata.getAnnotations()
.get(Role.ROLE_DEPENDENCY_RULES);
List<Role.PolicyRule> rules = convertFrom(roleDependencyRules);
rules.addAll(role.getRules());
return rules;
}
private List<Role.PolicyRule> convertFrom(String json) {
if (StringUtils.isBlank(json)) {
return new ArrayList<>();
}
try {
return JsonUtils.DEFAULT_JSON_MAPPER.readValue(json, new TypeReference<>() {
});
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
String roleBindingDescriber(String roleName, String subject) {
return String.format("Binding role [%s] to [%s]", roleName, subject);
}

View File

@ -1,34 +0,0 @@
package run.halo.app.theme;
import java.nio.file.Path;
import org.springframework.util.Assert;
import run.halo.app.core.extension.Theme;
/**
* Policy for generating theme directory path.
*
* @author guqing
* @since 2.0.0
* @deprecated Use {@code run.halo.app.infra.ThemeRootGetter}
*/
@Deprecated(forRemoval = true)
public class ThemePathPolicy {
public static final String THEME_WORK_DIR = "themes";
private final Path workDir;
public ThemePathPolicy(Path workDir) {
Assert.notNull(workDir, "The halo workDir must not be null.");
this.workDir = workDir;
}
public Path generate(Theme theme) {
Assert.notNull(theme, "The theme must not be null.");
String name = theme.getMetadata().getName();
return themesDir().resolve(name);
}
public Path themesDir() {
return workDir.resolve(ThemePathPolicy.THEME_WORK_DIR);
}
}

View File

@ -1,5 +1,7 @@
package run.halo.app.theme.finders.vo;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
@ -21,16 +23,16 @@ import run.halo.app.extension.MetadataOperator;
@EqualsAndHashCode
public class ReplyVo implements ExtensionVoOperator {
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private MetadataOperator metadata;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private Reply.ReplySpec spec;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private OwnerInfo owner;
@Schema(required = true)
@Schema(requiredMode = REQUIRED)
private CommentStatsVo stats;
/**

View File

@ -5,7 +5,6 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
@ -25,7 +24,6 @@ import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Role;
import run.halo.app.core.extension.service.RoleService;
import run.halo.app.extension.ExtensionClient;
@ -60,7 +58,6 @@ class ExtensionConfigurationTest {
role.setMetadata(new Metadata());
role.getMetadata().setName("supper-role");
role.setRules(List.of(rule));
when(roleService.getMonoRole(anyString())).thenReturn(Mono.just(role));
when(roleService.listDependenciesFlux(anySet())).thenReturn(Flux.just(role));
// register scheme
schemeManager.register(FakeExtension.class);

View File

@ -17,13 +17,11 @@ import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Role;
import run.halo.app.core.extension.content.Post;
import run.halo.app.core.extension.service.RoleService;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.MetadataOperator;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.utils.JsonUtils;
/**
@ -44,9 +42,6 @@ public class PostIntegrationTests {
@MockBean
RoleService roleService;
@Autowired
ReactiveExtensionClient client;
@BeforeEach
void setUp() {
var rule = new Role.PolicyRule.Builder()
@ -58,7 +53,6 @@ public class PostIntegrationTests {
role.setMetadata(new Metadata());
role.getMetadata().setName("super-role");
role.setRules(List.of(rule));
when(roleService.getMonoRole("authenticated")).thenReturn(Mono.just(role));
when(roleService.listDependenciesFlux(anySet())).thenReturn(Flux.just(role));
webTestClient = webTestClient.mutateWith(csrf());
}

View File

@ -81,17 +81,8 @@ class UserEndpointTest {
@BeforeEach
void setUp() {
// disable authorization
var rule = new Role.PolicyRule.Builder()
.apiGroups("*")
.resources("*")
.verbs("*")
.build();
var role = new Role();
role.setRules(List.of(rule));
when(roleService.getMonoRole("authenticated")).thenReturn(Mono.just(role));
webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint())
.build();
webClient = webClient.mutateWith(csrf());
webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint()).build()
.mutateWith(csrf());
}
@Nested

View File

@ -19,11 +19,12 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.json.JSONException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.stubbing.Answer;
@ -40,9 +41,8 @@ import run.halo.app.extension.Metadata;
import run.halo.app.extension.MetadataOperator;
import run.halo.app.extension.controller.Reconciler;
import run.halo.app.infra.SystemVersionSupplier;
import run.halo.app.infra.properties.HaloProperties;
import run.halo.app.infra.ThemeRootGetter;
import run.halo.app.infra.utils.JsonUtils;
import run.halo.app.theme.ThemePathPolicy;
/**
* Tests for {@link ThemeReconciler}.
@ -57,37 +57,31 @@ class ThemeReconcilerTest {
private ExtensionClient extensionClient;
@Mock
private HaloProperties haloProperties;
private SystemVersionSupplier systemVersionSupplier;
@Mock
private SystemVersionSupplier systemVersionSupplier;
ThemeRootGetter themeRoot;
@Mock
private File defaultTheme;
@InjectMocks
ThemeReconciler themeReconciler;
@TempDir
private Path tempDirectory;
@BeforeEach
void setUp() throws IOException {
tempDirectory = Files.createTempDirectory("halo-theme-");
defaultTheme = ResourceUtils.getFile("classpath:themes/default");
lenient().when(systemVersionSupplier.get()).thenReturn(Version.valueOf("0.0.0"));
}
@AfterEach
void tearDown() throws IOException {
FileSystemUtils.deleteRecursively(tempDirectory);
}
@Test
void reconcileDelete() throws IOException {
Path testWorkDir = tempDirectory.resolve("reconcile-delete");
Files.createDirectory(testWorkDir);
when(haloProperties.getWorkDir()).thenReturn(testWorkDir);
final ThemeReconciler themeReconciler =
new ThemeReconciler(extensionClient, haloProperties, systemVersionSupplier);
final ThemePathPolicy themePathPolicy = new ThemePathPolicy(testWorkDir);
when(themeRoot.get()).thenReturn(testWorkDir);
Theme theme = new Theme();
Metadata metadata = new Metadata();
@ -100,7 +94,7 @@ class ThemeReconcilerTest {
themeSpec.setSettingName("theme-test-setting");
theme.setSpec(themeSpec);
Path defaultThemePath = themePathPolicy.generate(theme);
Path defaultThemePath = testWorkDir.resolve("theme-test");
// copy to temp directory
FileSystemUtils.copyRecursively(defaultTheme.toPath(), defaultThemePath);
@ -130,10 +124,10 @@ class ThemeReconcilerTest {
final MetadataOperator metadata = theme.getMetadata();
Path testWorkDir = tempDirectory.resolve("reconcile-delete");
when(haloProperties.getWorkDir()).thenReturn(testWorkDir);
when(themeRoot.get()).thenReturn(testWorkDir);
final ThemeReconciler themeReconciler =
new ThemeReconciler(extensionClient, haloProperties, systemVersionSupplier);
new ThemeReconciler(extensionClient, themeRoot, systemVersionSupplier);
final int[] retryFlags = {0, 0};
when(extensionClient.fetch(eq(Setting.class), eq("theme-test-setting")))
@ -169,10 +163,10 @@ class ThemeReconcilerTest {
Theme theme = fakeTheme();
Path testWorkDir = tempDirectory.resolve("reconcile-delete");
when(haloProperties.getWorkDir()).thenReturn(testWorkDir);
when(themeRoot.get()).thenReturn(testWorkDir);
final ThemeReconciler themeReconciler =
new ThemeReconciler(extensionClient, haloProperties, systemVersionSupplier);
new ThemeReconciler(extensionClient, themeRoot, systemVersionSupplier);
final int[] retryFlags = {0};
when(extensionClient.fetch(eq(Setting.class), eq("theme-test-setting")))
@ -198,10 +192,10 @@ class ThemeReconcilerTest {
void reconcileStatus() {
when(systemVersionSupplier.get()).thenReturn(Version.valueOf("2.3.0"));
Path testWorkDir = tempDirectory.resolve("reconcile-delete");
when(haloProperties.getWorkDir()).thenReturn(testWorkDir);
when(themeRoot.get()).thenReturn(testWorkDir);
final ThemeReconciler themeReconciler =
new ThemeReconciler(extensionClient, haloProperties, systemVersionSupplier);
new ThemeReconciler(extensionClient, themeRoot, systemVersionSupplier);
Theme theme = fakeTheme();
theme.setStatus(null);
theme.getSpec().setRequires(">2.3.0");
@ -246,10 +240,7 @@ class ThemeReconcilerTest {
void themeSettingDefaultValue() throws IOException, JSONException {
Path testWorkDir = tempDirectory.resolve("reconcile-setting-value");
Files.createDirectory(testWorkDir);
when(haloProperties.getWorkDir()).thenReturn(testWorkDir);
final ThemeReconciler themeReconciler =
new ThemeReconciler(extensionClient, haloProperties, systemVersionSupplier);
when(themeRoot.get()).thenReturn(testWorkDir);
Theme theme = new Theme();
Metadata metadata = new Metadata();

View File

@ -1,9 +1,7 @@
package run.halo.app.core.extension.service;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.times;
@ -17,10 +15,10 @@ import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Stream;
import org.assertj.core.api.AssertionsForInterfaceTypes;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
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 reactor.core.publisher.Flux;
@ -28,7 +26,6 @@ import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import run.halo.app.core.extension.Role;
import run.halo.app.core.extension.RoleBinding;
import run.halo.app.core.extension.TestRole;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.utils.JsonUtils;
@ -44,56 +41,9 @@ class DefaultRoleServiceTest {
@Mock
private ReactiveExtensionClient extensionClient;
@InjectMocks
private DefaultRoleService roleService;
@BeforeEach
void setUp() {
roleService = new DefaultRoleService(extensionClient);
}
@Test
void listDependencies() {
Role roleManage = TestRole.getRoleManage();
Map<String, String> manageAnnotations = new HashMap<>();
manageAnnotations.put(Role.ROLE_DEPENDENCIES_ANNO, "[\"role-template-apple-view\"]");
roleManage.getMetadata().setAnnotations(manageAnnotations);
Role roleView = TestRole.getRoleView();
Map<String, String> viewAnnotations = new HashMap<>();
viewAnnotations.put(Role.ROLE_DEPENDENCIES_ANNO, "[\"role-template-apple-other\"]");
roleView.getMetadata().setAnnotations(viewAnnotations);
Role roleOther = TestRole.getRoleOther();
when(extensionClient.fetch(same(Role.class), eq("role-template-apple-manage")))
.thenReturn(Mono.just(roleManage));
when(extensionClient.fetch(same(Role.class), eq("role-template-apple-view")))
.thenReturn(Mono.just(roleView));
when(extensionClient.fetch(same(Role.class), eq("role-template-apple-other")))
.thenReturn(Mono.just(roleOther));
// list without cycle
List<Role> roles = roleService.listDependencies(Set.of("role-template-apple-manage"));
verify(extensionClient, times(1)).fetch(same(Role.class), eq("role-template-apple-manage"));
verify(extensionClient, times(1)).fetch(same(Role.class), eq("role-template-apple-view"));
verify(extensionClient, times(1)).fetch(same(Role.class), eq("role-template-apple-other"));
assertThat(roles).hasSize(3);
assertThat(roles).containsExactly(roleManage, roleView, roleOther);
// list dependencies with a cycle relation
Map<String, String> anotherAnnotations = new HashMap<>();
anotherAnnotations.put(Role.ROLE_DEPENDENCIES_ANNO, "[\"role-template-apple-view\"]");
roleOther.getMetadata().setAnnotations(anotherAnnotations);
when(extensionClient.fetch(same(Role.class), eq("role-template-apple-other")))
.thenReturn(Mono.just(roleOther));
// correct behavior is to ignore the cycle relation
List<Role> rolesFromCycle =
roleService.listDependencies(Set.of("role-template-apple-manage"));
assertThat(rolesFromCycle).hasSize(3);
}
@Nested
class ListDependenciesTest {
@Test

View File

@ -1,56 +0,0 @@
package run.halo.app.metrics;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.util.FileSystemUtils;
import run.halo.app.infra.properties.HaloProperties;
/**
* Tests for {@link VisitLogWriter}.
*
* @author guqing
* @since 2.0.0
*/
@Disabled
@Deprecated
@ExtendWith(MockitoExtension.class)
class VisitLogWriterTest {
@Mock
private HaloProperties haloProperties;
private VisitLogWriter visitLogWriter;
private Path workDir;
@BeforeEach
void setUp() throws IOException {
workDir = Files.createTempDirectory("halo-visitlog");
when(haloProperties.getWorkDir()).thenReturn(workDir);
visitLogWriter = new VisitLogWriter(haloProperties);
}
@AfterEach
void tearDown() throws Exception {
visitLogWriter.destroy();
FileSystemUtils.deleteRecursively(workDir);
}
@Test
void start() {
assertThat(visitLogWriter.isStarted()).isFalse();
visitLogWriter.start();
assertThat(visitLogWriter.isStarted()).isTrue();
}
}

View File

@ -7,7 +7,6 @@ import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@ -175,9 +174,8 @@ class BackupReconcilerTest {
backup.getSpec().setFormat("zip");
when(client.fetch(Backup.class, name)).thenReturn(Optional.of(backup));
doNothing().when(client).update(backup);
Mono<Void> mono = mock(Mono.class);
when(mono.block()).thenThrow(Exceptions.propagate(new InterruptedException()));
when(migrationService.backup(backup)).thenReturn(mono);
when(migrationService.backup(backup)).thenReturn(
Mono.error(Exceptions.propagate(new InterruptedException())));
var result = reconciler.reconcile(new Reconciler.Request(name));
@ -196,7 +194,6 @@ class BackupReconcilerTest {
verify(client, times(3)).fetch(Backup.class, name);
verify(client, times(3)).update(backup);
verify(migrationService).backup(backup);
verify(mono).block();
}
@Test
@ -206,9 +203,8 @@ class BackupReconcilerTest {
backup.getSpec().setFormat("zip");
when(client.fetch(Backup.class, name)).thenReturn(Optional.of(backup));
doNothing().when(client).update(backup);
Mono<Void> mono = mock(Mono.class);
when(mono.block()).thenThrow(Exceptions.propagate(new IOException("File not found")));
when(migrationService.backup(backup)).thenReturn(mono);
when(migrationService.backup(backup))
.thenReturn(Mono.error(Exceptions.propagate(new IOException("File not found"))));
var result = reconciler.reconcile(new Reconciler.Request(name));
@ -227,7 +223,6 @@ class BackupReconcilerTest {
verify(client, times(3)).fetch(Backup.class, name);
verify(client, times(3)).update(backup);
verify(migrationService).backup(backup);
verify(mono).block();
}
@Test

View File

@ -1,23 +0,0 @@
package run.halo.app.security;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;
import run.halo.app.security.authentication.jwt.LoginAuthenticationConverter;
public final class LoginUtils {
public static Mono<String> login(WebTestClient webClient, String username, String password) {
var request = new LoginAuthenticationConverter.UsernamePasswordRequest();
request.setUsername(username);
request.setPassword(password);
return webClient.post().uri("/api/auth/token")
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE).bodyValue(request)
.exchange().expectStatus().isOk().expectHeader().contentType(MediaType.APPLICATION_JSON)
.returnResult(ResponseMap.class)
.getResponseBody()
.single()
.map(responseMap -> responseMap.get("token").toString());
}
}

View File

@ -1,86 +0,0 @@
package run.halo.app.security.authentication.jwt;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Role;
import run.halo.app.core.extension.service.RoleService;
import run.halo.app.extension.Metadata;
import run.halo.app.infra.AnonymousUserConst;
import run.halo.app.security.LoginUtils;
@Disabled
@SpringBootTest
@AutoConfigureWebTestClient
class JwtAuthenticationTest {
@Autowired
WebTestClient webClient;
@MockBean
ReactiveUserDetailsService userDetailsService;
@MockBean
RoleService roleService;
@BeforeEach
void setUp() {
lenient().when(roleService.getMonoRole(eq(AnonymousUserConst.Role)))
.thenReturn(Mono.empty());
webClient = webClient.mutateWith(csrf());
}
@Test
void accessProtectedApiWithoutToken() {
webClient.get().uri("/api/v1/test/hello").exchange().expectStatus().isUnauthorized();
}
@Test
void accessProtectedApiUsingBearerToken() {
when(userDetailsService.findByUsername(anyString())).thenReturn(Mono.just(
User.withDefaultPasswordEncoder()
.username("username")
.password("password")
.roles("USER")
.build()
));
var role = new Role();
var metadata = new Metadata();
metadata.setName("USER");
role.setMetadata(metadata);
role.setRules(List.of(new Role.PolicyRule.Builder()
.apiGroups("")
.resources("test")
.resourceNames("hello")
.build()));
when(roleService.getMonoRole("authenticated")).thenReturn(Mono.empty());
when(roleService.getMonoRole("USER")).thenReturn(Mono.just(role));
final var token = LoginUtils.login(webClient, "username", "password").block();
webClient.get().uri("/api/v1/test/hello")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.exchange()
.expectStatus().isForbidden()
.expectHeader().value(HttpHeaders.WWW_AUTHENTICATE,
containsString("insufficient_scope"));
}
}

View File

@ -1,110 +0,0 @@
package run.halo.app.security.authentication.jwt;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
import com.nimbusds.jwt.JWTClaimNames;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;
@Disabled
@SpringBootTest
@AutoConfigureWebTestClient
class LoginTest {
@Autowired
WebTestClient webClient;
@MockBean
ReactiveUserDetailsService userDetailsService;
@BeforeEach
void setUp(@Autowired PasswordEncoder passwordEncoder) {
when(userDetailsService.findByUsername("user")).thenReturn(Mono.just(
User.builder()
.passwordEncoder(passwordEncoder::encode)
.username("user")
.password("password")
.roles("USER")
.build()
));
webClient = webClient.mutateWith(csrf());
}
@Test
void logintWithoutLoginRequest() {
webClient.post().uri("/api/auth/token").exchange().expectStatus().isUnauthorized();
}
@Test
void loginWithoutPostMethod() {
webClient.get().uri("/api/auth/token").exchange().expectStatus().isUnauthorized();
webClient.put().uri("/api/auth/token").exchange().expectStatus().isUnauthorized();
webClient.delete().uri("/api/auth/token").exchange().expectStatus().isUnauthorized();
webClient.patch().uri("/api/auth/token").exchange().expectStatus().isUnauthorized();
}
@Test
void loginWithoutApplicationJsonAcceptHeader() {
var request = new LoginAuthenticationConverter.UsernamePasswordRequest();
request.setUsername("user");
request.setPassword("invalid_password");
webClient.post().uri("/api/auth/token")
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_XML_VALUE).bodyValue(request)
.exchange().expectStatus().isUnauthorized();
}
@Test
void loginWithInvalidCredential() {
when(userDetailsService.findByUsername("user")).thenReturn(Mono.empty());
var request = new LoginAuthenticationConverter.UsernamePasswordRequest();
request.setUsername("user");
request.setPassword("invalid_password");
webClient.post().uri("/api/auth/token")
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE).bodyValue(request)
.exchange().expectStatus().isBadRequest().expectHeader()
.contentType(MediaType.APPLICATION_JSON).expectBody()
.jsonPath("$.error").value(equalTo("Invalid Credentials"));
}
@Test
void loginWithValidCredential(@Autowired ReactiveJwtDecoder jwtDecoder,
@Autowired PasswordEncoder passwordEncoder) {
when(userDetailsService.findByUsername("user")).thenReturn(Mono.just(
User.builder()
.passwordEncoder(passwordEncoder::encode)
.username("user")
.password("password")
.roles("USER")
.build()
));
var request = new LoginAuthenticationConverter.UsernamePasswordRequest();
request.setUsername("user");
request.setPassword("password");
webClient.post().uri("/api/auth/token")
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE).bodyValue(request)
.exchange().expectStatus().isOk().expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody().jsonPath("$.token").value(token -> {
var jwt = jwtDecoder.decode(token).block();
assertNotNull(jwt);
assertEquals("user", jwt.getClaim(JWTClaimNames.SUBJECT));
}, String.class);
}
}

View File

@ -1,6 +1,6 @@
package run.halo.app.security.authorization;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@ -10,12 +10,9 @@ import static org.springframework.web.reactive.function.server.RequestPredicates
import static org.springframework.web.reactive.function.server.RequestPredicates.PUT;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import static run.halo.app.extension.GroupVersionKind.fromExtension;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
@ -24,26 +21,23 @@ import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.lang.NonNull;
import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Role;
import run.halo.app.core.extension.Role.PolicyRule;
import run.halo.app.core.extension.service.RoleService;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.exception.ExtensionNotFoundException;
import run.halo.app.infra.AnonymousUserConst;
import run.halo.app.security.LoginUtils;
@Disabled
@SpringBootTest
@AutoConfigureWebTestClient
@Import(AuthorizationTest.TestConfig.class)
@ -55,6 +49,9 @@ class AuthorizationTest {
@MockBean
ReactiveUserDetailsService userDetailsService;
@MockBean
ReactiveUserDetailsPasswordService userDetailsPasswordService;
@MockBean
RoleService roleService;
@ -63,64 +60,16 @@ class AuthorizationTest {
webClient = webClient.mutateWith(csrf());
}
@Test
void accessProtectedApiWithoutSufficientRole() {
when(userDetailsService.findByUsername(eq("user"))).thenReturn(
Mono.just(User.withDefaultPasswordEncoder().username("user").password("password")
.roles("invalid-role").build()));
when(roleService.getMonoRole(any())).thenReturn(Mono.empty());
var token = LoginUtils.login(webClient, "user", "password").block();
webClient.get().uri("/apis/fake.halo.run/v1/posts")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token).exchange().expectStatus()
.isForbidden();
verify(roleService, times(1)).getMonoRole("authenticated");
verify(roleService, times(1)).getMonoRole("invalid-role");
}
@Test
void accessProtectedApiWithSufficientRole() {
when(userDetailsService.findByUsername(eq("user"))).thenReturn(Mono.just(
User.withDefaultPasswordEncoder().username("user").password("password")
.roles("post.read").build()));
when(roleService.getMonoRole(eq(AnonymousUserConst.Role)))
.thenReturn(Mono.empty());
var role = new Role();
role.setRules(List.of(
new PolicyRule.Builder().apiGroups("fake.halo.run").verbs("list").resources("posts")
.build()));
when(roleService.getMonoRole("post.read")).thenReturn(Mono.just(role));
when(roleService.getMonoRole("authenticated")).thenReturn(
Mono.error(
() -> new ExtensionNotFoundException(fromExtension(Role.class), "authenticated")));
var token = LoginUtils.login(webClient, "user", "password").block();
webClient.get().uri("/apis/fake.halo.run/v1/posts")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("returned posts");
webClient.put().uri("/apis/fake.halo.run/v1/posts/hello-halo")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token).exchange()
.expectStatus().isForbidden();
verify(roleService, times(2)).getMonoRole("authenticated");
verify(roleService, times(2)).getMonoRole("post.read");
}
@Test
void anonymousUserAccessProtectedApi() {
when(userDetailsService.findByUsername(eq(AnonymousUserConst.PRINCIPAL)))
.thenReturn(Mono.empty());
when(roleService.getMonoRole(AnonymousUserConst.Role))
.thenReturn(Mono.empty());
when(roleService.listDependenciesFlux(anySet())).thenReturn(Flux.empty());
webClient.get().uri("/apis/fake.halo.run/v1/posts").exchange().expectStatus()
.isUnauthorized();
verify(roleService, times(1)).getMonoRole(AnonymousUserConst.Role);
verify(roleService).listDependenciesFlux(anySet());
}
@Test
@ -137,25 +86,23 @@ class AuthorizationTest {
.resources("posts")
.build();
role.getRules().add(policyRule);
when(roleService.getMonoRole(AnonymousUserConst.Role))
.thenReturn(Mono.just(role));
when(roleService.listDependenciesFlux(anySet())).thenReturn(Flux.just(role));
webClient.get().uri("/apis/fake.halo.run/v1/posts").exchange().expectStatus()
.isOk()
.expectBody(String.class).isEqualTo("returned posts");
verify(roleService, times(1)).getMonoRole(AnonymousUserConst.Role);
verify(roleService).listDependenciesFlux(anySet());
webClient.get().uri("/apis/fake.halo.run/v1/posts/hello-halo").exchange()
.expectStatus()
.isUnauthorized();
verify(roleService, times(2)).getMonoRole(AnonymousUserConst.Role);
verify(roleService, times(2)).listDependenciesFlux(anySet());
}
@Test
@WithMockUser(username = "user", roles = "post.read")
void authenticatedUserAccessAuthenticationFreeApi() {
when(roleService.getMonoRole("authenticated")).thenReturn(Mono.empty());
when(roleService.getMonoRole("post.read")).thenReturn(Mono.empty());
Role role = new Role();
role.setMetadata(new Metadata());
role.getMetadata().setName(AnonymousUserConst.Role);
@ -166,11 +113,13 @@ class AuthorizationTest {
.resources("posts")
.build();
role.getRules().add(policyRule);
when(roleService.getMonoRole(AnonymousUserConst.Role))
.thenReturn(Mono.just(role));
when(roleService.listDependenciesFlux(anySet())).thenReturn(Flux.just(role));
webClient.get().uri("/apis/fake.halo.run/v1/posts").exchange().expectStatus()
.isOk()
.expectBody(String.class).isEqualTo("returned posts");
verify(roleService).listDependenciesFlux(anySet());
}
@TestConfiguration

View File

@ -0,0 +1,113 @@
package run.halo.app.security.authorization;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.mock.http.server.reactive.MockServerHttpRequest.method;
import static org.springframework.security.core.authority.AuthorityUtils.createAuthorityList;
import java.util.List;
import java.util.Set;
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.http.HttpMethod;
import org.springframework.security.core.userdetails.User;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import run.halo.app.core.extension.Role;
import run.halo.app.core.extension.Role.PolicyRule;
import run.halo.app.core.extension.service.RoleService;
import run.halo.app.extension.Metadata;
@ExtendWith(MockitoExtension.class)
class DefaultRuleResolverTest {
@Mock
RoleService roleService;
@InjectMocks
DefaultRuleResolver ruleResolver;
@Test
void visitRules() {
when(roleService.listDependenciesFlux(Set.of("authenticated", "anonymous", "ruleReadPost")))
.thenReturn(Flux.just(mockRole()));
var fakeUser = new User("admin", "123456", createAuthorityList("ruleReadPost"));
var cases = getRequestResolveCases();
cases.forEach(requestResolveCase -> {
var httpMethod = HttpMethod.valueOf(requestResolveCase.method);
var request = method(httpMethod, requestResolveCase.url).build();
var requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);
StepVerifier.create(ruleResolver.visitRules(fakeUser, requestInfo))
.assertNext(
visitor -> assertEquals(requestResolveCase.expected, visitor.isAllowed()))
.verifyComplete();
});
verify(roleService, times(cases.size())).listDependenciesFlux(
Set.of("authenticated", "anonymous", "ruleReadPost"));
}
Role mockRole() {
var role = new Role();
var rules = List.of(
new PolicyRule.Builder().apiGroups("").resources("posts").verbs("list", "get").build(),
new PolicyRule.Builder().apiGroups("").resources("categories").verbs("*").build(),
new PolicyRule.Builder().apiGroups("api.plugin.halo.run")
.resources("plugins/users")
.resourceNames("foo/bar").verbs("*").build(),
new PolicyRule.Builder().apiGroups("api.plugin.halo.run")
.resources("plugins/users")
.resourceNames("foo").verbs("*").build(),
new PolicyRule.Builder().nonResourceURLs("/healthy").verbs("get", "post", "head")
.build());
role.setRules(rules);
var metadata = new Metadata();
metadata.setName("ruleReadPost");
role.setMetadata(metadata);
return role;
}
List<RequestResolveCase> getRequestResolveCases() {
return List.of(new RequestResolveCase("/api/v1/tags", "GET", false),
new RequestResolveCase("/api/v1/tags/tagName", "GET", false),
new RequestResolveCase("/api/v1/categories/aName", "GET", true),
new RequestResolveCase("/api/v1//categories", "POST", true),
new RequestResolveCase("/api/v1/categories", "DELETE", true),
new RequestResolveCase("/api/v1/posts", "GET", true),
new RequestResolveCase("/api/v1/posts/aName", "GET", true),
new RequestResolveCase("/api/v1/posts", "DELETE", false),
new RequestResolveCase("/api/v1/posts/aName", "UPDATE", false),
// group resource url
new RequestResolveCase("/apis/group/v1/posts", "GET", false),
// plugin custom resource url
new RequestResolveCase("/apis/api.plugin.halo.run/v1alpha1/plugins/foo/users", "GET",
true),
new RequestResolveCase("/apis/api.plugin.halo.run/v1alpha1/plugins/foo/users/bar",
"GET", true),
new RequestResolveCase("/apis/api.plugin.halo.run/v1alpha1/plugins/foo/posts/bar",
"GET", false),
// non resource url
new RequestResolveCase("/healthy", "GET", true),
new RequestResolveCase("/healthy", "POST", true),
new RequestResolveCase("/healthy", "HEAD", true),
new RequestResolveCase("//healthy", "GET", false),
new RequestResolveCase("/healthy/name", "GET", false),
new RequestResolveCase("/healthy1", "GET", false),
new RequestResolveCase("//healthy//name", "GET", false),
new RequestResolveCase("/", "GET", false));
}
record RequestResolveCase(String url, String method, boolean expected) {
}
}

View File

@ -3,33 +3,19 @@ package run.halo.app.security.authorization;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.mock.http.server.reactive.MockServerHttpRequest.method;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import run.halo.app.core.extension.Role;
import run.halo.app.core.extension.Role.PolicyRule;
import run.halo.app.core.extension.service.RoleService;
import run.halo.app.extension.Metadata;
/**
* Tests for {@link RequestInfoFactory}.
*
* @author guqing
* @see RbacRequestEvaluation
* @see RequestInfo
* @see DefaultRuleResolver
* @since 2.0.0
*/
public class RequestInfoResolverTest {
@ -185,96 +171,13 @@ public class RequestInfoResolverTest {
}
}
@Test
public void defaultRuleResolverTest() {
var roleService = mock(RoleService.class);
var ruleResolver = new DefaultRuleResolver(roleService);
Role role = new Role();
List<PolicyRule> rules = List.of(
new PolicyRule.Builder().apiGroups("").resources("posts").verbs("list", "get")
.build(),
new PolicyRule.Builder().apiGroups("").resources("categories").verbs("*").build(),
new PolicyRule.Builder().apiGroups("api.plugin.halo.run").resources("plugins/users")
.resourceNames("foo/bar").verbs("*").build(),
new PolicyRule.Builder().apiGroups("api.plugin.halo.run").resources("plugins/users")
.resourceNames("foo").verbs("*").build(),
new PolicyRule.Builder().nonResourceURLs("/healthy").verbs("get", "post", "head")
.build());
role.setRules(rules);
Metadata metadata = new Metadata();
metadata.setName("ruleReadPost");
role.setMetadata(metadata);
when(roleService.getRole(anyString())).thenReturn(role);
// list bound role names
ruleResolver.setRoleBindingService(
(Collection<? extends GrantedAuthority> authorities) -> Set.of("ruleReadPost"));
User user = new User("admin", "123456", AuthorityUtils.createAuthorityList("ruleReadPost"));
// resolve user rules
List<PolicyRule> resolvedRules = ruleResolver.rulesFor(user);
assertThat(resolvedRules).isNotNull();
RbacRequestEvaluation rbacRequestEvaluation = new RbacRequestEvaluation();
for (RequestResolveCase requestResolveCase : getRequestResolveCases()) {
var request =
method(HttpMethod.valueOf(requestResolveCase.method),
requestResolveCase.url).build();
RequestInfo requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);
AttributesRecord attributes = new AttributesRecord(user, requestInfo);
boolean allowed = rbacRequestEvaluation.rulesAllow(attributes, resolvedRules);
assertThat(allowed).isEqualTo(requestResolveCase.expected);
}
}
public record NonApiCase(String url, boolean expected) {
}
public record ErrorCases(String desc, String url) {
}
List<RequestResolveCase> getRequestResolveCases() {
return List.of(new RequestResolveCase("/api/v1/tags", "GET", false),
new RequestResolveCase("/api/v1/tags/tagName", "GET", false),
new RequestResolveCase("/api/v1/categories/aName", "GET", true),
new RequestResolveCase("/api/v1//categories", "POST", true),
new RequestResolveCase("/api/v1/categories", "DELETE", true),
new RequestResolveCase("/api/v1/posts", "GET", true),
new RequestResolveCase("/api/v1/posts/aName", "GET", true),
new RequestResolveCase("/api/v1/posts", "DELETE", false),
new RequestResolveCase("/api/v1/posts/aName", "UPDATE", false),
// group resource url
new RequestResolveCase("/apis/group/v1/posts", "GET", false),
// plugin custom resource url
new RequestResolveCase("/apis/api.plugin.halo.run/v1alpha1/plugins/foo/users", "GET",
true),
new RequestResolveCase("/apis/api.plugin.halo.run/v1alpha1/plugins/foo/users/bar",
"GET", true),
new RequestResolveCase("/apis/api.plugin.halo.run/v1alpha1/plugins/foo/posts/bar",
"GET", false),
// non resource url
new RequestResolveCase("/healthy", "GET", true),
new RequestResolveCase("/healthy", "POST", true),
new RequestResolveCase("/healthy", "HEAD", true),
new RequestResolveCase("//healthy", "GET", false),
new RequestResolveCase("/healthy/name", "GET", false),
new RequestResolveCase("/healthy1", "GET", false),
new RequestResolveCase("//healthy//name", "GET", false),
new RequestResolveCase("/", "GET", false));
}
public record RequestResolveCase(String url, String method, boolean expected) {
}
public record SuccessCase(String method, String url, String expectedVerb,
String expectedAPIPrefix, String expectedAPIGroup,