feat: provide a secret extension to store sensitive data (#3594)

#### What type of PR is this?
/kind feature
/milestone 2.4.x
/area core

#### What this PR does / why we need it:
提供 Secret 自定义模型用于存储敏感数据
例如:密码、token 等
参考自: https://kubernetes.io/docs/concepts/configuration/secret

#### Which issue(s) this PR fixes:

Fixes #3267

#### Does this PR introduce a user-facing change?
```release-note
提供 Secret 自定义模型用于存储敏感数据
```
pull/3568/head
guqing 2023-03-27 17:25:59 +08:00 committed by GitHub
parent 403702021c
commit 3339b381c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 140 additions and 0 deletions

View File

@ -0,0 +1,54 @@
package run.halo.app.extension;
import java.util.Map;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Secret is a small piece of sensitive data which should be kept secret, such as a password,
* a token, or a key.
*
* @author guqing
* @see
* <a href="https://github.com/kubernetes/kubernetes/blob/f33498a8256b455b677ad4d30440869318b84204/staging/src/k8s.io/api/core/v1/types.go">kebernetes Secret</a>
* @since 2.0.0
*/
@Data
@EqualsAndHashCode(callSuper = true)
@GVK(group = "", version = "v1alpha1", kind = Secret.KIND, plural = "secrets", singular = "secret")
public class Secret extends AbstractExtension {
public static final String KIND = "Secret";
public static final String SECRET_TYPE_OPAQUE = "Opaque";
public static final int MAX_SECRET_SIZE = 1024 * 1024;
/**
* Used to facilitate programmatic handling of secret data.
* More info:
* <a href="https://kubernetes.io/docs/concepts/configuration/secret/#secret-types">secret-types</a>
*/
private String type;
/**
* <p>The total bytes of the values in
* the Data field must be less than {@link #MAX_SECRET_SIZE} bytes.</p>
* <p>{@code data} contains the secret data. Each key must consist of alphanumeric
* characters, '-', '_' or '.'. The serialized form of the secret data is a
* base64 encoded string, representing the arbitrary (possibly non-string)
* data value here. Described in
* <a href="https://tools.ietf.org/html/rfc4648#section-4">rfc4648#section-4</a>
* </p>
*/
private Map<String, byte[]> data;
/**
* {@code stringData} allows specifying non-binary secret data in string form.
* It is provided as a write-only input field for convenience.
* All keys and values are merged into the data field on write, overwriting any existing
* values.
* The stringData field is never output when reading from the API.
*/
private Map<String, String> stringData;
}

View File

@ -0,0 +1,84 @@
package run.halo.app.extension;
import static org.assertj.core.api.Assertions.assertThat;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import java.util.Map;
import org.json.JSONException;
import org.junit.jupiter.api.Test;
import org.skyscreamer.jsonassert.JSONAssert;
import run.halo.app.infra.utils.JsonUtils;
/**
* Tests for {@link Secret}.
*
* @author guqing
* @since 2.4.0
*/
class SecretTest {
@Test
void serialize() throws JSONException {
Secret secret = new Secret();
secret.setMetadata(new Metadata());
secret.getMetadata().setName("test-secret");
secret.setType(Secret.SECRET_TYPE_OPAQUE);
secret.setData(Map.of("password", "admin".getBytes()));
String s = JsonUtils.objectToJson(secret);
JSONAssert.assertEquals(testJsonString(), s, true);
}
@Test
void deserialize() {
String s = testJsonString();
Secret secret = JsonUtils.jsonToObject(s, Secret.class);
assertThat(secret).isNotNull();
assertThat(secret.getMetadata().getName()).isEqualTo("test-secret");
assertThat(secret.getType()).isEqualTo(Secret.SECRET_TYPE_OPAQUE);
assertThat(secret.getData()).containsEntry("password", "admin".getBytes());
}
@Test
void deserializeWithUnstructured() throws JsonProcessingException {
Secret secret = Unstructured.OBJECT_MAPPER.readValue(testJsonString(), Secret.class);
assertThat(secret.getMetadata().getName()).isEqualTo("test-secret");
assertThat(secret.getType()).isEqualTo(Secret.SECRET_TYPE_OPAQUE);
assertThat(secret.getData()).containsEntry("password", "admin".getBytes());
}
@Test
void deserializeYamlWithStringData() throws JsonProcessingException {
String s = """
apiVersion: v1alpha1
kind: Secret
metadata:
name: secret-basic-auth
type: halo.run/basic-auth
stringData:
username: admin
password: t0p-Secret
""";
Secret secret = new YAMLMapper().readValue(s, Secret.class);
assertThat(secret.getMetadata().getName()).isEqualTo("secret-basic-auth");
assertThat(secret.getType()).isEqualTo("halo.run/basic-auth");
assertThat(secret.getStringData()).containsEntry("username", "admin");
assertThat(secret.getStringData()).containsEntry("password", "t0p-Secret");
}
private String testJsonString() {
return """
{
"apiVersion": "v1alpha1",
"kind": "Secret",
"metadata": {
"name": "test-secret"
},
"type": "Opaque",
"data": {
"password": "YWRtaW4="
}
}
""";
}
}

View File

@ -31,6 +31,7 @@ import run.halo.app.core.extension.content.Snapshot;
import run.halo.app.core.extension.content.Tag;
import run.halo.app.extension.ConfigMap;
import run.halo.app.extension.SchemeManager;
import run.halo.app.extension.Secret;
import run.halo.app.plugin.extensionpoint.ExtensionDefinition;
import run.halo.app.plugin.extensionpoint.ExtensionPointDefinition;
import run.halo.app.search.extension.SearchEngine;
@ -66,6 +67,7 @@ public class SchemeInitializer implements ApplicationListener<ApplicationStarted
schemeManager.register(Setting.class);
schemeManager.register(AnnotationSetting.class);
schemeManager.register(ConfigMap.class);
schemeManager.register(Secret.class);
schemeManager.register(Theme.class);
schemeManager.register(Menu.class);
schemeManager.register(MenuItem.class);