mirror of https://github.com/halo-dev/halo
feat: add yaml unstructured loader (#2122)
parent
3c856d04af
commit
9a05942bc4
|
@ -212,7 +212,7 @@ public class WebSecurityConfig {
|
|||
// It'll be deleted next time
|
||||
UserDetails user = User.withUsername("user")
|
||||
.password(passwordEncoder().encode("123456"))
|
||||
.authorities("readPostRole")
|
||||
.authorities("role-template-view-posts", "role-template-manage-posts")
|
||||
.build();
|
||||
return new InMemoryUserDetailsManager(user);
|
||||
}
|
||||
|
|
|
@ -1,25 +1,29 @@
|
|||
package run.halo.app.infra;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import javax.crypto.SecretKey;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.Schemes;
|
||||
import run.halo.app.extension.Unstructured;
|
||||
import run.halo.app.identity.apitoken.PersonalAccessToken;
|
||||
import run.halo.app.identity.apitoken.PersonalAccessTokenType;
|
||||
import run.halo.app.identity.apitoken.PersonalAccessTokenUtils;
|
||||
import run.halo.app.identity.authentication.OAuth2Authorization;
|
||||
import run.halo.app.identity.authentication.OAuth2AuthorizationService;
|
||||
import run.halo.app.identity.authorization.PolicyRule;
|
||||
import run.halo.app.identity.authorization.Role;
|
||||
import run.halo.app.infra.utils.HaloUtils;
|
||||
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
|
@ -42,30 +46,12 @@ public class SchemeInitializer implements ApplicationListener<ApplicationStarted
|
|||
public void onApplicationEvent(ApplicationStartedEvent event) {
|
||||
Schemes.INSTANCE.register(Role.class);
|
||||
|
||||
// TODO The read location of the configuration file needs to be considered later
|
||||
createUnstructured();
|
||||
|
||||
// TODO These test only methods will be removed in the future
|
||||
initRoleForTesting();
|
||||
initPersonalAccessTokenForTesting();
|
||||
}
|
||||
|
||||
|
||||
private void initRoleForTesting() {
|
||||
Role role = new Role();
|
||||
role.setApiVersion("v1alpha1");
|
||||
role.setKind("Role");
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName("readPostRole");
|
||||
role.setMetadata(metadata);
|
||||
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().nonResourceURLs("/healthy").verbs("get", "post", "head")
|
||||
.build()
|
||||
);
|
||||
role.setRules(rules);
|
||||
|
||||
extensionClient.create(role);
|
||||
}
|
||||
|
||||
private void initPersonalAccessTokenForTesting() {
|
||||
|
@ -74,7 +60,8 @@ public class SchemeInitializer implements ApplicationListener<ApplicationStarted
|
|||
String tokenValue =
|
||||
PersonalAccessTokenUtils.generate(PersonalAccessTokenType.ADMIN_TOKEN, secretKey);
|
||||
|
||||
Set<String> roles = Set.of("readPostRole");
|
||||
Set<String> roles =
|
||||
Set.of("role-template-view-categories", "role-template-view-nonresources");
|
||||
OAuth2AccessToken personalAccessToken =
|
||||
new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, tokenValue, Instant.now(),
|
||||
Instant.now().plus(2, ChronoUnit.HOURS), roles);
|
||||
|
@ -91,4 +78,29 @@ public class SchemeInitializer implements ApplicationListener<ApplicationStarted
|
|||
.build();
|
||||
oauth2AuthorizationService.save(authorization);
|
||||
}
|
||||
|
||||
private void createUnstructured() {
|
||||
try {
|
||||
List<Unstructured> unstructuredList = loadClassPathResourcesToUnstructured();
|
||||
for (Unstructured unstructured : unstructuredList) {
|
||||
extensionClient.create(unstructured);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Unstructured> loadClassPathResourcesToUnstructured() throws IOException {
|
||||
PathMatchingResourcePatternResolver resourcePatternResolver =
|
||||
new PathMatchingResourcePatternResolver();
|
||||
// Gets yaml resources
|
||||
Resource[] yamlResources =
|
||||
resourcePatternResolver.getResources("classpath*:extensions/*.yaml");
|
||||
Resource[] ymlResources =
|
||||
resourcePatternResolver.getResources("classpath*:extensions/*.yml");
|
||||
|
||||
YamlUnstructuredLoader yamlUnstructuredLoader =
|
||||
new YamlUnstructuredLoader(ArrayUtils.addAll(ymlResources, yamlResources));
|
||||
return yamlUnstructuredLoader.load();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
package run.halo.app.infra.utils;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import java.util.Map;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Json utilities.
|
||||
*
|
||||
* @author guqing
|
||||
* @see JavaTimeModule
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class JsonUtils {
|
||||
public static final ObjectMapper DEFAULT_JSON_MAPPER = createDefaultJsonMapper();
|
||||
|
||||
private JsonUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a default json mapper.
|
||||
*
|
||||
* @return object mapper
|
||||
*/
|
||||
public static ObjectMapper createDefaultJsonMapper() {
|
||||
return createDefaultJsonMapper(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a default json mapper.
|
||||
*
|
||||
* @param strategy property naming strategy
|
||||
* @return object mapper
|
||||
*/
|
||||
@NonNull
|
||||
public static ObjectMapper createDefaultJsonMapper(@Nullable PropertyNamingStrategy strategy) {
|
||||
// Create object mapper
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
// Configure
|
||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
mapper.registerModule(new JavaTimeModule());
|
||||
// Set property naming strategy
|
||||
if (strategy != null) {
|
||||
mapper.setPropertyNamingStrategy(strategy);
|
||||
}
|
||||
return mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a map to the object specified type.
|
||||
*
|
||||
* @param sourceMap source map must not be empty
|
||||
* @param type object type must not be null
|
||||
* @param <T> target object type
|
||||
* @return the object specified type
|
||||
*/
|
||||
@NonNull
|
||||
public static <T> T mapToObject(@NonNull Map<String, ?> sourceMap, @NonNull Class<T> type) {
|
||||
return DEFAULT_JSON_MAPPER.convertValue(sourceMap, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts object to json format.
|
||||
*
|
||||
* @param source source object must not be null
|
||||
* @return json format of the source object
|
||||
* @throws JsonProcessingException throws when fail to convert
|
||||
*/
|
||||
@NonNull
|
||||
public static String objectToJson(@NonNull Object source)
|
||||
throws JsonProcessingException {
|
||||
Assert.notNull(source, "Source object must not be null");
|
||||
|
||||
return DEFAULT_JSON_MAPPER.writeValueAsString(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to deserialize JSON content from given JSON content String.
|
||||
*
|
||||
* @param json json content
|
||||
* @param toValueType object type to convert
|
||||
* @param <T> real type to convert
|
||||
* @return converted object
|
||||
*/
|
||||
public static <T> T jsonToObject(String json, Class<T> toValueType) {
|
||||
try {
|
||||
return DEFAULT_JSON_MAPPER.readValue(json, toValueType);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package run.halo.app.infra.utils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.config.YamlProcessor;
|
||||
import org.springframework.core.io.Resource;
|
||||
import run.halo.app.extension.Unstructured;
|
||||
|
||||
/**
|
||||
* <p>Process the content in yaml that matches the {@link DocumentMatcher} and convert it to an
|
||||
* unstructured list.</p>
|
||||
* <p>Multiple resources can be processed at one time.</p>
|
||||
* <p>The following specified key must be included before the resource can be processed:
|
||||
* <pre>
|
||||
* apiVersion
|
||||
* kind
|
||||
* metadata.name
|
||||
* </pre>
|
||||
* Otherwise, skip it and continue to read the next resource.
|
||||
* </p>
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class YamlUnstructuredLoader extends YamlProcessor {
|
||||
|
||||
private static final DocumentMatcher DEFAULT_UNSTRUCTURED_MATCHER = properties -> {
|
||||
if (properties.containsKey("apiVersion")
|
||||
&& properties.containsKey("kind")
|
||||
&& properties.containsKey("metadata.name")) {
|
||||
return YamlProcessor.MatchStatus.FOUND;
|
||||
}
|
||||
return MatchStatus.NOT_FOUND;
|
||||
};
|
||||
|
||||
public YamlUnstructuredLoader(Resource... resources) {
|
||||
setResources(resources);
|
||||
setDocumentMatchers(DEFAULT_UNSTRUCTURED_MATCHER);
|
||||
}
|
||||
|
||||
public List<Unstructured> load() {
|
||||
List<Unstructured> unstructuredList = new ArrayList<>();
|
||||
process((properties, map) -> {
|
||||
Unstructured unstructured = JsonUtils.mapToObject(map, Unstructured.class);
|
||||
unstructuredList.add(unstructured);
|
||||
});
|
||||
return unstructuredList;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
apiVersion: v1alpha1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: role-template-view-categories
|
||||
labels:
|
||||
halo.run/role-template: "true"
|
||||
annotations:
|
||||
halo.run/module: "Categories Management"
|
||||
halo.run/alias-name: "Categories View"
|
||||
rules:
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ "categories" ]
|
||||
verbs: [ "get", "list" ]
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: role-template-manage-categories
|
||||
labels:
|
||||
halo.run/role-template: "true"
|
||||
annotations:
|
||||
halo.run/dependencies: '[ "role-template-view-categories" ]'
|
||||
halo.run/module: "Categories Management"
|
||||
halo.run/alias-name: "Categories Management"
|
||||
rules:
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ "categories" ]
|
||||
verbs: [ "create", "delete", "deletecollection", "patch", "update" ]
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: role-template-view-posts
|
||||
labels:
|
||||
halo.run/role-template: "true"
|
||||
annotations:
|
||||
halo.run/module: "Posts Management"
|
||||
halo.run/alias-name: "Posts View"
|
||||
rules:
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ "posts", "categories", "tags" ]
|
||||
verbs: [ "get", "list" ]
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: role-template-manage-posts
|
||||
labels:
|
||||
halo.run/role-template: "true"
|
||||
annotations:
|
||||
halo.run/module: "Posts Management"
|
||||
halo.run/alias-name: "Posts Management"
|
||||
rules:
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ "posts", "categories", "tags" ]
|
||||
verbs: [ "create", "delete", "deletecollection", "patch", "update" ]
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: role-template-view-nonresources
|
||||
labels:
|
||||
halo.run/role-template: "true"
|
||||
annotations:
|
||||
halo.run/module: "Other"
|
||||
halo.run/alias-name: "Non Resources View"
|
||||
rules:
|
||||
- nonResourceURLs: ["/healthy", "/static/*"]
|
||||
verbs: ["get", "post"]
|
|
@ -0,0 +1,36 @@
|
|||
package run.halo.app.infra.utils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests for {@link JsonUtils}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class JsonUtilsTest {
|
||||
|
||||
@Test
|
||||
public void serializerTime() throws JsonProcessingException {
|
||||
Instant now = Instant.now();
|
||||
String instantStr = JsonUtils.objectToJson(now);
|
||||
assertThat(instantStr).isNotNull();
|
||||
|
||||
String localDateTimeStr = JsonUtils.objectToJson(LocalDateTime.now());
|
||||
assertThat(localDateTimeStr).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("rawtypes")
|
||||
public void deserializerArrayString() {
|
||||
String s = "[\"hello\", \"world\"]";
|
||||
List list = JsonUtils.jsonToObject(s, List.class);
|
||||
assertThat(list).isEqualTo(List.of("hello", "world"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
package run.halo.app.infra.utils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.security.util.InMemoryResource;
|
||||
import run.halo.app.extension.Unstructured;
|
||||
|
||||
/**
|
||||
* Tests for {@link YamlUnstructuredLoader}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class YamlUnstructuredLoaderTest {
|
||||
|
||||
private List<InMemoryResource> yamlResources;
|
||||
private String notSpecYaml;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
String viewCategoriesRoleYaml = """
|
||||
apiVersion: v1alpha1
|
||||
kind: Fake
|
||||
metadata:
|
||||
name: test1
|
||||
hello:
|
||||
world: halo
|
||||
""";
|
||||
|
||||
String multipleRoleYaml = """
|
||||
apiVersion: v1alpha1
|
||||
kind: Fake
|
||||
metadata:
|
||||
name: test2
|
||||
hello:
|
||||
world: haha
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: Fake
|
||||
metadata:
|
||||
name: test2
|
||||
hello:
|
||||
world: bang
|
||||
""";
|
||||
|
||||
notSpecYaml = """
|
||||
server:
|
||||
port: 8090
|
||||
spring:
|
||||
jackson:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
""";
|
||||
|
||||
yamlResources = Stream.of(viewCategoriesRoleYaml, multipleRoleYaml, notSpecYaml)
|
||||
.map(InMemoryResource::new)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadTest() throws JsonProcessingException {
|
||||
Resource[] resources = yamlResources.toArray(Resource[]::new);
|
||||
YamlUnstructuredLoader yamlUnstructuredLoader = new YamlUnstructuredLoader(resources);
|
||||
List<Unstructured> unstructuredList = yamlUnstructuredLoader.load();
|
||||
assertThat(unstructuredList).isNotNull();
|
||||
assertThat(unstructuredList).hasSize(3);
|
||||
|
||||
assertThat(JsonUtils.objectToJson(unstructuredList)).isEqualToIgnoringWhitespace("""
|
||||
[
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "Fake",
|
||||
"metadata": {
|
||||
"name": "test1"
|
||||
},
|
||||
"hello": {
|
||||
"world": "halo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "Fake",
|
||||
"metadata": {
|
||||
"name": "test2"
|
||||
},
|
||||
"hello": {
|
||||
"world": "haha"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "Fake",
|
||||
"metadata": {
|
||||
"name": "test2"
|
||||
},
|
||||
"hello": {
|
||||
"world": "bang"
|
||||
}
|
||||
}
|
||||
]
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadIgnore() {
|
||||
InMemoryResource resource = new InMemoryResource(notSpecYaml);
|
||||
YamlUnstructuredLoader yamlUnstructuredLoader = new YamlUnstructuredLoader(resource);
|
||||
List<Unstructured> unstructuredList = yamlUnstructuredLoader.load();
|
||||
assertThat(unstructuredList).isEmpty();
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue