Skip to content

Commit

Permalink
implement server side bucket encryption service (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
Robothy authored Dec 11, 2022
1 parent 22a7f6a commit fe665f1
Show file tree
Hide file tree
Showing 23 changed files with 340 additions and 30 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ and without heavy dependencies, it starts up quickly and handles requests effici
+ CreateMultipartUpload
+ CompleteMultipartUpload
+ DeleteBucket
+ DeleteBucketEncryption
+ DeleteBucketPolicy
+ DeleteBucketReplication
+ DeleteBucketTagging
+ DeleteObject
+ GetObject
+ GetBucketAcl
+ GetBucketEncryption
+ GetBucketPolicy
+ GetBucketReplication
+ GetBucketVersioning
Expand All @@ -33,6 +35,7 @@ and without heavy dependencies, it starts up quickly and handles requests effici
+ ListObjectVersions
+ PutObject
+ PutBucketAcl
+ PutBucketEncryption
+ PutBucketPolicy
+ PutBucketReplication
+ PutBucketVersioning
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ repositories {
task mergeReports(type: JacocoReport) {
executionData.from(project.fileTree(dir: '.', include: '**/build/jacoco/test.exec'))
sourceDirectories.from(project.fileTree(dir: '.', include: '**/src/main/java/**', exclude: 'local-s3-docker/src/main/java/**'))
classDirectories.from(project.fileTree(dir: '.', include: '**/build/classes/java/main/**'))
classDirectories.from(project.fileTree(dir: '.', include: '**/build/classes/java/main/**', exclude: 'local-s3-docker/build/classes/java/main/**'))
reports {
xml.required.set(true)
html.required.set(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.robothy.s3.core.exception.BucketReplicationNotExistException;
import com.robothy.s3.core.exception.BucketTaggingNotExistException;
import com.robothy.s3.core.exception.InvalidBucketNameException;
import com.robothy.s3.core.exception.ServerSideEncryptionConfigurationNotFoundException;
import com.robothy.s3.core.model.internal.BucketMetadata;
import com.robothy.s3.core.model.internal.LocalS3Metadata;
import java.util.Collection;
Expand Down Expand Up @@ -104,4 +105,17 @@ public static String assertBucketReplicationExist(LocalS3Metadata s3Metadata, St
return bucketMetadata.getReplication().orElseThrow(BucketReplicationNotExistException::new);
}

/**
* Assert the specified bucket has configured server side encryption.
*
* @param localS3Metadata LocalS3 metadata.
* @param bucketName the bucket name.
* @return bucket encryption configuration.
*/
public static String assertBucketEncryptionExist(LocalS3Metadata localS3Metadata, String bucketName) {
BucketMetadata bucketMetadata = assertBucketExists(localS3Metadata, bucketName);
return bucketMetadata.getEncryption().orElseThrow(() ->
new ServerSideEncryptionConfigurationNotFoundException(bucketName));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,26 @@

public class VersionedObjectAssertions {

/**
* Asset that the specified object and version is exist in the bucket.
* @param objectMetadata object metadata.
* @param version object version.
* @return the {@linkplain VersionedObjectMetadata} of the specified version.
*/
public static VersionedObjectMetadata assertVersionedObjectExist(ObjectMetadata objectMetadata, String version) {
return objectMetadata.getVersionedObjectMetadata(version)
.orElseThrow(() -> new VersionedObjectNotExistException(objectMetadata.getKey(), version));
.orElseThrow(() -> new VersionedObjectNotExistException(version));
}

/**
* Assert that the specified {@linkplain ObjectMetadata} has virtual version.
*
* @param objectMetadata the object metadata.
* @return the {@linkplain VersionedObjectMetadata} instance.
*/
public static VersionedObjectMetadata assertVirtualVersionExist(ObjectMetadata objectMetadata) {
return objectMetadata.getVirtualVersion().map(virtualVersion -> objectMetadata.getVersionedObjectMap().get(virtualVersion))
.orElseThrow(() -> new VersionedObjectNotExistException(objectMetadata.getKey(), ObjectMetadata.NULL_VERSION));
.orElseThrow(() -> new VersionedObjectNotExistException(ObjectMetadata.NULL_VERSION));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ public abstract class LocalS3Exception extends RuntimeException {

private final S3ErrorCode s3ErrorCode;

private String bucketName;

LocalS3Exception(S3ErrorCode s3ErrorCode, String message, Throwable cause) {
super(message, cause);
this.s3ErrorCode = s3ErrorCode;
Expand All @@ -24,8 +26,17 @@ public abstract class LocalS3Exception extends RuntimeException {
this(s3ErrorCode, message, null);
}

LocalS3Exception(String bucketName, S3ErrorCode s3ErrorCode) {
this(s3ErrorCode);
this.bucketName = bucketName;
}

public S3ErrorCode getS3ErrorCode() {
return s3ErrorCode;
}

public String getBucketName() {
return this.bucketName;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public enum S3ErrorCode {
RequestTorrentOfBucketError("RequestTorrentOfBucketError", 400, "Requesting the torrent file of a bucket is not permitted."),
ReplicationConfigurationNotFoundError("ReplicationConfigurationNotFoundError", 404, "The replication configuration was not found"),
//SignatureDoesNotMatch("SignatureDoesNotMatch", 403, "The request signature we calculated does not match the signature you provided. Check your AWS secret access key and signing method. For more information, see REST Authentication and "),
ServerSideEncryptionConfigurationNotFoundError("ServerSideEncryptionConfigurationNotFoundError", 404, "The server side encryption configuration was not found"),
ServiceUnavailable("ServiceUnavailable", 503, "Service is unable to handle request."),
//SlowDown("SlowDown", 503, "Reduce your request rate."),
//TemporaryRedirect("TemporaryRedirect", 307, "You are being redirected to the bucket while DNS updates."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.robothy.s3.core.exception;

/**
* Server side encryption not configured.
*/
public class ServerSideEncryptionConfigurationNotFoundException extends LocalS3Exception {

/**
* Constructor.
*
* @param bucketName the bucket that not configured server side encryption.
*/
public ServerSideEncryptionConfigurationNotFoundException(String bucketName) {
super(bucketName, S3ErrorCode.ServerSideEncryptionConfigurationNotFoundError);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,23 @@
*/
public class VersionedObjectNotExistException extends LocalS3Exception {

/**
* Create a {@linkplain VersionedObjectNotExistException} instance.
*
* @param key the object key.
* @param versionId the version ID.
*/
public VersionedObjectNotExistException(String key, String versionId) {
super(S3ErrorCode.NoSuchVersion, "Object(key=" + key + ") hasn't version '" + versionId + "'.");
}

/**
* Create an instance.
*
* @param version the object version.
*/
public VersionedObjectNotExistException(String version) {
super(S3ErrorCode.NoSuchVersion, "The object doesn't have version ID: " + version);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
@Data
public class BucketMetadata {

/**
* Create a {@linkplain BucketMetadata} instance.
*/
public BucketMetadata() {

}
Expand Down Expand Up @@ -47,20 +50,28 @@ public BucketMetadata() {

private String replication;

private String encryption;

/**
* Get metadata of the specified object.
*
* @param key the object key.
* @return the object metadata of the specified object key.
*/
public Optional<ObjectMetadata> getObjectMetadata(String key) {
return Optional.ofNullable(objectMap.get(key));
}


/**
* Add an {@linkplain ObjectMetadata} instance.
* Put an {@linkplain ObjectMetadata} instance.
*
* @param key the object key.
* @param objectMetadata object metadata.
* @return added object metadata.
*/
public ObjectMetadata addObjectMetadata(ObjectMetadata objectMetadata) {
ObjectAssertions.assertObjectKeyIsValid(objectMetadata.getKey());
objectMap.put(objectMetadata.getKey(), objectMetadata);
public ObjectMetadata putObjectMetadata(String key, ObjectMetadata objectMetadata) {
ObjectAssertions.assertObjectKeyIsValid(key);
objectMap.put(key, objectMetadata);
return objectMetadata;
}

Expand Down Expand Up @@ -131,4 +142,22 @@ public void setReplication(String replication) {
this.replication = replication;
}

/**
* Get the bucket encryption.
*
* @return the bucket encryption.
*/
public Optional<String> getEncryption() {
return Optional.ofNullable(encryption);
}

/**
* Set bucket encryption.
*
* @param encryption the bucket encryption.
*/
public void setEncryption(String encryption) {
this.encryption = encryption;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,15 @@ public class ObjectMetadata {
private String virtualVersion;



/**
* Construct an {@linkplain ObjectMetadata} instance. A new {@linkplain ObjectMetadata} instance must
* associate with a {@linkplain VersionedObjectMetadata}.
*
* @param key the object key.
* @param version the version ID. If the bucket versioning is not enabled, it
* should be set to virtual version via {@linkplain #setVirtualVersion(String)}.
* @param firstVersion first versioned instance of the {@linkplain ObjectMetadata}.
*/
public ObjectMetadata(String key, String version, VersionedObjectMetadata firstVersion) {
this.key = key;
public ObjectMetadata(String version, VersionedObjectMetadata firstVersion) {
this.putVersionedObjectMetadata(version, firstVersion);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.robothy.s3.core.service;

import com.robothy.s3.core.annotations.BucketChanged;
import com.robothy.s3.core.asserionts.BucketAssertions;
import com.robothy.s3.core.model.internal.BucketMetadata;

/**
* Bucket encryption put/get/delete service.
*/
public interface BucketEncryptionService extends LocalS3MetadataApplicable {

/**
* Put encryption configuration to the specified bucket.
*
* @param bucketName bucket name.
* @param encryption encryption configuration.
*/
@BucketChanged
default void putBucketEncryption(String bucketName, String encryption) {
BucketMetadata bucketMetadata = BucketAssertions.assertBucketExists(localS3Metadata(), bucketName);
bucketMetadata.setEncryption(encryption);
}

/**
* Get the encryption configuration of the specified bucket.
*
* @param bucketName the bucket name.
* @return encryption configuration the specified bucket.
*/
default String getBucketEncryption(String bucketName) {
return BucketAssertions.assertBucketEncryptionExist(localS3Metadata(), bucketName);
}

/**
* Remove bucket encryption configuration from the specified bucket.
*
* @param bucketName the bucket name.
*/
@BucketChanged
default void deleteBucketEncryption(String bucketName) {
BucketMetadata bucketMetadata = BucketAssertions.assertBucketExists(localS3Metadata(), bucketName);
bucketMetadata.setEncryption(null);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
import java.util.stream.Collectors;

public interface BucketService extends BucketTaggingService,
BucketAclService, BucketPolicyService, BucketReplicationService {
BucketAclService, BucketPolicyService, BucketReplicationService,
BucketEncryptionService {

/**
* Create a bucket.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@ static DeleteObjectAns deleteWithoutVersionId(Storage storage, BucketMetadata bu
} else { // key not exists.
VersionedObjectMetadata deleteMarker = createDeleteMarker();
String versionId = returnedVersionId = IdUtils.defaultGenerator().nextStrId();
ObjectMetadata objectMetadata = new ObjectMetadata(key, versionId, deleteMarker);
ObjectMetadata objectMetadata = new ObjectMetadata(versionId, deleteMarker);
if (!Boolean.TRUE.equals(bucketMetadata.getVersioningEnabled())) {
objectMetadata.setVirtualVersion(versionId);
returnedVersionId = ObjectMetadata.NULL_VERSION;
}
bucketMetadata.addObjectMetadata(objectMetadata);
bucketMetadata.putObjectMetadata(key, objectMetadata);
}

return DeleteObjectAns.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ default PutObjectAns putObject(String bucketName, String key, PutObjectOptions o
objectMetadata = bucketMetadata.getObjectMetadata(key).get();
objectMetadata.putVersionedObjectMetadata(versionId, versionedObjectMetadata);
} else {
objectMetadata = new ObjectMetadata(key, versionId, versionedObjectMetadata);
bucketMetadata.addObjectMetadata(objectMetadata);
objectMetadata = new ObjectMetadata(versionId, versionedObjectMetadata);
bucketMetadata.putObjectMetadata(key, objectMetadata);
}

String returnedVersionId = versionId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ class BucketMetadataTest {
@Test
void addObjectMetadata() {
BucketMetadata bucketMetadata = new BucketMetadata();
assertThrows(InvalidObjectKeyException.class, () -> bucketMetadata.addObjectMetadata(new ObjectMetadata()));
assertThrows(InvalidObjectKeyException.class, () -> bucketMetadata.putObjectMetadata("", new ObjectMetadata()));

ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setKey("abc.txt");
bucketMetadata.addObjectMetadata(objectMetadata);
bucketMetadata.putObjectMetadata("abc.txt", objectMetadata);
Optional<ObjectMetadata> optionalObjectMetadata = bucketMetadata.getObjectMetadata("abc.txt");
assertTrue(optionalObjectMetadata.isPresent());
assertTrue(bucketMetadata.getObjectMap().containsKey("abc.txt"));
Expand Down Expand Up @@ -58,10 +57,10 @@ void serialize() {
versionedObj1.setContentType("application/json");
versionedObj1.setModificationDate(1L);
versionedObj1.setCreationDate(2L);
ObjectMetadata obj1 = new ObjectMetadata("a.txt", "12", versionedObj1);
ObjectMetadata obj2 = new ObjectMetadata("b.json", "11", versionedObj1);
bucketMetadata.addObjectMetadata(obj1);
bucketMetadata.addObjectMetadata(obj2);
ObjectMetadata obj1 = new ObjectMetadata("12", versionedObj1);
ObjectMetadata obj2 = new ObjectMetadata("11", versionedObj1);
bucketMetadata.putObjectMetadata("a.txt", obj1);
bucketMetadata.putObjectMetadata("b.json", obj2);

UploadMetadata uploadMetadata = new UploadMetadata();
uploadMetadata.setCreateDate(1000);
Expand All @@ -78,6 +77,7 @@ void serialize() {
bucketMetadata.getUploads().put("a.txt", upload);

bucketMetadata.setReplication("Replication Configuration");
bucketMetadata.setEncryption("Encryption");

String json = JsonUtils.toJson(bucketMetadata);
BucketMetadata deserialized = JsonUtils.fromJson(json, BucketMetadata.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@ class ObjectMetadataTest {

@Test
void getLatest() {
String key = "a.txt";
String versionId = IdUtils.defaultGenerator().nextStrId();
VersionedObjectMetadata versionedObjectMetadata = new VersionedObjectMetadata();
ObjectMetadata objectMetadata = new ObjectMetadata(key, versionId, versionedObjectMetadata);
assertEquals(key, objectMetadata.getKey());
ObjectMetadata objectMetadata = new ObjectMetadata(versionId, versionedObjectMetadata);
assertSame(versionedObjectMetadata, objectMetadata.getLatest());
assertEquals(versionId, objectMetadata.getLatestVersion());

Expand All @@ -26,10 +24,9 @@ void getLatest() {

@Test
void serialize() {
String key = "a.txt";
String versionId = IdUtils.defaultGenerator().nextStrId();
VersionedObjectMetadata versionedObjectMetadata = new VersionedObjectMetadata();
ObjectMetadata objectMetadata = new ObjectMetadata(key, versionId, versionedObjectMetadata);
ObjectMetadata objectMetadata = new ObjectMetadata(versionId, versionedObjectMetadata);
String json = JsonUtils.toJson(objectMetadata);
ObjectMetadata serialized = JsonUtils.fromJson(json, ObjectMetadata.class);
assertEquals(objectMetadata, serialized);
Expand Down
Loading

0 comments on commit fe665f1

Please sign in to comment.