diff --git a/app/src/main/java/org/vss/KVStore.java b/app/src/main/java/org/vss/KVStore.java index 23aae28..ea09991 100644 --- a/app/src/main/java/org/vss/KVStore.java +++ b/app/src/main/java/org/vss/KVStore.java @@ -4,11 +4,11 @@ public interface KVStore { String GLOBAL_VERSION_KEY = "vss_global_version"; - GetObjectResponse get(GetObjectRequest request); + GetObjectResponse get(String userToken, GetObjectRequest request); - PutObjectResponse put(PutObjectRequest request); + PutObjectResponse put(String userToken, PutObjectRequest request); - DeleteObjectResponse delete(DeleteObjectRequest request); + DeleteObjectResponse delete(String userToken, DeleteObjectRequest request); - ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request); + ListKeyVersionsResponse listKeyVersions(String userToken, ListKeyVersionsRequest request); } diff --git a/app/src/main/java/org/vss/api/AbstractVssApi.java b/app/src/main/java/org/vss/api/AbstractVssApi.java index de4a6ef..ba6f6fa 100644 --- a/app/src/main/java/org/vss/api/AbstractVssApi.java +++ b/app/src/main/java/org/vss/api/AbstractVssApi.java @@ -7,16 +7,19 @@ import org.vss.ErrorCode; import org.vss.ErrorResponse; import org.vss.KVStore; +import org.vss.auth.Authorizer; import org.vss.exception.AuthException; import org.vss.exception.ConflictException; import org.vss.exception.NoSuchKeyException; public abstract class AbstractVssApi { final KVStore kvStore; + final Authorizer authorizer; @Inject - public AbstractVssApi(KVStore kvStore) { + public AbstractVssApi(KVStore kvStore, Authorizer authorizer) { this.kvStore = kvStore; + this.authorizer = authorizer; } Response toResponse(GeneratedMessageV3 protoResponse) { diff --git a/app/src/main/java/org/vss/api/DeleteObjectApi.java b/app/src/main/java/org/vss/api/DeleteObjectApi.java index dd4b874..c803a90 100644 --- a/app/src/main/java/org/vss/api/DeleteObjectApi.java +++ b/app/src/main/java/org/vss/api/DeleteObjectApi.java @@ -4,27 +4,32 @@ import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import lombok.extern.slf4j.Slf4j; import org.vss.DeleteObjectRequest; import org.vss.DeleteObjectResponse; import org.vss.KVStore; +import org.vss.auth.AuthResponse; +import org.vss.auth.Authorizer; @Path(VssApiEndpoint.DELETE_OBJECT) @Slf4j public class DeleteObjectApi extends AbstractVssApi { @Inject - public DeleteObjectApi(KVStore kvstore) { - super(kvstore); + public DeleteObjectApi(KVStore kvstore, Authorizer authorizer) { + super(kvstore, authorizer); } @POST @Produces(MediaType.APPLICATION_OCTET_STREAM) - public Response execute(byte[] payload) { + public Response execute(byte[] payload, @Context HttpHeaders headers) { try { + AuthResponse authResponse = authorizer.verify(headers); DeleteObjectRequest request = DeleteObjectRequest.parseFrom(payload); - DeleteObjectResponse response = kvStore.delete(request); + DeleteObjectResponse response = kvStore.delete(authResponse.getUserToken(), request); return toResponse(response); } catch (Exception e) { log.error("Exception in DeleteObjectApi: ", e); diff --git a/app/src/main/java/org/vss/api/GetObjectApi.java b/app/src/main/java/org/vss/api/GetObjectApi.java index 9e0a358..cdc5bdd 100644 --- a/app/src/main/java/org/vss/api/GetObjectApi.java +++ b/app/src/main/java/org/vss/api/GetObjectApi.java @@ -4,28 +4,33 @@ import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import lombok.extern.slf4j.Slf4j; import org.vss.GetObjectRequest; import org.vss.GetObjectResponse; import org.vss.KVStore; +import org.vss.auth.AuthResponse; +import org.vss.auth.Authorizer; @Path(VssApiEndpoint.GET_OBJECT) @Slf4j public class GetObjectApi extends AbstractVssApi { @Inject - public GetObjectApi(KVStore kvstore) { - super(kvstore); + public GetObjectApi(KVStore kvstore, Authorizer authorizer) { + super(kvstore, authorizer); } @POST @Produces(MediaType.APPLICATION_OCTET_STREAM) - public Response execute(byte[] payload) { + public Response execute(byte[] payload, @Context HttpHeaders headers) { try { + AuthResponse authResponse = authorizer.verify(headers); GetObjectRequest request = GetObjectRequest.parseFrom(payload); - GetObjectResponse response = kvStore.get(request); + GetObjectResponse response = kvStore.get(authResponse.getUserToken(), request); return toResponse(response); } catch (Exception e) { log.error("Exception in GetObjectApi: ", e); diff --git a/app/src/main/java/org/vss/api/ListKeyVersionsApi.java b/app/src/main/java/org/vss/api/ListKeyVersionsApi.java index 0ce2237..3ac8ba7 100644 --- a/app/src/main/java/org/vss/api/ListKeyVersionsApi.java +++ b/app/src/main/java/org/vss/api/ListKeyVersionsApi.java @@ -4,28 +4,33 @@ import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import lombok.extern.slf4j.Slf4j; import org.vss.KVStore; import org.vss.ListKeyVersionsRequest; import org.vss.ListKeyVersionsResponse; +import org.vss.auth.AuthResponse; +import org.vss.auth.Authorizer; @Path(VssApiEndpoint.LIST_KEY_VERSIONS) @Slf4j public class ListKeyVersionsApi extends AbstractVssApi { @Inject - public ListKeyVersionsApi(KVStore kvStore) { - super(kvStore); + public ListKeyVersionsApi(KVStore kvStore, Authorizer authorizer) { + super(kvStore, authorizer); } @POST @Produces(MediaType.APPLICATION_OCTET_STREAM) - public Response execute(byte[] payload) { + public Response execute(byte[] payload, @Context HttpHeaders headers) { try { + AuthResponse authResponse = authorizer.verify(headers); ListKeyVersionsRequest request = ListKeyVersionsRequest.parseFrom(payload); - ListKeyVersionsResponse response = kvStore.listKeyVersions(request); + ListKeyVersionsResponse response = kvStore.listKeyVersions(authResponse.getUserToken(), request); return toResponse(response); } catch (Exception e) { log.error("Exception in ListKeyVersionsApi: ", e); diff --git a/app/src/main/java/org/vss/api/PutObjectsApi.java b/app/src/main/java/org/vss/api/PutObjectsApi.java index ab3689f..8fedc0e 100644 --- a/app/src/main/java/org/vss/api/PutObjectsApi.java +++ b/app/src/main/java/org/vss/api/PutObjectsApi.java @@ -4,28 +4,33 @@ import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import lombok.extern.slf4j.Slf4j; import org.vss.KVStore; import org.vss.PutObjectRequest; import org.vss.PutObjectResponse; +import org.vss.auth.AuthResponse; +import org.vss.auth.Authorizer; @Path(VssApiEndpoint.PUT_OBJECTS) @Slf4j public class PutObjectsApi extends AbstractVssApi { @Inject - public PutObjectsApi(KVStore kvStore) { - super(kvStore); + public PutObjectsApi(KVStore kvStore, Authorizer authorizer) { + super(kvStore, authorizer); } @POST @Produces(MediaType.APPLICATION_OCTET_STREAM) - public Response execute(byte[] payload) { + public Response execute(byte[] payload, @Context HttpHeaders headers) { try { + AuthResponse authResponse = authorizer.verify(headers); PutObjectRequest putObjectRequest = PutObjectRequest.parseFrom(payload); - PutObjectResponse response = kvStore.put(putObjectRequest); + PutObjectResponse response = kvStore.put(authResponse.getUserToken(), putObjectRequest); return toResponse(response); } catch (Exception e) { log.error("Exception in PutObjectsApi: ", e); diff --git a/app/src/main/java/org/vss/auth/AuthResponse.java b/app/src/main/java/org/vss/auth/AuthResponse.java new file mode 100644 index 0000000..ac4ec92 --- /dev/null +++ b/app/src/main/java/org/vss/auth/AuthResponse.java @@ -0,0 +1,10 @@ +package org.vss.auth; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class AuthResponse { + private String userToken; +} diff --git a/app/src/main/java/org/vss/auth/Authorizer.java b/app/src/main/java/org/vss/auth/Authorizer.java new file mode 100644 index 0000000..55bf548 --- /dev/null +++ b/app/src/main/java/org/vss/auth/Authorizer.java @@ -0,0 +1,9 @@ +package org.vss.auth; + +import jakarta.ws.rs.core.HttpHeaders; +import org.vss.exception.AuthException; + +// Interface for authorizer that is run before every request. +public interface Authorizer { + AuthResponse verify(HttpHeaders headers) throws AuthException; +} diff --git a/app/src/main/java/org/vss/auth/NoopAuthorizer.java b/app/src/main/java/org/vss/auth/NoopAuthorizer.java new file mode 100644 index 0000000..b9db3c3 --- /dev/null +++ b/app/src/main/java/org/vss/auth/NoopAuthorizer.java @@ -0,0 +1,14 @@ +package org.vss.auth; + +import jakarta.ws.rs.core.HttpHeaders; +import org.vss.exception.AuthException; + +// A no-operation authorizer, that lets any user-request go through. +public class NoopAuthorizer implements Authorizer { + private static String UNAUTHENTICATED_USER = "unauth-user"; + + @Override + public AuthResponse verify(HttpHeaders headers) throws AuthException { + return new AuthResponse(UNAUTHENTICATED_USER); + } +} diff --git a/app/src/main/java/org/vss/guice/BaseModule.java b/app/src/main/java/org/vss/guice/BaseModule.java index a33ccac..69a5d4f 100644 --- a/app/src/main/java/org/vss/guice/BaseModule.java +++ b/app/src/main/java/org/vss/guice/BaseModule.java @@ -11,7 +11,10 @@ import org.jooq.DSLContext; import org.jooq.SQLDialect; import org.jooq.impl.DSL; +import org.jooq.tools.StringUtils; import org.vss.KVStore; +import org.vss.auth.Authorizer; +import org.vss.auth.NoopAuthorizer; import org.vss.impl.postgres.PostgresBackendImpl; public class BaseModule extends AbstractModule { @@ -20,6 +23,9 @@ public class BaseModule extends AbstractModule { protected void configure() { // Provide PostgresBackend as default implementation for KVStore. bind(KVStore.class).to(PostgresBackendImpl.class).in(Singleton.class); + + // Default to Noop Authorizer. + bind(Authorizer.class).to(NoopAuthorizer.class).in(Singleton.class); } @Provides diff --git a/app/src/main/java/org/vss/impl/postgres/PostgresBackendImpl.java b/app/src/main/java/org/vss/impl/postgres/PostgresBackendImpl.java index 376fc07..c9330d8 100644 --- a/app/src/main/java/org/vss/impl/postgres/PostgresBackendImpl.java +++ b/app/src/main/java/org/vss/impl/postgres/PostgresBackendImpl.java @@ -48,11 +48,12 @@ public PostgresBackendImpl(DSLContext context) { } @Override - public GetObjectResponse get(GetObjectRequest request) { + public GetObjectResponse get(String userToken, GetObjectRequest request) { VssDbRecord vssDbRecord = context.selectFrom(VSS_DB) - .where(VSS_DB.STORE_ID.eq(request.getStoreId()) - .and(VSS_DB.KEY.eq(request.getKey()))) + .where(VSS_DB.USER_TOKEN.eq(userToken) + .and(VSS_DB.STORE_ID.eq(request.getStoreId()) + .and(VSS_DB.KEY.eq(request.getKey())))) .fetchOne(); final KeyValue keyValue; @@ -77,18 +78,18 @@ public GetObjectResponse get(GetObjectRequest request) { } @Override - public PutObjectResponse put(PutObjectRequest request) { + public PutObjectResponse put(String userToken, PutObjectRequest request) { String storeId = request.getStoreId(); List vssPutRecords = new ArrayList<>(request.getTransactionItemsList().stream() - .map(kv -> buildVssRecord(storeId, kv)).toList()); + .map(kv -> buildVssRecord(userToken, storeId, kv)).toList()); List vssDeleteRecords = new ArrayList<>(request.getDeleteItemsList().stream() - .map(kv -> buildVssRecord(storeId, kv)).toList()); + .map(kv -> buildVssRecord(userToken, storeId, kv)).toList()); if (request.hasGlobalVersion()) { - VssDbRecord globalVersionRecord = buildVssRecord(storeId, + VssDbRecord globalVersionRecord = buildVssRecord(userToken, storeId, KeyValue.newBuilder() .setKey(GLOBAL_VERSION_KEY) .setVersion(request.getGlobalVersion()) @@ -130,15 +131,17 @@ private Query buildDeleteObjectQuery(DSLContext dsl, VssDbRecord vssRecord) { private static DeleteConditionStep buildNonConditionalDeleteQuery(DSLContext dsl, VssDbRecord vssRecord) { - return dsl.deleteFrom(VSS_DB).where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId()) - .and(VSS_DB.KEY.eq(vssRecord.getKey()))); + return dsl.deleteFrom(VSS_DB).where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken()) + .and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId()) + .and(VSS_DB.KEY.eq(vssRecord.getKey())))); } private static DeleteConditionStep buildConditionalDeleteQuery(DSLContext dsl, VssDbRecord vssRecord) { - return dsl.deleteFrom(VSS_DB).where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId()) + return dsl.deleteFrom(VSS_DB).where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken()) + .and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId()) .and(VSS_DB.KEY.eq(vssRecord.getKey())) - .and(VSS_DB.VERSION.eq(vssRecord.getVersion()))); + .and(VSS_DB.VERSION.eq(vssRecord.getVersion())))); } private Query buildPutObjectQuery(DSLContext dsl, VssDbRecord vssRecord) { @@ -153,9 +156,9 @@ private Query buildPutObjectQuery(DSLContext dsl, VssDbRecord vssRecord) { private Query buildNonConditionalUpsertRecordQuery(DSLContext dsl, VssDbRecord vssRecord) { return dsl.insertInto(VSS_DB) - .values(vssRecord.getStoreId(), vssRecord.getKey(), + .values(vssRecord.getUserToken(), vssRecord.getStoreId(), vssRecord.getKey(), vssRecord.getValue(), 1, vssRecord.getCreatedAt(), vssRecord.getLastUpdatedAt()) - .onConflict(VSS_DB.STORE_ID, VSS_DB.KEY) + .onConflict(VSS_DB.USER_TOKEN, VSS_DB.STORE_ID, VSS_DB.KEY) .doUpdate() .set(VSS_DB.VALUE, vssRecord.getValue()) .set(VSS_DB.VERSION, 1L) @@ -165,7 +168,7 @@ private Query buildNonConditionalUpsertRecordQuery(DSLContext dsl, VssDbRecord v private Insert buildConditionalInsertRecordQuery(DSLContext dsl, VssDbRecord vssRecord) { return dsl.insertInto(VSS_DB) - .values(vssRecord.getStoreId(), vssRecord.getKey(), + .values(vssRecord.getUserToken(), vssRecord.getStoreId(), vssRecord.getKey(), vssRecord.getValue(), 1, vssRecord.getCreatedAt(), vssRecord.getLastUpdatedAt()) .onDuplicateKeyIgnore(); } @@ -175,14 +178,16 @@ private Update buildConditionalUpdateRecordQuery(DSLContext dsl, Vs .set(Map.of(VSS_DB.VALUE, vssRecord.getValue(), VSS_DB.VERSION, vssRecord.getVersion() + 1, VSS_DB.LAST_UPDATED_AT, vssRecord.getLastUpdatedAt())) - .where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId()) + .where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken()) + .and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId()) .and(VSS_DB.KEY.eq(vssRecord.getKey())) - .and(VSS_DB.VERSION.eq(vssRecord.getVersion()))); + .and(VSS_DB.VERSION.eq(vssRecord.getVersion())))); } - private VssDbRecord buildVssRecord(String storeId, KeyValue kv) { + private VssDbRecord buildVssRecord(String userToken, String storeId, KeyValue kv) { OffsetDateTime today = OffsetDateTime.now(ZoneOffset.UTC).truncatedTo(ChronoUnit.DAYS); return new VssDbRecord() + .setUserToken(userToken) .setStoreId(storeId) .setKey(kv.getKey()) .setValue(kv.getValue().toByteArray()) @@ -192,9 +197,9 @@ private VssDbRecord buildVssRecord(String storeId, KeyValue kv) { } @Override - public DeleteObjectResponse delete(DeleteObjectRequest request) { + public DeleteObjectResponse delete(String userToken, DeleteObjectRequest request) { String storeId = request.getStoreId(); - VssDbRecord vssDbRecord = buildVssRecord(storeId, request.getKeyValue()); + VssDbRecord vssDbRecord = buildVssRecord(userToken, storeId, request.getKeyValue()); context.transaction((ctx) -> { DSLContext dsl = ctx.dsl(); @@ -206,7 +211,7 @@ public DeleteObjectResponse delete(DeleteObjectRequest request) { } @Override - public ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request) { + public ListKeyVersionsResponse listKeyVersions(String userToken, ListKeyVersionsRequest request) { String storeId = request.getStoreId(); String keyPrefix = request.getKeyPrefix(); String pageToken = request.getPageToken(); @@ -221,12 +226,13 @@ public ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request) { .setStoreId(storeId) .setKey(GLOBAL_VERSION_KEY) .build(); - globalVersion = get(getGlobalVersionRequest).getValue().getVersion(); + globalVersion = get(userToken, getGlobalVersionRequest).getValue().getVersion(); } List vssDbRecords = context.select(VSS_DB.KEY, VSS_DB.VERSION).from(VSS_DB) - .where(VSS_DB.STORE_ID.eq(storeId) - .and(VSS_DB.KEY.startsWith(keyPrefix))) + .where(VSS_DB.USER_TOKEN.eq(userToken) + .and(VSS_DB.STORE_ID.eq(storeId) + .and(VSS_DB.KEY.startsWith(keyPrefix)))) .orderBy(VSS_DB.KEY) .seek(pageToken) .limit(Math.min(pageSize, LIST_KEY_VERSIONS_MAX_PAGE_SIZE)) diff --git a/app/src/main/java/org/vss/impl/postgres/sql/v0_create_vss_db.sql b/app/src/main/java/org/vss/impl/postgres/sql/v0_create_vss_db.sql index 4051bca..8d91c25 100644 --- a/app/src/main/java/org/vss/impl/postgres/sql/v0_create_vss_db.sql +++ b/app/src/main/java/org/vss/impl/postgres/sql/v0_create_vss_db.sql @@ -1,9 +1,10 @@ CREATE TABLE vss_db ( + user_token character varying(120) NOT NULL CHECK (user_token <> ''), store_id character varying(120) NOT NULL CHECK (store_id <> ''), key character varying(600) NOT NULL, value bytea NULL, version bigint NOT NULL, created_at TIMESTAMP WITH TIME ZONE, last_updated_at TIMESTAMP WITH TIME ZONE, - PRIMARY KEY (store_id, key) + PRIMARY KEY (user_token, store_id, key) ); diff --git a/app/src/test/java/org/vss/AbstractKVStoreIntegrationTest.java b/app/src/test/java/org/vss/AbstractKVStoreIntegrationTest.java index 3f05d50..5687cb4 100644 --- a/app/src/test/java/org/vss/AbstractKVStoreIntegrationTest.java +++ b/app/src/test/java/org/vss/AbstractKVStoreIntegrationTest.java @@ -23,6 +23,7 @@ public abstract class AbstractKVStoreIntegrationTest { + private final String USER_TOKEN = "userToken"; private final String STORE_ID = "storeId"; protected KVStore kvStore; @@ -426,7 +427,7 @@ private KeyValue getObject(String key) { .setStoreId(STORE_ID) .setKey(key) .build(); - return this.kvStore.get(getRequest).getValue(); + return this.kvStore.get(USER_TOKEN, getRequest).getValue(); } private void putObjects(@Nullable Long globalVersion, List keyValues) { @@ -438,7 +439,7 @@ private void putObjects(@Nullable Long globalVersion, List keyValues) putObjectRequestBuilder.setGlobalVersion(globalVersion); } - this.kvStore.put(putObjectRequestBuilder.build()); + this.kvStore.put(USER_TOKEN, putObjectRequestBuilder.build()); } private void putAndDeleteObjects(@Nullable Long globalVersion, List putKeyValues, List deleteKeyValues) { @@ -451,13 +452,13 @@ private void putAndDeleteObjects(@Nullable Long globalVersion, List pu putObjectRequestBuilder.setGlobalVersion(globalVersion); } - this.kvStore.put(putObjectRequestBuilder.build()); + this.kvStore.put(USER_TOKEN, putObjectRequestBuilder.build()); } private void deleteObject(KeyValue keyValue) { DeleteObjectRequest request = DeleteObjectRequest.newBuilder() .setStoreId(STORE_ID).setKeyValue(keyValue).build(); - this.kvStore.delete(request); + this.kvStore.delete(USER_TOKEN, request); } private ListKeyVersionsResponse list(@Nullable String nextPageToken, @Nullable Integer pageSize, @@ -475,7 +476,7 @@ private ListKeyVersionsResponse list(@Nullable String nextPageToken, @Nullable I listRequestBuilder.setKeyPrefix(keyPrefix); } - return this.kvStore.listKeyVersions(listRequestBuilder.build()); + return this.kvStore.listKeyVersions(USER_TOKEN, listRequestBuilder.build()); } private KeyValue kv(String key, String value, int version) { diff --git a/app/src/test/java/org/vss/api/DeleteObjectApiTest.java b/app/src/test/java/org/vss/api/DeleteObjectApiTest.java index 9d85206..044b60d 100644 --- a/app/src/test/java/org/vss/api/DeleteObjectApiTest.java +++ b/app/src/test/java/org/vss/api/DeleteObjectApiTest.java @@ -1,6 +1,7 @@ package org.vss.api; import com.google.protobuf.ByteString; +import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; import java.nio.charset.StandardCharsets; import java.util.stream.Stream; @@ -15,6 +16,8 @@ import org.vss.ErrorResponse; import org.vss.KVStore; import org.vss.KeyValue; +import org.vss.auth.AuthResponse; +import org.vss.auth.Authorizer; import org.vss.exception.ConflictException; import static org.hamcrest.MatcherAssert.assertThat; @@ -27,7 +30,10 @@ public class DeleteObjectApiTest { private DeleteObjectApi deleteObjectApi; private KVStore mockKVStore; + private Authorizer mockAuthorizer; + private HttpHeaders mockHeaders; + private static String TEST_USER_TOKEN = "userToken"; private static String TEST_STORE_ID = "storeId"; private static String TEST_KEY = "key"; private static KeyValue TEST_KV = KeyValue.newBuilder().setKey(TEST_KEY).setValue( @@ -36,7 +42,10 @@ public class DeleteObjectApiTest { @BeforeEach void setUp() { mockKVStore = mock(KVStore.class); - deleteObjectApi = new DeleteObjectApi(mockKVStore); + mockAuthorizer = mock(Authorizer.class); + deleteObjectApi = new DeleteObjectApi(mockKVStore, mockAuthorizer); + mockHeaders = mock(HttpHeaders.class); + when(mockAuthorizer.verify(any())).thenReturn(new AuthResponse(TEST_USER_TOKEN)); } @Test @@ -47,13 +56,13 @@ void execute_ValidPayload_ReturnsResponse() { ).build(); byte[] payload = expectedRequest.toByteArray(); DeleteObjectResponse mockResponse = DeleteObjectResponse.newBuilder().build(); - when(mockKVStore.delete(expectedRequest)).thenReturn(mockResponse); + when(mockKVStore.delete(TEST_USER_TOKEN, expectedRequest)).thenReturn(mockResponse); - Response actualResponse = deleteObjectApi.execute(payload); + Response actualResponse = deleteObjectApi.execute(payload, mockHeaders); assertThat(actualResponse.getStatus(), is(Response.Status.OK.getStatusCode())); assertThat(actualResponse.getEntity(), is(mockResponse.toByteArray())); - verify(mockKVStore).delete(expectedRequest); + verify(mockKVStore).delete(TEST_USER_TOKEN, expectedRequest); } @ParameterizedTest @@ -65,9 +74,9 @@ void execute_InvalidPayload_ReturnsErrorResponse(Exception exception, KeyValue.newBuilder().setKey(TEST_KEY).setVersion(0) ).build(); byte[] payload = expectedRequest.toByteArray(); - when(mockKVStore.delete(any())).thenThrow(exception); + when(mockKVStore.delete(any(), any())).thenThrow(exception); - Response response = deleteObjectApi.execute(payload); + Response response = deleteObjectApi.execute(payload, mockHeaders); ErrorResponse expectedErrorResponse = ErrorResponse.newBuilder() .setErrorCode(errorCode) @@ -75,7 +84,7 @@ void execute_InvalidPayload_ReturnsErrorResponse(Exception exception, .build(); assertThat(response.getEntity(), is(expectedErrorResponse.toByteArray())); assertThat(response.getStatus(), is(statusCode)); - verify(mockKVStore).delete(expectedRequest); + verify(mockKVStore).delete(TEST_USER_TOKEN, expectedRequest); } private static Stream provideErrorTestCases() { diff --git a/app/src/test/java/org/vss/api/GetObjectApiTest.java b/app/src/test/java/org/vss/api/GetObjectApiTest.java index 6324ddf..60b7d11 100644 --- a/app/src/test/java/org/vss/api/GetObjectApiTest.java +++ b/app/src/test/java/org/vss/api/GetObjectApiTest.java @@ -1,6 +1,7 @@ package org.vss.api; import com.google.protobuf.ByteString; +import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; import java.nio.charset.StandardCharsets; import java.util.stream.Stream; @@ -15,6 +16,8 @@ import org.vss.GetObjectResponse; import org.vss.KVStore; import org.vss.KeyValue; +import org.vss.auth.AuthResponse; +import org.vss.auth.Authorizer; import org.vss.exception.ConflictException; import org.vss.exception.NoSuchKeyException; @@ -28,7 +31,10 @@ class GetObjectApiTest { private GetObjectApi getObjectApi; private KVStore mockKVStore; + private Authorizer mockAuthorizer; + private HttpHeaders mockHeaders; + private static String TEST_USER_TOKEN = "userToken"; private static String TEST_STORE_ID = "storeId"; private static String TEST_KEY = "key"; private static KeyValue TEST_KV = KeyValue.newBuilder().setKey(TEST_KEY).setValue( @@ -37,7 +43,10 @@ class GetObjectApiTest { @BeforeEach void setUp() { mockKVStore = mock(KVStore.class); - getObjectApi = new GetObjectApi(mockKVStore); + mockAuthorizer = mock(Authorizer.class); + getObjectApi = new GetObjectApi(mockKVStore, mockAuthorizer); + mockHeaders = mock(HttpHeaders.class); + when(mockAuthorizer.verify(any())).thenReturn(new AuthResponse(TEST_USER_TOKEN)); } @Test @@ -46,13 +55,13 @@ void execute_ValidPayload_ReturnsResponse() { GetObjectRequest.newBuilder().setStoreId(TEST_STORE_ID).setKey(TEST_KEY).build(); byte[] payload = expectedRequest.toByteArray(); GetObjectResponse mockResponse = GetObjectResponse.newBuilder().setValue(TEST_KV).build(); - when(mockKVStore.get(expectedRequest)).thenReturn(mockResponse); + when(mockKVStore.get(TEST_USER_TOKEN, expectedRequest)).thenReturn(mockResponse); - Response actualResponse = getObjectApi.execute(payload); + Response actualResponse = getObjectApi.execute(payload, mockHeaders); assertThat(actualResponse.getStatus(), is(Response.Status.OK.getStatusCode())); assertThat(actualResponse.getEntity(), is(mockResponse.toByteArray())); - verify(mockKVStore).get(expectedRequest); + verify(mockKVStore).get(TEST_USER_TOKEN, expectedRequest); } @ParameterizedTest @@ -64,9 +73,9 @@ void execute_InvalidPayload_ReturnsErrorResponse(Exception exception, .setKey(TEST_KEY) .build(); byte[] payload = expectedRequest.toByteArray(); - when(mockKVStore.get(any())).thenThrow(exception); + when(mockKVStore.get(any(), any())).thenThrow(exception); - Response response = getObjectApi.execute(payload); + Response response = getObjectApi.execute(payload, mockHeaders); ErrorResponse expectedErrorResponse = ErrorResponse.newBuilder() .setErrorCode(errorCode) @@ -74,7 +83,7 @@ void execute_InvalidPayload_ReturnsErrorResponse(Exception exception, .build(); assertThat(response.getEntity(), is(expectedErrorResponse.toByteArray())); assertThat(response.getStatus(), is(statusCode)); - verify(mockKVStore).get(expectedRequest); + verify(mockKVStore).get(TEST_USER_TOKEN, expectedRequest); } private static Stream provideErrorTestCases() { diff --git a/app/src/test/java/org/vss/api/ListKeyVersionsApiTest.java b/app/src/test/java/org/vss/api/ListKeyVersionsApiTest.java index 2f27c6a..8a4110c 100644 --- a/app/src/test/java/org/vss/api/ListKeyVersionsApiTest.java +++ b/app/src/test/java/org/vss/api/ListKeyVersionsApiTest.java @@ -1,6 +1,7 @@ package org.vss.api; import com.google.protobuf.ByteString; +import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; import java.nio.charset.StandardCharsets; import java.util.List; @@ -16,6 +17,8 @@ import org.vss.KeyValue; import org.vss.ListKeyVersionsRequest; import org.vss.ListKeyVersionsResponse; +import org.vss.auth.AuthResponse; +import org.vss.auth.Authorizer; import org.vss.exception.ConflictException; import static org.hamcrest.MatcherAssert.assertThat; @@ -28,7 +31,10 @@ public class ListKeyVersionsApiTest { private ListKeyVersionsApi listKeyVersionsApi; private KVStore mockKVStore; + private Authorizer mockAuthorizer; + private HttpHeaders mockHeaders; + private static String TEST_USER_TOKEN = "userToken"; private static String TEST_STORE_ID = "storeId"; private static String TEST_KEY = "key"; private static KeyValue TEST_KV = KeyValue.newBuilder().setKey(TEST_KEY).setValue( @@ -37,7 +43,10 @@ public class ListKeyVersionsApiTest { @BeforeEach void setUp() { mockKVStore = mock(KVStore.class); - listKeyVersionsApi = new ListKeyVersionsApi(mockKVStore); + mockAuthorizer = mock(Authorizer.class); + listKeyVersionsApi = new ListKeyVersionsApi(mockKVStore, mockAuthorizer); + mockHeaders = mock(HttpHeaders.class); + when(mockAuthorizer.verify(any())).thenReturn(new AuthResponse(TEST_USER_TOKEN)); } @Test @@ -50,13 +59,13 @@ void execute_ValidPayload_ReturnsResponse() { byte[] payload = expectedRequest.toByteArray(); ListKeyVersionsResponse mockResponse = ListKeyVersionsResponse.newBuilder().addAllKeyVersions( List.of(TEST_KV)).build(); - when(mockKVStore.listKeyVersions(expectedRequest)).thenReturn(mockResponse); + when(mockKVStore.listKeyVersions(TEST_USER_TOKEN, expectedRequest)).thenReturn(mockResponse); - Response actualResponse = listKeyVersionsApi.execute(payload); + Response actualResponse = listKeyVersionsApi.execute(payload, mockHeaders); assertThat(actualResponse.getStatus(), is(Response.Status.OK.getStatusCode())); assertThat(actualResponse.getEntity(), is(mockResponse.toByteArray())); - verify(mockKVStore).listKeyVersions(expectedRequest); + verify(mockKVStore).listKeyVersions(TEST_USER_TOKEN, expectedRequest); } @ParameterizedTest @@ -69,9 +78,9 @@ void execute_InvalidPayload_ReturnsErrorResponse(Exception exception, .setKeyPrefix(TEST_KEY) .build(); byte[] payload = expectedRequest.toByteArray(); - when(mockKVStore.listKeyVersions(any())).thenThrow(exception); + when(mockKVStore.listKeyVersions(any(), any())).thenThrow(exception); - Response response = listKeyVersionsApi.execute(payload); + Response response = listKeyVersionsApi.execute(payload, mockHeaders); ErrorResponse expectedErrorResponse = ErrorResponse.newBuilder() .setErrorCode(errorCode) @@ -79,7 +88,7 @@ void execute_InvalidPayload_ReturnsErrorResponse(Exception exception, .build(); assertThat(response.getEntity(), is(expectedErrorResponse.toByteArray())); assertThat(response.getStatus(), is(statusCode)); - verify(mockKVStore).listKeyVersions(expectedRequest); + verify(mockKVStore).listKeyVersions(TEST_USER_TOKEN, expectedRequest); } private static Stream provideErrorTestCases() { diff --git a/app/src/test/java/org/vss/api/PutObjectsApiTest.java b/app/src/test/java/org/vss/api/PutObjectsApiTest.java index 9a7316b..2bfe077 100644 --- a/app/src/test/java/org/vss/api/PutObjectsApiTest.java +++ b/app/src/test/java/org/vss/api/PutObjectsApiTest.java @@ -1,6 +1,7 @@ package org.vss.api; import com.google.protobuf.ByteString; +import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; import java.nio.charset.StandardCharsets; import java.util.List; @@ -16,6 +17,8 @@ import org.vss.KeyValue; import org.vss.PutObjectRequest; import org.vss.PutObjectResponse; +import org.vss.auth.AuthResponse; +import org.vss.auth.Authorizer; import org.vss.exception.ConflictException; import static org.hamcrest.MatcherAssert.assertThat; @@ -28,7 +31,10 @@ public class PutObjectsApiTest { private PutObjectsApi putObjectsApi; private KVStore mockKVStore; + private Authorizer mockAuthorizer; + private HttpHeaders mockHeaders; + private static String TEST_USER_TOKEN = "userToken"; private static String TEST_STORE_ID = "storeId"; private static String TEST_KEY = "key"; private static KeyValue TEST_KV = KeyValue.newBuilder().setKey(TEST_KEY).setValue( @@ -37,7 +43,10 @@ public class PutObjectsApiTest { @BeforeEach void setUp() { mockKVStore = mock(KVStore.class); - putObjectsApi = new PutObjectsApi(mockKVStore); + mockAuthorizer = mock(Authorizer.class); + putObjectsApi = new PutObjectsApi(mockKVStore, mockAuthorizer); + mockHeaders = mock(HttpHeaders.class); + when(mockAuthorizer.verify(any())).thenReturn(new AuthResponse(TEST_USER_TOKEN)); } @Test @@ -49,13 +58,13 @@ void execute_ValidPayload_ReturnsResponse() { .build(); byte[] payload = expectedRequest.toByteArray(); PutObjectResponse mockResponse = PutObjectResponse.newBuilder().build(); - when(mockKVStore.put(expectedRequest)).thenReturn(mockResponse); + when(mockKVStore.put(TEST_USER_TOKEN, expectedRequest)).thenReturn(mockResponse); - Response actualResponse = putObjectsApi.execute(payload); + Response actualResponse = putObjectsApi.execute(payload, mockHeaders); assertThat(actualResponse.getStatus(), is(Response.Status.OK.getStatusCode())); assertThat(actualResponse.getEntity(), is(mockResponse.toByteArray())); - verify(mockKVStore).put(expectedRequest); + verify(mockKVStore).put(TEST_USER_TOKEN, expectedRequest); } @ParameterizedTest @@ -68,9 +77,9 @@ void execute_InvalidPayload_ReturnsErrorResponse(Exception exception, .addAllTransactionItems(List.of(TEST_KV)) .build(); byte[] payload = expectedRequest.toByteArray(); - when(mockKVStore.put(any())).thenThrow(exception); + when(mockKVStore.put(any(), any())).thenThrow(exception); - Response response = putObjectsApi.execute(payload); + Response response = putObjectsApi.execute(payload, mockHeaders); ErrorResponse expectedErrorResponse = ErrorResponse.newBuilder() .setErrorCode(errorCode) @@ -78,7 +87,7 @@ void execute_InvalidPayload_ReturnsErrorResponse(Exception exception, .build(); assertThat(response.getEntity(), is(expectedErrorResponse.toByteArray())); assertThat(response.getStatus(), is(statusCode)); - verify(mockKVStore).put(expectedRequest); + verify(mockKVStore).put(TEST_USER_TOKEN, expectedRequest); } private static Stream provideErrorTestCases() { diff --git a/app/src/test/java/org/vss/impl/postgres/PostgresBackendImplIntegrationTest.java b/app/src/test/java/org/vss/impl/postgres/PostgresBackendImplIntegrationTest.java index 57b5197..238ffde 100644 --- a/app/src/test/java/org/vss/impl/postgres/PostgresBackendImplIntegrationTest.java +++ b/app/src/test/java/org/vss/impl/postgres/PostgresBackendImplIntegrationTest.java @@ -46,14 +46,15 @@ void destroy() throws Exception { } private void createTable(DSLContext dslContext) { - dslContext.execute("CREATE TABLE vss_db (" - + "store_id character varying(120) NOT NULL CHECK (store_id <> '')," - + "key character varying(600) NOT NULL," - + "value bytea NULL," - + "version bigint NOT NULL," - + "created_at TIMESTAMP WITH TIME ZONE," - + "last_updated_at TIMESTAMP WITH TIME ZONE," - + "PRIMARY KEY (store_id, key)" - + ");"); + dslContext.execute("CREATE TABLE vss_db (" + + "user_token character varying(120) NOT NULL CHECK (user_token <> '')," + + "store_id character varying(120) NOT NULL CHECK (store_id <> '')," + + "key character varying(600) NOT NULL," + + "value bytea NULL," + + "version bigint NOT NULL," + + "created_at TIMESTAMP WITH TIME ZONE," + + "last_updated_at TIMESTAMP WITH TIME ZONE," + + "PRIMARY KEY (user_token, store_id, key)" + + ");"); } }