feat: add yaml unstructured loader (#2122)

pull/2128/head
guqing 2022-05-28 17:44:08 +08:00 committed by GitHub
parent 3c856d04af
commit 9a05942bc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 406 additions and 25 deletions

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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"]

View File

@ -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"));
}
}

View File

@ -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();
}
}