mirror of
https://github.com/google/nomulus
synced 2026-07-08 00:56:53 +00:00
Enforce GCS Public Access Prevention (#3133)
Implement programmatic verification of Public Access Prevention (PAP) on all GCS buckets before performing write operations via GcsUtils. - Add verifyPublicAccessPrevention(String bucketName) to GcsUtils.java, querying the bucket's IAM configuration and asserting that PAP is ENFORCED. - Call verifyPublicAccessPrevention in all output stream and byte creation write paths in GcsUtils.
This commit is contained in:
@@ -14,12 +14,15 @@
|
||||
|
||||
package google.registry.gcs;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.Iterables.getLast;
|
||||
|
||||
import com.google.cloud.storage.Blob;
|
||||
import com.google.cloud.storage.BlobId;
|
||||
import com.google.cloud.storage.BlobInfo;
|
||||
import com.google.cloud.storage.Bucket;
|
||||
import com.google.cloud.storage.BucketInfo;
|
||||
import com.google.cloud.storage.Storage;
|
||||
import com.google.cloud.storage.Storage.BlobListOption;
|
||||
import com.google.cloud.storage.StorageException;
|
||||
@@ -33,12 +36,14 @@ import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.config.CredentialModule.ApplicationDefaultCredential;
|
||||
import google.registry.util.GoogleCredentialsBundle;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import jakarta.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.nio.channels.Channels;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
|
||||
/**
|
||||
@@ -50,6 +55,8 @@ public class GcsUtils implements Serializable {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private static final ConcurrentHashMap<String, Boolean> PAP_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
private static final ImmutableMap<String, MediaType> EXTENSIONS =
|
||||
new ImmutableMap.Builder<String, MediaType>()
|
||||
.put("ghostryde", MediaType.APPLICATION_BINARY)
|
||||
@@ -85,6 +92,7 @@ public class GcsUtils implements Serializable {
|
||||
/** Opens a GCS file for writing as an {@link OutputStream}, overwriting existing files. */
|
||||
@CheckReturnValue
|
||||
public OutputStream openOutputStream(BlobId blobId) {
|
||||
verifyPublicAccessPrevention(blobId.getBucket());
|
||||
return Channels.newOutputStream(storage().writer(createBlobInfo(blobId)));
|
||||
}
|
||||
|
||||
@@ -94,6 +102,7 @@ public class GcsUtils implements Serializable {
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public OutputStream openOutputStream(BlobId blobId, ImmutableMap<String, String> metadata) {
|
||||
verifyPublicAccessPrevention(blobId.getBucket());
|
||||
return Channels.newOutputStream(
|
||||
storage().writer(BlobInfo.newBuilder(blobId).setMetadata(metadata).build()));
|
||||
}
|
||||
@@ -105,6 +114,7 @@ public class GcsUtils implements Serializable {
|
||||
|
||||
/** Creates a GCS file with the given byte contents and metadata, overwriting existing files. */
|
||||
public void createFromBytes(BlobInfo blobInfo, byte[] bytes) throws StorageException {
|
||||
verifyPublicAccessPrevention(blobInfo.getBucket());
|
||||
storage().create(blobInfo, bytes);
|
||||
}
|
||||
|
||||
@@ -120,6 +130,7 @@ public class GcsUtils implements Serializable {
|
||||
|
||||
/** Update file content type on existing GCS file */
|
||||
public void updateContentType(BlobId blobId, String contentType) throws StorageException {
|
||||
verifyPublicAccessPrevention(blobId.getBucket());
|
||||
if (existsAndNotEmpty(blobId)) {
|
||||
Blob blob = storage().get(blobId);
|
||||
blob.toBuilder().setContentType(contentType).build().update();
|
||||
@@ -154,12 +165,6 @@ public class GcsUtils implements Serializable {
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the user defined metadata of a GCS file if the file exists, or an empty map. */
|
||||
public ImmutableMap<String, String> getMetadata(BlobId blobId) throws StorageException {
|
||||
Blob blob = storage().get(blobId);
|
||||
return blob == null ? ImmutableMap.of() : ImmutableMap.copyOf(blob.getMetadata());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link BlobInfo} of the given GCS file.
|
||||
*
|
||||
@@ -179,6 +184,37 @@ public class GcsUtils implements Serializable {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that Public Access Prevention (PAP) is enforced on the GCS bucket.
|
||||
*
|
||||
* @throws IllegalStateException if PAP is not ENFORCED.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void verifyPublicAccessPrevention(String bucketName) {
|
||||
if (RegistryEnvironment.get() != RegistryEnvironment.PRODUCTION) {
|
||||
return;
|
||||
}
|
||||
PAP_CACHE.computeIfAbsent(
|
||||
bucketName,
|
||||
name -> {
|
||||
Bucket bucket = storage().get(name);
|
||||
checkState(bucket != null, "Bucket %s does not exist", name);
|
||||
BucketInfo.PublicAccessPrevention pap =
|
||||
bucket.getIamConfiguration().getPublicAccessPrevention();
|
||||
checkState(
|
||||
pap == BucketInfo.PublicAccessPrevention.ENFORCED,
|
||||
"Public Access Prevention is not enforced on bucket %s. Current state: %s",
|
||||
name,
|
||||
pap);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static void clearPapCache() {
|
||||
PAP_CACHE.clear();
|
||||
}
|
||||
|
||||
// These two methods are needed to check whether serialization is done correctly in tests.
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
|
||||
@@ -15,27 +15,40 @@
|
||||
package google.registry.gcs;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.cloud.storage.BlobId;
|
||||
import com.google.cloud.storage.BlobInfo;
|
||||
import com.google.cloud.storage.Bucket;
|
||||
import com.google.cloud.storage.BucketInfo;
|
||||
import com.google.cloud.storage.Storage;
|
||||
import com.google.cloud.storage.StorageOptions;
|
||||
import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.testing.SystemPropertyExtension;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
/** Unit tests for {@link GcsUtilsTest}. */
|
||||
class GcsUtilsTest {
|
||||
|
||||
@RegisterExtension
|
||||
final SystemPropertyExtension systemPropertyExtension = new SystemPropertyExtension();
|
||||
|
||||
private GcsUtils gcsUtils = new GcsUtils(LocalStorageHelper.getOptions());
|
||||
|
||||
private String bucket = "my-bucket";
|
||||
@@ -43,9 +56,18 @@ class GcsUtilsTest {
|
||||
private BlobId blobId = BlobId.of(bucket, filename);
|
||||
private ImmutableMap<String, String> metadata = ImmutableMap.of("key1", "val1", "Key2", "val2");
|
||||
private final byte[] bytes = new byte[] {'a', 'b', 'c'};
|
||||
private RegistryEnvironment previousEnvironment;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {}
|
||||
void beforeEach() {
|
||||
previousEnvironment = RegistryEnvironment.get();
|
||||
GcsUtils.clearPapCache();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void afterEach() {
|
||||
previousEnvironment.setup();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSerialization_testStorage() throws Exception {
|
||||
@@ -111,6 +133,88 @@ class GcsUtilsTest {
|
||||
assertThat(gcsUtils.existsAndNotEmpty(blobId)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVerifyPublicAccessPrevention_enforced() {
|
||||
RegistryEnvironment.PRODUCTION.setup(systemPropertyExtension);
|
||||
Storage mockStorage = mock(Storage.class);
|
||||
StorageOptions mockOptions = mock(StorageOptions.class);
|
||||
when(mockOptions.getService()).thenReturn(mockStorage);
|
||||
|
||||
Bucket mockBucket = mock(Bucket.class);
|
||||
when(mockStorage.get("my-bucket")).thenReturn(mockBucket);
|
||||
|
||||
BucketInfo.IamConfiguration mockIamConfig = mock(BucketInfo.IamConfiguration.class);
|
||||
when(mockBucket.getIamConfiguration()).thenReturn(mockIamConfig);
|
||||
when(mockIamConfig.getPublicAccessPrevention())
|
||||
.thenReturn(BucketInfo.PublicAccessPrevention.ENFORCED);
|
||||
|
||||
GcsUtils utils = new GcsUtils(mockOptions);
|
||||
utils.verifyPublicAccessPrevention("my-bucket");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVerifyPublicAccessPrevention_notCheckedInNonProd() {
|
||||
RegistryEnvironment.SANDBOX.setup(systemPropertyExtension);
|
||||
Storage mockStorage = mock(Storage.class);
|
||||
StorageOptions mockOptions = mock(StorageOptions.class);
|
||||
when(mockOptions.getService()).thenReturn(mockStorage);
|
||||
|
||||
Bucket mockBucket = mock(Bucket.class);
|
||||
when(mockStorage.get("my-bucket")).thenReturn(mockBucket);
|
||||
|
||||
BucketInfo.IamConfiguration mockIamConfig = mock(BucketInfo.IamConfiguration.class);
|
||||
when(mockBucket.getIamConfiguration()).thenReturn(mockIamConfig);
|
||||
when(mockIamConfig.getPublicAccessPrevention())
|
||||
.thenReturn(BucketInfo.PublicAccessPrevention.INHERITED);
|
||||
|
||||
GcsUtils utils = new GcsUtils(mockOptions);
|
||||
// no exception thrown even though PAP isn't enforced
|
||||
utils.verifyPublicAccessPrevention("my-bucket");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVerifyPublicAccessPrevention_notEnforced() {
|
||||
RegistryEnvironment.PRODUCTION.setup(systemPropertyExtension);
|
||||
Storage mockStorage = mock(Storage.class);
|
||||
StorageOptions mockOptions = mock(StorageOptions.class);
|
||||
when(mockOptions.getService()).thenReturn(mockStorage);
|
||||
|
||||
Bucket mockBucket = mock(Bucket.class);
|
||||
when(mockStorage.get("my-bucket")).thenReturn(mockBucket);
|
||||
|
||||
BucketInfo.IamConfiguration mockIamConfig = mock(BucketInfo.IamConfiguration.class);
|
||||
when(mockBucket.getIamConfiguration()).thenReturn(mockIamConfig);
|
||||
when(mockIamConfig.getPublicAccessPrevention())
|
||||
.thenReturn(BucketInfo.PublicAccessPrevention.INHERITED);
|
||||
|
||||
GcsUtils utils = new GcsUtils(mockOptions);
|
||||
|
||||
IllegalStateException thrown =
|
||||
assertThrows(
|
||||
IllegalStateException.class, () -> utils.verifyPublicAccessPrevention("my-bucket"));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains("Public Access Prevention is not enforced on bucket my-bucket");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVerifyPublicAccessPrevention_nonexistentBucket() {
|
||||
RegistryEnvironment.PRODUCTION.setup(systemPropertyExtension);
|
||||
Storage mockStorage = mock(Storage.class);
|
||||
StorageOptions mockOptions = mock(StorageOptions.class);
|
||||
when(mockOptions.getService()).thenReturn(mockStorage);
|
||||
|
||||
when(mockStorage.get("nonexistent-bucket")).thenReturn(null);
|
||||
|
||||
GcsUtils utils = new GcsUtils(mockOptions);
|
||||
|
||||
IllegalStateException thrown =
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> utils.verifyPublicAccessPrevention("nonexistent-bucket"));
|
||||
assertThat(thrown).hasMessageThat().contains("Bucket nonexistent-bucket does not exist");
|
||||
}
|
||||
|
||||
private static byte[] serialize(Object object) throws IOException {
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
ObjectOutputStream oos = new ObjectOutputStream(baos);
|
||||
|
||||
Reference in New Issue
Block a user