From 3339b381c81ae0e574d072532fa6c057ba43eb64 Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Mon, 27 Mar 2023 17:25:59 +0800 Subject: [PATCH] feat: provide a secret extension to store sensitive data (#3594) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### 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 自定义模型用于存储敏感数据 ``` --- .../java/run/halo/app/extension/Secret.java | 54 ++++++++++++ .../run/halo/app/extension/SecretTest.java | 84 +++++++++++++++++++ .../run/halo/app/infra/SchemeInitializer.java | 2 + 3 files changed, 140 insertions(+) create mode 100644 api/src/main/java/run/halo/app/extension/Secret.java create mode 100644 api/src/test/java/run/halo/app/extension/SecretTest.java diff --git a/api/src/main/java/run/halo/app/extension/Secret.java b/api/src/main/java/run/halo/app/extension/Secret.java new file mode 100644 index 000000000..ce029d807 --- /dev/null +++ b/api/src/main/java/run/halo/app/extension/Secret.java @@ -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 + * kebernetes Secret + * @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: + * secret-types + */ + private String type; + + /** + *

The total bytes of the values in + * the Data field must be less than {@link #MAX_SECRET_SIZE} bytes.

+ *

{@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 + * rfc4648#section-4 + *

+ */ + private Map 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 stringData; + +} diff --git a/api/src/test/java/run/halo/app/extension/SecretTest.java b/api/src/test/java/run/halo/app/extension/SecretTest.java new file mode 100644 index 000000000..af18827f7 --- /dev/null +++ b/api/src/test/java/run/halo/app/extension/SecretTest.java @@ -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=" + } + } + """; + } +} diff --git a/application/src/main/java/run/halo/app/infra/SchemeInitializer.java b/application/src/main/java/run/halo/app/infra/SchemeInitializer.java index 326e27108..ce5f21ba8 100644 --- a/application/src/main/java/run/halo/app/infra/SchemeInitializer.java +++ b/application/src/main/java/run/halo/app/infra/SchemeInitializer.java @@ -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