From 3d86906b2aefadae5f17d8f2725e969d4fc8d6d9 Mon Sep 17 00:00:00 2001 From: G8XSU <3442979+G8XSU@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:13:33 -0700 Subject: [PATCH] Add support for pluggable Authorizer, default to NoopAuthorizer. Different authorizers can act upon headers to return userToken or throw AuthException if auth fails. If hosting providers, want to support alternative auths or add additional checks in auth, they can just implement this interface. --- app/src/main/java/org/vss/KVStore.java | 8 +-- .../main/java/org/vss/api/AbstractVssApi.java | 7 ++- .../java/org/vss/api/DeleteObjectApi.java | 13 +++-- .../main/java/org/vss/api/GetObjectApi.java | 15 ++++-- .../java/org/vss/api/ListKeyVersionsApi.java | 13 +++-- .../main/java/org/vss/api/PutObjectsApi.java | 13 +++-- .../main/java/org/vss/auth/AuthResponse.java | 10 ++++ .../main/java/org/vss/auth/Authorizer.java | 9 ++++ .../java/org/vss/auth/NoopAuthorizer.java | 13 +++++ .../main/java/org/vss/guice/BaseModule.java | 6 +++ .../impl/postgres/PostgresBackendImpl.java | 52 +++++++++++-------- .../impl/postgres/sql/v0_create_vss_db.sql | 3 +- .../vss/AbstractKVStoreIntegrationTest.java | 11 ++-- .../java/org/vss/api/DeleteObjectApiTest.java | 23 +++++--- .../java/org/vss/api/GetObjectApiTest.java | 23 +++++--- .../org/vss/api/ListKeyVersionsApiTest.java | 23 +++++--- .../java/org/vss/api/PutObjectsApiTest.java | 23 +++++--- .../PostgresBackendImplIntegrationTest.java | 19 +++---- 18 files changed, 196 insertions(+), 88 deletions(-) create mode 100644 app/src/main/java/org/vss/auth/AuthResponse.java create mode 100644 app/src/main/java/org/vss/auth/Authorizer.java create mode 100644 app/src/main/java/org/vss/auth/NoopAuthorizer.java 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..1f94e69 100644 --- a/app/src/main/java/org/vss/api/AbstractVssApi.java +++ b/app/src/main/java/org/vss/api/AbstractVssApi.java @@ -7,17 +7,20 @@ 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..81e085d 100644 --- a/app/src/main/java/org/vss/api/GetObjectApi.java +++ b/app/src/main/java/org/vss/api/GetObjectApi.java @@ -4,28 +4,35 @@ 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..331bc71 --- /dev/null +++ b/app/src/main/java/org/vss/auth/NoopAuthorizer.java @@ -0,0 +1,13 @@ +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)" + + ");"); } }