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

View File

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

View File

@ -1,5 +1,6 @@
package run.halo.app.core.extension; 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 java.util.Arrays.compare;
import static run.halo.app.core.extension.Role.GROUP; import static run.halo.app.core.extension.Role.GROUP;
import static run.halo.app.core.extension.Role.KIND; 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 VERSION = "v1alpha1";
public static final String KIND = "Role"; public static final String KIND = "Role";
@Schema(required = true) @Schema(requiredMode = REQUIRED)
List<PolicyRule> rules; List<PolicyRule> rules;
/** /**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,7 @@
package run.halo.app.extension; 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.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -13,7 +15,7 @@ import org.springframework.util.StringUtils;
*/ */
public interface ExtensionOperator { public interface ExtensionOperator {
@Schema(required = true) @Schema(requiredMode = REQUIRED)
@JsonProperty("apiVersion") @JsonProperty("apiVersion")
default String getApiVersion() { default String getApiVersion() {
final var gvk = getClass().getAnnotation(GVK.class); final var gvk = getClass().getAnnotation(GVK.class);
@ -27,7 +29,7 @@ public interface ExtensionOperator {
return gvk.version(); return gvk.version();
} }
@Schema(required = true) @Schema(requiredMode = REQUIRED)
@JsonProperty("kind") @JsonProperty("kind")
default String getKind() { default String getKind() {
final var gvk = getClass().getAnnotation(GVK.class); final var gvk = getClass().getAnnotation(GVK.class);
@ -38,7 +40,7 @@ public interface ExtensionOperator {
return gvk.kind(); return gvk.kind();
} }
@Schema(required = true, implementation = Metadata.class) @Schema(requiredMode = REQUIRED, implementation = Metadata.class)
@JsonProperty("metadata") @JsonProperty("metadata")
MetadataOperator getMetadata(); MetadataOperator getMetadata();
@ -48,30 +50,6 @@ public interface ExtensionOperator {
void setMetadata(MetadataOperator metadata); 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. * Sets GroupVersionKind of the Extension.
* *

View File

@ -1,5 +1,7 @@
package run.halo.app.extension; 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.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import java.util.ArrayList; 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>> { 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 " @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; private final int page;
@Schema(description = "Size of each page. If not set or equal to 0, it means no pagination.", @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; private final int size;
@Schema(description = "Total elements.", required = true) @Schema(description = "Total elements.", requiredMode = REQUIRED)
private final long total; 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; private final List<T> items;
public ListResult(int page, int size, long total, 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); 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() { public boolean isFirst() {
return !hasPrevious(); 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() { public boolean isLast() {
return !hasNext(); 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") @JsonProperty("hasNext")
public boolean hasNext() { public boolean hasNext() {
if (page <= 0) { if (page <= 0) {
@ -69,7 +74,8 @@ public class ListResult<T> implements Iterable<T>, Supplier<Stream<T>> {
return page < getTotalPages(); 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") @JsonProperty("hasPrevious")
public boolean hasPrevious() { public boolean hasPrevious() {
return page > 1; return page > 1;
@ -80,7 +86,7 @@ public class ListResult<T> implements Iterable<T>, Supplier<Stream<T>> {
return items.iterator(); return items.iterator();
} }
@Schema(description = "Indicates total pages.", required = true) @Schema(description = "Indicates total pages.", requiredMode = REQUIRED)
@JsonProperty("totalPages") @JsonProperty("totalPages")
public long getTotalPages() { public long getTotalPages() {
return size == 0 ? 1 : (total + size - 1) / size; return size == 0 ? 1 : (total + size - 1) / size;

View File

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

View File

@ -1,5 +1,7 @@
package run.halo.app.extension; 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 io.swagger.v3.oas.annotations.media.Schema;
import java.util.Objects; import java.util.Objects;
import lombok.Data; import lombok.Data;
@ -17,7 +19,7 @@ public class Ref {
@Schema(description = "Extension kind") @Schema(description = "Extension kind")
private String 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; private String name;
public static Ref of(String name) { public static Ref of(String name) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
package run.halo.app.core.extension.attachment.endpoint; 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 java.util.Comparator.comparing;
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder; import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
import static org.springdoc.core.fn.builders.content.Builder.contentBuilder; import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
@ -274,10 +275,10 @@ public class AttachmentEndpoint implements CustomEndpoint {
public interface IUploadRequest { public interface IUploadRequest {
@Schema(required = true, description = "Attachment file") @Schema(requiredMode = REQUIRED, description = "Attachment file")
FilePart getFile(); FilePart getFile();
@Schema(required = true, description = "Storage policy name") @Schema(requiredMode = REQUIRED, description = "Storage policy name")
String getPolicyName(); String getPolicyName();
@Schema(description = "The name of the group to which the attachment belongs") @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( record ChangePasswordRequest(
@Schema(description = "New password.", required = true, minLength = 6) @Schema(description = "New password.", requiredMode = REQUIRED, minLength = 6)
String password) { String password) {
} }
@ -522,8 +522,8 @@ public class UserEndpoint implements CustomEndpoint {
.flatMapIterable(Function.identity()); .flatMapIterable(Function.identity());
} }
record UserPermission(@Schema(required = true) Set<Role> roles, record UserPermission(@Schema(requiredMode = REQUIRED) Set<Role> roles,
@Schema(required = true) Set<String> uiPermissions) { @Schema(requiredMode = REQUIRED) Set<String> uiPermissions) {
} }
public class ListRequest extends IListRequest.QueryListRequest { 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 static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path;
import java.time.Instant; import java.time.Instant;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; 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.Condition;
import run.halo.app.infra.ConditionStatus; import run.halo.app.infra.ConditionStatus;
import run.halo.app.infra.SystemVersionSupplier; import run.halo.app.infra.SystemVersionSupplier;
import run.halo.app.infra.ThemeRootGetter;
import run.halo.app.infra.exception.ThemeUninstallException; 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.JsonUtils;
import run.halo.app.infra.utils.VersionUtils; import run.halo.app.infra.utils.VersionUtils;
import run.halo.app.theme.ThemePathPolicy;
/** /**
* Reconciler for theme. * Reconciler for theme.
@ -45,7 +43,8 @@ public class ThemeReconciler implements Reconciler<Request> {
private static final String FINALIZER_NAME = "theme-protection"; private static final String FINALIZER_NAME = "theme-protection";
private final ExtensionClient client; private final ExtensionClient client;
private final ThemePathPolicy themePathPolicy;
private final ThemeRootGetter themeRoot;
private final SystemVersionSupplier systemVersionSupplier; private final SystemVersionSupplier systemVersionSupplier;
private final RetryTemplate retryTemplate = RetryTemplate.builder() private final RetryTemplate retryTemplate = RetryTemplate.builder()
@ -54,10 +53,10 @@ public class ThemeReconciler implements Reconciler<Request> {
.retryOn(IllegalStateException.class) .retryOn(IllegalStateException.class)
.build(); .build();
public ThemeReconciler(ExtensionClient client, HaloProperties haloProperties, public ThemeReconciler(ExtensionClient client, ThemeRootGetter themeRoot,
SystemVersionSupplier systemVersionSupplier) { SystemVersionSupplier systemVersionSupplier) {
this.client = client; this.client = client;
themePathPolicy = new ThemePathPolicy(haloProperties.getWorkDir()); this.themeRoot = themeRoot;
this.systemVersionSupplier = systemVersionSupplier; this.systemVersionSupplier = systemVersionSupplier;
} }
@ -90,7 +89,7 @@ public class ThemeReconciler implements Reconciler<Request> {
final Theme.ThemeStatus oldStatus = JsonUtils.deepCopy(status); final Theme.ThemeStatus oldStatus = JsonUtils.deepCopy(status);
theme.setStatus(status); theme.setStatus(status);
Path themePath = themePathPolicy.generate(theme); var themePath = themeRoot.get().resolve(name);
status.setLocation(themePath.toAbsolutePath().toString()); status.setLocation(themePath.toAbsolutePath().toString());
status.setPhase(Theme.ThemePhase.READY); status.setPhase(Theme.ThemePhase.READY);
@ -216,7 +215,7 @@ public class ThemeReconciler implements Reconciler<Request> {
} }
private void deleteThemeFiles(Theme theme) { private void deleteThemeFiles(Theme theme) {
Path themeDir = themePathPolicy.generate(theme); var themeDir = themeRoot.get().resolve(theme.getMetadata().getName());
try { try {
FileSystemUtils.deleteRecursively(themeDir); FileSystemUtils.deleteRecursively(themeDir);
} catch (IOException e) { } 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 static run.halo.app.extension.MetadataUtil.nullSafeLabels;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Deque;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -19,7 +15,6 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Role; import run.halo.app.core.extension.Role;
import run.halo.app.core.extension.RoleBinding; import run.halo.app.core.extension.RoleBinding;
import run.halo.app.core.extension.RoleBinding.RoleRef; import run.halo.app.core.extension.RoleBinding.RoleRef;
@ -42,17 +37,6 @@ public class DefaultRoleService implements RoleService {
this.extensionClient = extensionClient; 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 @Override
public Flux<RoleRef> listRoleRefs(Subject subject) { public Flux<RoleRef> listRoleRefs(Subject subject) {
return extensionClient.list(RoleBinding.class, return extensionClient.list(RoleBinding.class,
@ -61,39 +45,6 @@ public class DefaultRoleService implements RoleService {
.map(RoleBinding::getRoleRef); .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 @Override
public Flux<Role> listDependenciesFlux(Set<String> names) { public Flux<Role> listDependenciesFlux(Set<String> names) {
if (names == null) { if (names == null) {

View File

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

View File

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

View File

@ -102,6 +102,7 @@ public class ReactiveExtensionClientImpl implements ReactiveExtensionClient {
} }
@Override @Override
@SuppressWarnings("unchecked")
public <E extends Extension> Mono<E> create(E extension) { public <E extends Extension> Mono<E> create(E extension) {
return Mono.just(extension) return Mono.just(extension)
.doOnNext(ext -> { .doOnNext(ext -> {
@ -133,6 +134,7 @@ public class ReactiveExtensionClientImpl implements ReactiveExtensionClient {
} }
@Override @Override
@SuppressWarnings("unchecked")
public <E extends Extension> Mono<E> update(E extension) { public <E extends Extension> Mono<E> update(E extension) {
// Refactor the atomic reference if we have a better solution. // Refactor the atomic reference if we have a better solution.
final var statusChangeOnly = new AtomicBoolean(false); final var statusChangeOnly = new AtomicBoolean(false);
@ -181,6 +183,7 @@ public class ReactiveExtensionClientImpl implements ReactiveExtensionClient {
} }
@Override @Override
@SuppressWarnings("unchecked")
public <E extends Extension> Mono<E> delete(E extension) { public <E extends Extension> Mono<E> delete(E extension) {
// set deletionTimestamp // set deletionTimestamp
extension.getMetadata().setDeletionTimestamp(Instant.now()); 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; 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 io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@ -15,7 +17,7 @@ import run.halo.app.extension.Ref;
plural = "searchengines", singular = "searchengine") plural = "searchengines", singular = "searchengine")
public class SearchEngine extends AbstractExtension { public class SearchEngine extends AbstractExtension {
@Schema(required = true) @Schema(requiredMode = REQUIRED)
private SearchEngineSpec spec; private SearchEngineSpec spec;
@Data @Data
@ -25,7 +27,7 @@ public class SearchEngine extends AbstractExtension {
private String website; private String website;
@Schema(required = true) @Schema(requiredMode = REQUIRED)
private String displayName; private String displayName;
private String description; private String description;

View File

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

View File

@ -9,28 +9,5 @@ import reactor.core.publisher.Mono;
*/ */
public interface AuthorizationRuleResolver { 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); Mono<AuthorizingVisitor> visitRules(UserDetails user, RequestInfo requestInfo);
} }

View File

@ -1,27 +1,17 @@
package run.halo.app.security.authorization; 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.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Mono; 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.DefaultRoleBindingService;
import run.halo.app.core.extension.service.RoleBindingService; import run.halo.app.core.extension.service.RoleBindingService;
import run.halo.app.core.extension.service.RoleService; 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.AnonymousUserConst;
import run.halo.app.infra.utils.JsonUtils;
/** /**
* @author guqing * @author guqing
@ -33,57 +23,11 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
private static final String AUTHENTICATED_ROLE = "authenticated"; private static final String AUTHENTICATED_ROLE = "authenticated";
private RoleService roleService; private RoleService roleService;
private RoleBindingService roleBindingService = new DefaultRoleBindingService(); private RoleBindingService roleBindingService;
public DefaultRuleResolver(RoleService roleService) { public DefaultRuleResolver(RoleService roleService) {
this.roleService = roleService; this.roleService = roleService;
} this.roleBindingService = new DefaultRoleBindingService();
@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;
}
}
}
} }
@Override @Override
@ -123,32 +67,6 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
.then(Mono.just(visitor)); .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) { String roleBindingDescriber(String roleName, String subject) {
return String.format("Binding role [%s] to [%s]", roleName, 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; 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 io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@ -21,16 +23,16 @@ import run.halo.app.extension.MetadataOperator;
@EqualsAndHashCode @EqualsAndHashCode
public class ReplyVo implements ExtensionVoOperator { public class ReplyVo implements ExtensionVoOperator {
@Schema(required = true) @Schema(requiredMode = REQUIRED)
private MetadataOperator metadata; private MetadataOperator metadata;
@Schema(required = true) @Schema(requiredMode = REQUIRED)
private Reply.ReplySpec spec; private Reply.ReplySpec spec;
@Schema(required = true) @Schema(requiredMode = REQUIRED)
private OwnerInfo owner; private OwnerInfo owner;
@Schema(required = true) @Schema(requiredMode = REQUIRED)
private CommentStatsVo stats; 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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anySet; import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf; 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.security.test.context.support.WithMockUser;
import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Role; import run.halo.app.core.extension.Role;
import run.halo.app.core.extension.service.RoleService; import run.halo.app.core.extension.service.RoleService;
import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.ExtensionClient;
@ -60,7 +58,6 @@ class ExtensionConfigurationTest {
role.setMetadata(new Metadata()); role.setMetadata(new Metadata());
role.getMetadata().setName("supper-role"); role.getMetadata().setName("supper-role");
role.setRules(List.of(rule)); role.setRules(List.of(rule));
when(roleService.getMonoRole(anyString())).thenReturn(Mono.just(role));
when(roleService.listDependenciesFlux(anySet())).thenReturn(Flux.just(role)); when(roleService.listDependenciesFlux(anySet())).thenReturn(Flux.just(role));
// register scheme // register scheme
schemeManager.register(FakeExtension.class); 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.security.test.context.support.WithMockUser;
import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Role; import run.halo.app.core.extension.Role;
import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.Post;
import run.halo.app.core.extension.service.RoleService; import run.halo.app.core.extension.service.RoleService;
import run.halo.app.extension.Metadata; import run.halo.app.extension.Metadata;
import run.halo.app.extension.MetadataOperator; import run.halo.app.extension.MetadataOperator;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.utils.JsonUtils; import run.halo.app.infra.utils.JsonUtils;
/** /**
@ -44,9 +42,6 @@ public class PostIntegrationTests {
@MockBean @MockBean
RoleService roleService; RoleService roleService;
@Autowired
ReactiveExtensionClient client;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
var rule = new Role.PolicyRule.Builder() var rule = new Role.PolicyRule.Builder()
@ -58,7 +53,6 @@ public class PostIntegrationTests {
role.setMetadata(new Metadata()); role.setMetadata(new Metadata());
role.getMetadata().setName("super-role"); role.getMetadata().setName("super-role");
role.setRules(List.of(rule)); role.setRules(List.of(rule));
when(roleService.getMonoRole("authenticated")).thenReturn(Mono.just(role));
when(roleService.listDependenciesFlux(anySet())).thenReturn(Flux.just(role)); when(roleService.listDependenciesFlux(anySet())).thenReturn(Flux.just(role));
webTestClient = webTestClient.mutateWith(csrf()); webTestClient = webTestClient.mutateWith(csrf());
} }

View File

@ -81,17 +81,8 @@ class UserEndpointTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
// disable authorization // disable authorization
var rule = new Role.PolicyRule.Builder() webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint()).build()
.apiGroups("*") .mutateWith(csrf());
.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());
} }
@Nested @Nested

View File

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

View File

@ -1,9 +1,7 @@
package run.halo.app.core.extension.service; 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.any;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.eq; import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
@ -17,10 +15,10 @@ import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.assertj.core.api.AssertionsForInterfaceTypes; import org.assertj.core.api.AssertionsForInterfaceTypes;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
@ -28,7 +26,6 @@ import reactor.core.publisher.Mono;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
import run.halo.app.core.extension.Role; import run.halo.app.core.extension.Role;
import run.halo.app.core.extension.RoleBinding; 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.Metadata;
import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.utils.JsonUtils; import run.halo.app.infra.utils.JsonUtils;
@ -44,56 +41,9 @@ class DefaultRoleServiceTest {
@Mock @Mock
private ReactiveExtensionClient extensionClient; private ReactiveExtensionClient extensionClient;
@InjectMocks
private DefaultRoleService roleService; 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 @Nested
class ListDependenciesTest { class ListDependenciesTest {
@Test @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.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@ -175,9 +174,8 @@ class BackupReconcilerTest {
backup.getSpec().setFormat("zip"); backup.getSpec().setFormat("zip");
when(client.fetch(Backup.class, name)).thenReturn(Optional.of(backup)); when(client.fetch(Backup.class, name)).thenReturn(Optional.of(backup));
doNothing().when(client).update(backup); doNothing().when(client).update(backup);
Mono<Void> mono = mock(Mono.class); when(migrationService.backup(backup)).thenReturn(
when(mono.block()).thenThrow(Exceptions.propagate(new InterruptedException())); Mono.error(Exceptions.propagate(new InterruptedException())));
when(migrationService.backup(backup)).thenReturn(mono);
var result = reconciler.reconcile(new Reconciler.Request(name)); 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)).fetch(Backup.class, name);
verify(client, times(3)).update(backup); verify(client, times(3)).update(backup);
verify(migrationService).backup(backup); verify(migrationService).backup(backup);
verify(mono).block();
} }
@Test @Test
@ -206,9 +203,8 @@ class BackupReconcilerTest {
backup.getSpec().setFormat("zip"); backup.getSpec().setFormat("zip");
when(client.fetch(Backup.class, name)).thenReturn(Optional.of(backup)); when(client.fetch(Backup.class, name)).thenReturn(Optional.of(backup));
doNothing().when(client).update(backup); doNothing().when(client).update(backup);
Mono<Void> mono = mock(Mono.class); when(migrationService.backup(backup))
when(mono.block()).thenThrow(Exceptions.propagate(new IOException("File not found"))); .thenReturn(Mono.error(Exceptions.propagate(new IOException("File not found"))));
when(migrationService.backup(backup)).thenReturn(mono);
var result = reconciler.reconcile(new Reconciler.Request(name)); 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)).fetch(Backup.class, name);
verify(client, times(3)).update(backup); verify(client, times(3)).update(backup);
verify(migrationService).backup(backup); verify(migrationService).backup(backup);
verify(mono).block();
} }
@Test @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; 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.ArgumentMatchers.eq;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; 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.PUT;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept; import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RouterFunctions.route; 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.ArrayList;
import java.util.List;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; 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.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.lang.NonNull; 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.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.test.context.support.WithMockUser; import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Role; import run.halo.app.core.extension.Role;
import run.halo.app.core.extension.Role.PolicyRule; import run.halo.app.core.extension.Role.PolicyRule;
import run.halo.app.core.extension.service.RoleService; import run.halo.app.core.extension.service.RoleService;
import run.halo.app.extension.Metadata; import run.halo.app.extension.Metadata;
import run.halo.app.extension.exception.ExtensionNotFoundException;
import run.halo.app.infra.AnonymousUserConst; import run.halo.app.infra.AnonymousUserConst;
import run.halo.app.security.LoginUtils;
@Disabled
@SpringBootTest @SpringBootTest
@AutoConfigureWebTestClient @AutoConfigureWebTestClient
@Import(AuthorizationTest.TestConfig.class) @Import(AuthorizationTest.TestConfig.class)
@ -55,6 +49,9 @@ class AuthorizationTest {
@MockBean @MockBean
ReactiveUserDetailsService userDetailsService; ReactiveUserDetailsService userDetailsService;
@MockBean
ReactiveUserDetailsPasswordService userDetailsPasswordService;
@MockBean @MockBean
RoleService roleService; RoleService roleService;
@ -63,64 +60,16 @@ class AuthorizationTest {
webClient = webClient.mutateWith(csrf()); 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 @Test
void anonymousUserAccessProtectedApi() { void anonymousUserAccessProtectedApi() {
when(userDetailsService.findByUsername(eq(AnonymousUserConst.PRINCIPAL))) when(userDetailsService.findByUsername(eq(AnonymousUserConst.PRINCIPAL)))
.thenReturn(Mono.empty()); .thenReturn(Mono.empty());
when(roleService.getMonoRole(AnonymousUserConst.Role)) when(roleService.listDependenciesFlux(anySet())).thenReturn(Flux.empty());
.thenReturn(Mono.empty());
webClient.get().uri("/apis/fake.halo.run/v1/posts").exchange().expectStatus() webClient.get().uri("/apis/fake.halo.run/v1/posts").exchange().expectStatus()
.isUnauthorized(); .isUnauthorized();
verify(roleService, times(1)).getMonoRole(AnonymousUserConst.Role); verify(roleService).listDependenciesFlux(anySet());
} }
@Test @Test
@ -137,25 +86,23 @@ class AuthorizationTest {
.resources("posts") .resources("posts")
.build(); .build();
role.getRules().add(policyRule); role.getRules().add(policyRule);
when(roleService.getMonoRole(AnonymousUserConst.Role)) when(roleService.listDependenciesFlux(anySet())).thenReturn(Flux.just(role));
.thenReturn(Mono.just(role));
webClient.get().uri("/apis/fake.halo.run/v1/posts").exchange().expectStatus() webClient.get().uri("/apis/fake.halo.run/v1/posts").exchange().expectStatus()
.isOk() .isOk()
.expectBody(String.class).isEqualTo("returned posts"); .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() webClient.get().uri("/apis/fake.halo.run/v1/posts/hello-halo").exchange()
.expectStatus() .expectStatus()
.isUnauthorized(); .isUnauthorized();
verify(roleService, times(2)).getMonoRole(AnonymousUserConst.Role);
verify(roleService, times(2)).listDependenciesFlux(anySet());
} }
@Test @Test
@WithMockUser(username = "user", roles = "post.read") @WithMockUser(username = "user", roles = "post.read")
void authenticatedUserAccessAuthenticationFreeApi() { void authenticatedUserAccessAuthenticationFreeApi() {
when(roleService.getMonoRole("authenticated")).thenReturn(Mono.empty());
when(roleService.getMonoRole("post.read")).thenReturn(Mono.empty());
Role role = new Role(); Role role = new Role();
role.setMetadata(new Metadata()); role.setMetadata(new Metadata());
role.getMetadata().setName(AnonymousUserConst.Role); role.getMetadata().setName(AnonymousUserConst.Role);
@ -166,11 +113,13 @@ class AuthorizationTest {
.resources("posts") .resources("posts")
.build(); .build();
role.getRules().add(policyRule); 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() webClient.get().uri("/apis/fake.halo.run/v1/posts").exchange().expectStatus()
.isOk() .isOk()
.expectBody(String.class).isEqualTo("returned posts"); .expectBody(String.class).isEqualTo("returned posts");
verify(roleService).listDependenciesFlux(anySet());
} }
@TestConfiguration @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.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; 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 static org.springframework.mock.http.server.reactive.MockServerHttpRequest.method;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod; 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}. * Tests for {@link RequestInfoFactory}.
* *
* @author guqing * @author guqing
* @see RbacRequestEvaluation
* @see RequestInfo * @see RequestInfo
* @see DefaultRuleResolver
* @since 2.0.0 * @since 2.0.0
*/ */
public class RequestInfoResolverTest { public class RequestInfoResolverTest {
@ -173,7 +159,7 @@ public class RequestInfoResolverTest {
List.of(new ErrorCases("api resource has name and no subresource but post", List.of(new ErrorCases("api resource has name and no subresource but post",
"/api/version/themes/install"), "/api/version/themes/install"),
new ErrorCases("apis resource has name and no subresource but post", new ErrorCases("apis resource has name and no subresource but post",
"/apis/api.halo.run/v1alpha1/themes/install")); "/apis/api.halo.run/v1alpha1/themes/install"));
for (ErrorCases errorCase : postCases) { for (ErrorCases errorCase : postCases) {
var request = var request =
method(HttpMethod.POST, errorCase.url).build(); method(HttpMethod.POST, errorCase.url).build();
@ -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 NonApiCase(String url, boolean expected) {
} }
public record ErrorCases(String desc, String url) { 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, public record SuccessCase(String method, String url, String expectedVerb,
String expectedAPIPrefix, String expectedAPIGroup, String expectedAPIPrefix, String expectedAPIGroup,