You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by ah...@apache.org on 2026/04/08 07:42:35 UTC

(causeway) branch main updated: CAUSEWAY-3988: Support for Env Var provided HMAC Secret

This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/causeway.git


The following commit(s) were added to refs/heads/main by this push:
     new c004df1e76f CAUSEWAY-3988: Support for Env Var provided HMAC Secret
c004df1e76f is described below

commit c004df1e76fd2501b901b0efb35aba69e9c85f9e
Author: andi-huber <ah...@apache.org>
AuthorDate: Wed Apr 8 09:42:22 2026 +0200

    CAUSEWAY-3988: Support for Env Var provided HMAC Secret
---
 .../causeway/commons/internal/base/_Bytes.java     | 84 ++++++++++++----------
 .../causeway/commons/internal/base/_BytesTest.java | 40 +++++++++++
 .../CausewayModuleCoreRuntimeServices.java         | 22 +++++-
 3 files changed, 106 insertions(+), 40 deletions(-)

diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/base/_Bytes.java b/commons/src/main/java/org/apache/causeway/commons/internal/base/_Bytes.java
index 8e1d5a47714..696854bbd7d 100644
--- a/commons/src/main/java/org/apache/causeway/commons/internal/base/_Bytes.java
+++ b/commons/src/main/java/org/apache/causeway/commons/internal/base/_Bytes.java
@@ -25,12 +25,12 @@
 import java.util.function.UnaryOperator;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
+import java.util.stream.Stream;
 import java.util.zip.Deflater;
 import java.util.zip.Inflater;
 
-import org.jspecify.annotations.Nullable;
-
 import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
 
 /**
  * <h1>- internal use only -</h1>
@@ -59,9 +59,8 @@ private _Bytes(){}
      * @throws IOException
      */
     public static byte[] of(final @Nullable InputStream input) throws IOException {
-        if(input==null) {
+        if(input==null)
             return null;
-        }
 
         try(final ByteArrayOutputStream bos = new ByteArrayOutputStream()){
             final byte[] buffer = new byte[BUFFER_SIZE];
@@ -84,9 +83,8 @@ public static byte[] of(final @Nullable InputStream input) throws IOException {
      * @throws IOException
      */
     public static byte[] ofKeepOpen(final @Nullable InputStream input) throws IOException {
-        if(input==null) {
+        if(input==null)
             return null;
-        }
 
         try(final ByteArrayOutputStream bos = new ByteArrayOutputStream()){
             final byte[] buffer = new byte[BUFFER_SIZE];
@@ -110,9 +108,8 @@ public static byte[] ofKeepOpen(final @Nullable InputStream input) throws IOExce
      */
     public static IntStream streamAsInts(final @Nullable byte[] bytes) {
         if(bytes==null
-                || bytes.length==0) {
+                || bytes.length==0)
             return IntStream.empty();
-        }
         return IntStream.range(0, bytes.length)
                 .map(index->(bytes[index] & 0xff));
     }
@@ -126,9 +123,8 @@ public static IntStream streamAsInts(final @Nullable byte[] bytes) {
      * @see #streamAsInts(byte[])
      */
     private static byte[] ofIntStream(final @Nullable IntStream intStream) {
-        if(intStream==null) {
+        if(intStream==null)
             return new byte[0];
-        }
         var listOfInts = intStream
                 .boxed()
                 .collect(Collectors.toList());
@@ -148,9 +144,8 @@ private static byte[] ofIntStream(final @Nullable IntStream intStream) {
      * @see #ofHexDump(String, String)
      */
     public static String hexDump(final @Nullable byte[] bytes, final @Nullable String delimiter) {
-        if(bytes==null) {
+        if(bytes==null)
             return "";
-        }
         return _Bytes.streamAsInts(bytes)
                 .mapToObj(Integer::toHexString)
                 .map(s->s.length()==1
@@ -172,9 +167,8 @@ public static String hexDump(final @Nullable byte[] bytes) {
      * @see #hexDump(byte[], String)
      */
     public static byte[] ofHexDump(final @Nullable String hexDump, final @Nullable String delimiter) {
-        if(hexDump==null) {
+        if(hexDump==null)
             return new byte[0];
-        }
         final int delimLen = _NullSafe.size(delimiter);
         final int stride = 2 + delimLen;
 
@@ -211,14 +205,12 @@ public static byte[] nullToEmpty(final byte[] bytes) {
      */
     public static final byte[] prepend(final @Nullable byte[] target, final @Nullable byte ... bytes) {
         if(target==null) {
-            if(bytes==null) {
+            if(bytes==null)
                 return null;
-            }
             return bytes.clone();
         }
-        if(bytes==null) {
+        if(bytes==null)
             return target.clone();
-        }
         final byte[] result = new byte[target.length + bytes.length];
         System.arraycopy(bytes, 0, result, 0, bytes.length);
         System.arraycopy(target, 0, result, bytes.length, target.length);
@@ -233,14 +225,12 @@ public static final byte[] prepend(final @Nullable byte[] target, final @Nullabl
      */
     public static final byte[] append(final @Nullable byte[] target, final @Nullable byte ... bytes) {
         if(target==null) {
-            if(bytes==null) {
+            if(bytes==null)
                 return null;
-            }
             return bytes.clone();
         }
-        if(bytes==null) {
+        if(bytes==null)
             return target.clone();
-        }
         final byte[] result = new byte[target.length + bytes.length];
         System.arraycopy(target, 0, result, 0, target.length);
         System.arraycopy(bytes, 0, result, target.length, bytes.length);
@@ -281,12 +271,10 @@ public static final byte[] decodeBase64(final Base64.@NonNull Decoder decoder, f
      * @return null if {@code input} is null
      */
     public static final byte[] compress(final @Nullable byte[] input) {
-        if(input==null) {
+        if(input==null)
             return null;
-        }
-        if(input.length==0) {
+        if(input.length==0)
             return input;
-        }
         try {
             return _Bytes_GZipCompressorSmart.compress(input);
         } catch (IOException e) {
@@ -302,12 +290,10 @@ public static final byte[] compress(final @Nullable byte[] input) {
      * @return null if {@code compressed} is null
      */
     public static final byte[] decompress(final @Nullable byte[] compressed) {
-        if(compressed==null) {
+        if(compressed==null)
             return null;
-        }
-        if(compressed.length==0) {
+        if(compressed.length==0)
             return compressed;
-        }
         try {
             return _Bytes_GZipCompressorSmart.decompress(compressed);
         } catch (IOException e) {
@@ -323,12 +309,10 @@ public static final byte[] decompress(final @Nullable byte[] compressed) {
      * @return null if {@code input} is null
      */
     public static final byte[] compressZlib(final @Nullable byte[] input) {
-        if(input==null) {
+        if(input==null)
             return null;
-        }
-        if(input.length==0) {
+        if(input.length==0)
             return input;
-        }
         try {
             return _Bytes_ZLibCompressor.compress(input);
         } catch (IOException e) {
@@ -344,12 +328,10 @@ public static final byte[] compressZlib(final @Nullable byte[] input) {
      * @return null if {@code compressed} is null
      */
     public static final byte[] decompressZlib(final @Nullable byte[] compressed) {
-        if(compressed==null) {
+        if(compressed==null)
             return null;
-        }
-        if(compressed.length==0) {
+        if(compressed.length==0)
             return compressed;
-        }
         try {
             return _Bytes_ZLibCompressor.decompress(compressed);
         } catch (IOException e) {
@@ -427,6 +409,30 @@ public static BytesOperator operator() {
             .andThen(bytes->decodeBase64(Base64.getDecoder(), bytes))
             .andThen(_Bytes::decompress);
 
-    // --
+
+    // -- EXTERNAL FORMAT
+
+    public static byte[] fromInts(final int[] ints) {
+        var bytes = new byte[ints.length];
+        IntStream.range(0, bytes.length)
+            .forEach(i->bytes[i] = (byte)ints[i]);
+        return bytes;
+    }
+
+    public static byte[] parse(final String asString) {
+        var ints = Stream.of(asString.split("\\,"))
+            .map(String::trim)
+            .mapToInt(Integer::parseInt)
+            .toArray();
+        return fromInts(ints);
+    }
+
+    public static String stringify(final byte[] bytes) {
+        var asString = IntStream.range(0, bytes.length)
+                .map(i->(int)bytes[i])
+                .mapToObj(i->""+i)
+                .collect(Collectors.joining(","));
+        return asString;
+    }
 
 }
diff --git a/commons/src/test/java/org/apache/causeway/commons/internal/base/_BytesTest.java b/commons/src/test/java/org/apache/causeway/commons/internal/base/_BytesTest.java
new file mode 100644
index 00000000000..a3093bfa9d5
--- /dev/null
+++ b/commons/src/test/java/org/apache/causeway/commons/internal/base/_BytesTest.java
@@ -0,0 +1,40 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.commons.internal.base;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class _BytesTest {
+
+    @Test
+    void stringifyRoundtrip() throws NoSuchAlgorithmException {
+        var secret = new byte[32];
+        SecureRandom.getInstanceStrong().nextBytes(secret);
+        var before = _Bytes.stringify(secret);
+        var after = _Bytes.stringify(_Bytes.parse(before));
+
+        assertEquals(before, after);
+    }
+
+}
diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java
index 413d8bffdcc..24042c20ee0 100644
--- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java
+++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java
@@ -18,6 +18,8 @@
  */
 package org.apache.causeway.core.runtimeservices;
 
+import java.util.Optional;
+
 import org.springframework.boot.autoconfigure.AutoConfigureOrder;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.context.annotation.Bean;
@@ -26,9 +28,12 @@
 import org.springframework.context.annotation.Import;
 import org.springframework.core.OrderComparator;
 import org.springframework.core.annotation.AnnotationAwareOrderComparator;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
 
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.applib.services.bookmark.HmacAuthority;
+import org.apache.causeway.commons.internal.base._Bytes;
 import org.apache.causeway.core.codegen.bytebuddy.CausewayModuleCoreCodegenByteBuddy;
 import org.apache.causeway.core.runtime.CausewayModuleCoreRuntime;
 import org.apache.causeway.core.runtimeservices.bookmarks.BookmarkServiceDefault;
@@ -73,6 +78,8 @@
 import org.apache.causeway.core.runtimeservices.xml.XmlServiceDefault;
 import org.apache.causeway.core.runtimeservices.xmlsnapshot.XmlSnapshotServiceDefault;
 
+import lombok.extern.slf4j.Slf4j;
+
 @Configuration(proxyBeanMethods = false)
 @Import({
         // Modules
@@ -132,6 +139,7 @@
 
 })
 @ComponentScan(basePackages = "org.apache.causeway.core.runtimeservices.icons")
+@Slf4j
 public class CausewayModuleCoreRuntimeServices {
 
     public static final String NAMESPACE = "causeway.runtimeservices";
@@ -147,7 +155,19 @@ static class HmacAuthorityAutoconfigure {
         @Bean(NAMESPACE + ".fallbackHmacAuthority")
         @ConditionalOnMissingBean(HmacAuthority.class)
         public HmacAuthority fallbackHmacAuthority() {
-            return HmacAuthority.HmacSHA256.randomInstance();
+
+            var secretFromEnv = Optional.ofNullable(System.getenv("causeway_hmac_sha256"))
+                .filter(StringUtils::hasText)
+                .map(_Bytes::parse)
+                .filter(secret->{
+                    Assert.isTrue(secret.length>16, ()->"Illegal Argument: "
+                            + "env. var. 'causeway_hmac_sha256' underflows required byte count of 16");
+                    log.info("using secret from env. var. 'causeway_hmac_sha256' having length {} bytes", secret.length);
+                    return true;
+                });
+
+            return secretFromEnv.map(HmacAuthority.HmacSHA256::new)
+                    .orElseGet(HmacAuthority.HmacSHA256::randomInstance);
         }
     }