diff --git a/server/app/src/main/java/io/whitefox/api/deltasharing/server/DeltaSharesApiImpl.java b/server/app/src/main/java/io/whitefox/api/deltasharing/server/DeltaSharesApiImpl.java index eceda2e08..ecffa9b4d 100644 --- a/server/app/src/main/java/io/whitefox/api/deltasharing/server/DeltaSharesApiImpl.java +++ b/server/app/src/main/java/io/whitefox/api/deltasharing/server/DeltaSharesApiImpl.java @@ -5,10 +5,7 @@ import io.whitefox.api.deltasharing.ClientCapabilitiesMapper; import io.whitefox.api.deltasharing.DeltaMappers; import io.whitefox.api.deltasharing.encoders.DeltaPageTokenEncoder; -import io.whitefox.api.deltasharing.model.v1.generated.ListSchemasResponse; -import io.whitefox.api.deltasharing.model.v1.generated.ListShareResponse; -import io.whitefox.api.deltasharing.model.v1.generated.ListTablesResponse; -import io.whitefox.api.deltasharing.model.v1.generated.QueryRequest; +import io.whitefox.api.deltasharing.model.v1.generated.*; import io.whitefox.api.deltasharing.serializers.TableMetadataSerializer; import io.whitefox.api.deltasharing.serializers.TableQueryResponseSerializer; import io.whitefox.api.deltasharing.server.v1.generated.DeltaApiApi; @@ -51,8 +48,10 @@ public DeltaSharesApiImpl( @Override public Response getShare(String share) { return wrapExceptions( - () -> - optionalToNotFound(shareService.getShare(share), s -> Response.ok(s).build()), + () -> optionalToNotFound(shareService.getShare(share), s -> { + var resultShare = new Share().name(s.name()).id(s.id()); + return Response.ok(resultShare).build(); + }), exceptionToResponse); } @@ -85,9 +84,15 @@ public Response getTableMetadata( clientCapabilitiesMapper.parseDeltaSharingCapabilities(deltaSharingCapabilities); return optionalToNotFound( deltaSharesService.getTableMetadata( - share, schema, table, startingTimestamp, clientCapabilities), + share, + schema, + table, + startingTimestamp, + clientCapabilities, + getRequestPrincipal()), m -> optionalToNotFound( - deltaSharesService.getTableVersion(share, schema, table, startingTimestamp), + deltaSharesService.getTableVersion( + share, schema, table, startingTimestamp, getRequestPrincipal()), v -> Response.ok( tableResponseSerializer.serialize( DeltaMappers.toTableResponseMetadata(m)), @@ -104,12 +109,12 @@ public Response getTableMetadata( @Override public Response getTableVersion( String share, String schema, String table, String startingTimestampStr) { - return wrapExceptions( () -> { var startingTimestamp = parseTimestamp(startingTimestampStr); return optionalToNotFound( - deltaSharesService.getTableVersion(share, schema, table, startingTimestamp), + deltaSharesService.getTableVersion( + share, schema, table, startingTimestamp, getRequestPrincipal()), t -> Response.ok().header(DELTA_TABLE_VERSION_HEADER, t).build()); }, exceptionToResponse); @@ -120,7 +125,10 @@ public Response listALLTables(String share, Integer maxResults, String pageToken return wrapExceptions( () -> optionalToNotFound( deltaSharesService.listTablesOfShare( - share, parseToken(pageToken), Optional.ofNullable(maxResults)), + share, + parseToken(pageToken), + Optional.ofNullable(maxResults), + getRequestPrincipal()), c -> Response.ok(c.getToken() .map(t -> new ListTablesResponse() .items(mapList(c.getContent(), DeltaMappers::table2api)) @@ -136,7 +144,11 @@ public Response listSchemas(String share, Integer maxResults, String pageToken) return wrapExceptions( () -> optionalToNotFound( deltaSharesService - .listSchemas(share, parseToken(pageToken), Optional.ofNullable(maxResults)) + .listSchemas( + share, + parseToken(pageToken), + Optional.ofNullable(maxResults), + getRequestPrincipal()) .map(ct -> ct.getToken() .map(t -> new ListSchemasResponse() .nextPageToken(tokenEncoder.encodePageToken(t)) @@ -151,8 +163,8 @@ public Response listSchemas(String share, Integer maxResults, String pageToken) public Response listShares(Integer maxResults, String pageToken) { return wrapExceptions( () -> { - var c = - deltaSharesService.listShares(parseToken(pageToken), Optional.ofNullable(maxResults)); + var c = deltaSharesService.listShares( + parseToken(pageToken), Optional.ofNullable(maxResults), getRequestPrincipal()); var response = new ListShareResponse().items(mapList(c.getContent(), DeltaMappers::share2api)); return Response.ok(c.getToken() @@ -168,7 +180,11 @@ public Response listTables(String share, String schema, Integer maxResults, Stri return wrapExceptions( () -> optionalToNotFound( deltaSharesService.listTables( - share, schema, parseToken(pageToken), Optional.ofNullable(maxResults)), + share, + schema, + parseToken(pageToken), + Optional.ofNullable(maxResults), + getRequestPrincipal()), c -> Response.ok(c.getToken() .map(t -> new ListTablesResponse() .items(mapList(c.getContent(), DeltaMappers::table2api)) @@ -201,7 +217,8 @@ public Response queryTable( schema, table, DeltaMappers.api2ReadTableRequest(queryRequest), - clientCapabilitiesMapper.parseDeltaSharingCapabilities(deltaSharingCapabilities)); + clientCapabilitiesMapper.parseDeltaSharingCapabilities(deltaSharingCapabilities), + getRequestPrincipal()); var serializedReadResult = tableQueryResponseSerializer.serialize(DeltaMappers.readTableResult2api(readResult)); return Response.ok(serializedReadResult, ndjsonMediaType) diff --git a/server/app/src/main/java/io/whitefox/api/server/ApiUtils.java b/server/app/src/main/java/io/whitefox/api/server/ApiUtils.java index 8ef2a5f7d..446ff0c27 100644 --- a/server/app/src/main/java/io/whitefox/api/server/ApiUtils.java +++ b/server/app/src/main/java/io/whitefox/api/server/ApiUtils.java @@ -5,6 +5,7 @@ import io.whitefox.core.Principal; import io.whitefox.core.services.exceptions.AlreadyExists; import io.whitefox.core.services.exceptions.NotFound; +import jakarta.ws.rs.ForbiddenException; import jakarta.ws.rs.core.Response; import java.sql.Timestamp; import java.time.OffsetDateTime; @@ -45,6 +46,12 @@ public interface ApiUtils extends DeltaHeaders { .errorCode("BAD REQUEST - timestamp provided is not formatted correctly") .message(ExceptionUtil.generateStackTrace(t))) .build(); + } else if (t instanceof ForbiddenException) { + return Response.status(Response.Status.FORBIDDEN) + .entity(new CommonErrorResponse() + .errorCode("FORBIDDEN ACCESS") + .message(ExceptionUtil.generateStackTrace(t))) + .build(); } else { return Response.status(Response.Status.BAD_GATEWAY) .entity(new CommonErrorResponse() @@ -68,6 +75,12 @@ default Response notFoundResponse() { .build(); } + default Response forbiddenResponse() { + return Response.status(Response.Status.FORBIDDEN) + .entity(new CommonErrorResponse().errorCode("2").message("UNAUTHORIZED ACCESS")) + .build(); + } + default Response optionalToNotFound(Optional opt, Function fn) { return opt.map(fn).orElse(notFoundResponse()); } diff --git a/server/app/src/test/java/io/whitefox/api/deltasharing/SampleTables.java b/server/app/src/test/java/io/whitefox/api/deltasharing/SampleTables.java index 2312aeb41..1f0b56c70 100644 --- a/server/app/src/test/java/io/whitefox/api/deltasharing/SampleTables.java +++ b/server/app/src/test/java/io/whitefox/api/deltasharing/SampleTables.java @@ -55,22 +55,39 @@ public static InternalTable s3IcebergTable1( public static final InternalTable deltaTableWithHistory1 = deltaTable("delta-table-with-history"); public static StorageManager createStorageManager() { - return new InMemoryStorageManager(List.of(new io.whitefox.core.Share( - "name", - "key", - Map.of( - "default", - new io.whitefox.core.Schema( + return new InMemoryStorageManager(List.of( + new io.whitefox.core.Share( + "name", + "key", + Map.of( "default", - List.of( - new SharedTable("table1", "default", "name", deltaTable1), - new SharedTable( - "table-with-history", "default", "name", deltaTableWithHistory1), - new SharedTable("icebergtable1", "default", "name", icebergtable1), - new SharedTable("icebergtable2", "default", "name", icebergtable2)), - "name")), - testPrincipal, - 0L))); + new io.whitefox.core.Schema( + "default", + List.of( + new SharedTable("table1", "default", "name", deltaTable1), + new SharedTable( + "table-with-history", "default", "name", deltaTableWithHistory1), + new SharedTable("icebergtable1", "default", "name", icebergtable1), + new SharedTable("icebergtable2", "default", "name", icebergtable2)), + "name")), + testPrincipal, + 0L), + new io.whitefox.core.Share( + "noauthShare", + "key", + Map.of( + "default", + new io.whitefox.core.Schema( + "default", + List.of( + new SharedTable("table1", "default", "name", deltaTable1), + new SharedTable( + "table-with-history", "default", "name", deltaTableWithHistory1), + new SharedTable("icebergtable1", "default", "name", icebergtable1), + new SharedTable("icebergtable2", "default", "name", icebergtable2)), + "name")), + new Principal("Mr. White"), + 0L))); } public static final ParquetMetadata deltaTable1Metadata = ParquetMetadata.builder() diff --git a/server/app/src/test/java/io/whitefox/api/deltasharing/server/DeltaSharesApiImplAwsTest.java b/server/app/src/test/java/io/whitefox/api/deltasharing/server/DeltaSharesApiImplAwsTest.java index 1c0f3341d..1d846a97f 100644 --- a/server/app/src/test/java/io/whitefox/api/deltasharing/server/DeltaSharesApiImplAwsTest.java +++ b/server/app/src/test/java/io/whitefox/api/deltasharing/server/DeltaSharesApiImplAwsTest.java @@ -91,7 +91,7 @@ public void updateStorageManagerWithS3Tables() { "s3share", s3IcebergTable1(s3TestConfig, awsGlueTestConfig))), "s3share")), - new Principal("Mr fox"), + new Principal("Mr. Fox"), 0L)); } diff --git a/server/app/src/test/java/io/whitefox/api/deltasharing/server/DeltaSharesApiImplTest.java b/server/app/src/test/java/io/whitefox/api/deltasharing/server/DeltaSharesApiImplTest.java index b0c9ea4e1..1b5a2b76c 100644 --- a/server/app/src/test/java/io/whitefox/api/deltasharing/server/DeltaSharesApiImplTest.java +++ b/server/app/src/test/java/io/whitefox/api/deltasharing/server/DeltaSharesApiImplTest.java @@ -108,6 +108,16 @@ public void listNotFoundSchemas() { .body("message", is("NOT FOUND")); } + @Test + public void listSchemasNoAuth() { + given() + .when() + .filter(deltaFilter) + .get("delta-api/v1/shares/{share}/schemas", "noauthShare") + .then() + .statusCode(403); + } + @Test public void listSchemas() { given() diff --git a/server/app/src/test/java/io/whitefox/api/server/ShareV1ApiImplTest.java b/server/app/src/test/java/io/whitefox/api/server/ShareV1ApiImplTest.java index 7f2e2ee03..0586d592c 100644 --- a/server/app/src/test/java/io/whitefox/api/server/ShareV1ApiImplTest.java +++ b/server/app/src/test/java/io/whitefox/api/server/ShareV1ApiImplTest.java @@ -57,7 +57,7 @@ void createShare() { .statusCode(201) .body("name", is("share1")) .body("comment", is(nullValue())) - .body("recipients", is(hasSize(0))) + .body("recipients", is(hasSize(1))) .body("schemas", is(hasSize(0))) .body("createdAt", is(0)) .body("createdBy", is("Mr. Fox")) @@ -89,7 +89,7 @@ void addRecipientsToShare() { .statusCode(200) .body("name", is("share1")) .body("comment", is(nullValue())) - .body("recipients", is(hasSize(3))) + .body("recipients", is(hasSize(4))) .body("schemas", is(hasSize(0))) .body("createdAt", is(0)) .body("createdBy", is("Mr. Fox")) @@ -105,7 +105,7 @@ void addSameRecipientTwice() { .statusCode(200) .body("name", is("share1")) .body("comment", is(nullValue())) - .body("recipients", is(hasSize(3))) + .body("recipients", is(hasSize(4))) .body("schemas", is(hasSize(0))) .body("createdAt", is(0)) .body("createdBy", is("Mr. Fox")) @@ -121,7 +121,7 @@ void addAnotherRecipient() { .statusCode(200) .body("name", is("share1")) .body("comment", is(nullValue())) - .body("recipients", is(hasSize(4))) + .body("recipients", is(hasSize(5))) .body("schemas", is(hasSize(0))) .body("createdAt", is(0)) .body("createdBy", is("Mr. Fox")) @@ -143,7 +143,7 @@ public void createSchema() { .statusCode(201) .body("name", is("share1")) .body("comment", is(nullValue())) - .body("recipients", is(hasSize(4))) + .body("recipients", is(hasSize(5))) .body("schemas", is(hasSize(1))) .body("schemas[0]", is("schema1")) .body("createdAt", is(0)) @@ -185,7 +185,7 @@ public void addTableToSchema() { .statusCode(201) .body("name", is("share1")) .body("comment", is(nullValue())) - .body("recipients", is(hasSize(4))) + .body("recipients", is(hasSize(5))) .body("schemas", is(hasSize(1))) .body("schemas[0]", is("schema1")) .body("createdAt", is(0)) @@ -200,7 +200,7 @@ ValidatableResponse createEmptyShare(String name) { .when() .filter(whitefoxFilter) .body( - new CreateShareInput().name(name).recipients(List.of()).schemas(List.of()), + new CreateShareInput().name(name).recipients(List.of("Mr. Fox")).schemas(List.of()), new Jackson2Mapper((cls, charset) -> objectMapper)) .header(new Header("Content-Type", "application/json")) .post("/whitefox-api/v1/shares") diff --git a/server/app/src/test/resources/application.properties b/server/app/src/test/resources/application.properties index 6f8d01116..1a659c66d 100644 --- a/server/app/src/test/resources/application.properties +++ b/server/app/src/test/resources/application.properties @@ -1 +1,2 @@ -quarkus.http.test-port=8080 \ No newline at end of file +quarkus.http.test-port=8080 +quarkus.test.arg-line=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005 \ No newline at end of file diff --git a/server/core/build.gradle.kts b/server/core/build.gradle.kts index 16cfff076..68977a638 100644 --- a/server/core/build.gradle.kts +++ b/server/core/build.gradle.kts @@ -16,9 +16,9 @@ dependencies { implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")) // QUARKUS compileOnly("jakarta.enterprise:jakarta.enterprise.cdi-api") - compileOnly("jakarta.ws.rs:jakarta.ws.rs-api") + implementation("jakarta.ws.rs:jakarta.ws.rs-api") compileOnly("org.eclipse.microprofile.config:microprofile-config-api") - + implementation("org.glassfish.jersey.core:jersey-common:3.1.2") testFixturesImplementation("jakarta.inject:jakarta.inject-api") testFixturesImplementation("org.eclipse.microprofile.config:microprofile-config-api") diff --git a/server/core/src/main/java/io/whitefox/core/WhitefoxAuthorization.java b/server/core/src/main/java/io/whitefox/core/WhitefoxAuthorization.java new file mode 100644 index 000000000..35712d389 --- /dev/null +++ b/server/core/src/main/java/io/whitefox/core/WhitefoxAuthorization.java @@ -0,0 +1,19 @@ +package io.whitefox.core; + +import jakarta.enterprise.context.ApplicationScoped; +import lombok.Data; + +public interface WhitefoxAuthorization { + + Boolean authorize(Share share, Principal principal); + + @Data + @ApplicationScoped + class WhitefoxSimpleAuthorization implements WhitefoxAuthorization { + + @Override + public Boolean authorize(Share share, Principal principal) { + return share.recipients().contains(principal) || share.owner().equals(principal); + } + } +} diff --git a/server/core/src/main/java/io/whitefox/core/services/DeltaSharesService.java b/server/core/src/main/java/io/whitefox/core/services/DeltaSharesService.java index 3d6bcc297..5005d2dbb 100644 --- a/server/core/src/main/java/io/whitefox/core/services/DeltaSharesService.java +++ b/server/core/src/main/java/io/whitefox/core/services/DeltaSharesService.java @@ -13,34 +13,49 @@ public interface DeltaSharesService { Optional getTableVersion( - String share, String schema, String table, Optional startingTimestamp); + String share, + String schema, + String table, + Optional startingTimestamp, + Principal principal); ContentAndToken> listShares( - Optional nextPageToken, Optional maxResults); + Optional nextPageToken, + Optional maxResults, + Principal currentPrincipal); Optional getTableMetadata( String share, String schema, String table, Optional startingTimestamp, - ClientCapabilities clientCapabilities); + ClientCapabilities clientCapabilities, + Principal currentPrincipal); Optional>> listSchemas( - String share, Optional nextPageToken, Optional maxResults); + String share, + Optional nextPageToken, + Optional maxResults, + Principal currentPrincipal); Optional>> listTables( String share, String schema, Optional nextPageToken, - Optional maxResults); + Optional maxResults, + Principal currentPrincipal); Optional>> listTablesOfShare( - String share, Optional token, Optional maxResults); + String share, + Optional token, + Optional maxResults, + Principal currentPrincipal); ReadTableResult queryTable( String share, String schema, String table, ReadTableRequest queryRequest, - ClientCapabilities clientCapabilities); + ClientCapabilities clientCapabilities, + Principal currentPrincipal); } diff --git a/server/core/src/main/java/io/whitefox/core/services/DeltaSharesServiceImpl.java b/server/core/src/main/java/io/whitefox/core/services/DeltaSharesServiceImpl.java index da18c1d80..c10539ea8 100644 --- a/server/core/src/main/java/io/whitefox/core/services/DeltaSharesServiceImpl.java +++ b/server/core/src/main/java/io/whitefox/core/services/DeltaSharesServiceImpl.java @@ -4,10 +4,12 @@ import io.whitefox.core.services.capabilities.ClientCapabilities; import io.whitefox.core.services.capabilities.ResponseFormat; import io.whitefox.core.services.exceptions.IncompatibleTableWithClient; +import io.whitefox.core.services.exceptions.ShareNotFound; import io.whitefox.core.services.exceptions.TableNotFound; import io.whitefox.persistence.StorageManager; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import jakarta.ws.rs.ForbiddenException; import java.sql.Timestamp; import java.util.List; import java.util.Optional; @@ -23,6 +25,7 @@ public class DeltaSharesServiceImpl implements DeltaSharesService { private final Integer defaultMaxResults; private final TableLoaderFactory tableLoaderFactory; private final FileSignerFactory fileSignerFactory; + private final WhitefoxAuthorization whitefoxAuthorization; @Inject public DeltaSharesServiceImpl( @@ -30,35 +33,50 @@ public DeltaSharesServiceImpl( @ConfigProperty(name = "io.delta.sharing.api.server.defaultMaxResults") Integer defaultMaxResults, TableLoaderFactory tableLoaderFactory, - FileSignerFactory signerFactory) { + FileSignerFactory signerFactory, + WhitefoxAuthorization whitefoxAuthorization) { this.storageManager = storageManager; this.defaultMaxResults = defaultMaxResults; this.tableLoaderFactory = tableLoaderFactory; this.fileSignerFactory = signerFactory; + this.whitefoxAuthorization = whitefoxAuthorization; } @Override public Optional getTableVersion( - String share, String schema, String table, Optional startingTimestamp) { + String share, + String schema, + String table, + Optional startingTimestamp, + Principal currentPrincipal) { return storageManager - .getSharedTable(share, schema, table) - .map(t -> tableLoaderFactory - .newTableLoader(t.internalTable()) - .loadTable(t) - .getTableVersion(startingTimestamp)) + .getShare(share) + .map(s -> { + var authorization = whitefoxAuthorization.authorize(s, currentPrincipal); + if (!authorization) throw new ForbiddenException(); + return storageManager.getSharedTable(share, schema, table).flatMap(t -> tableLoaderFactory + .newTableLoader(t.internalTable()) + .loadTable(t) + .getTableVersion(startingTimestamp)); + }) .orElse(Optional.empty()); } @Override public ContentAndToken> listShares( - Optional nextPageToken, Optional maxResults) { + Optional nextPageToken, + Optional maxResults, + Principal currentPrincipal) { Integer finalMaxResults = maxResults.orElse(defaultMaxResults); Integer start = nextPageToken.map(ContentAndToken.Token::value).orElse(0); var pageContent = storageManager.getShares(start, finalMaxResults); int end = start + finalMaxResults; Optional optionalToken = end < pageContent.size() ? Optional.of(new ContentAndToken.Token(end)) : Optional.empty(); - var content = pageContent.result(); + var content = pageContent.result().stream() + .filter( + s -> s.recipients().contains(currentPrincipal) || s.owner().equals(currentPrincipal)) + .collect(Collectors.toList()); return optionalToken .map(t -> ContentAndToken.of(content, t)) .orElse(ContentAndToken.withoutToken(content)); @@ -70,31 +88,50 @@ public Optional getTableMetadata( String schema, String tableName, Optional startingTimestamp, - ClientCapabilities clientCapabilities) { - var table = storageManager - .getSharedTable(share, schema, tableName) - .map(t -> tableLoaderFactory.newTableLoader(t.internalTable()).loadTable(t)); - return table - .flatMap(t -> t.getMetadata(startingTimestamp)) - .map(m -> checkResponseFormat(clientCapabilities, Metadata::format, m, tableName)); + ClientCapabilities clientCapabilities, + Principal currentPrincipal) { + return storageManager + .getShare(share) + .map(s -> { + var authorization = whitefoxAuthorization.authorize(s, currentPrincipal); + if (!authorization) throw new ForbiddenException(); + var table = storageManager + .getSharedTable(share, schema, tableName) + .map(t -> tableLoaderFactory.newTableLoader(t.internalTable()).loadTable(t)); + return table + .flatMap(t -> t.getMetadata(startingTimestamp)) + .map(m -> checkResponseFormat(clientCapabilities, Metadata::format, m, tableName)); + }) + .orElse(Optional.empty()); } @Override public Optional>> listSchemas( - String share, Optional nextPageToken, Optional maxResults) { - Integer finalMaxResults = maxResults.orElse(defaultMaxResults); - Integer start = nextPageToken.map(ContentAndToken.Token::value).orElse(0); - var optPageContent = storageManager.listSchemas(share, start, finalMaxResults); - int end = start + finalMaxResults; + String share, + Optional nextPageToken, + Optional maxResults, + Principal currentPrincipal) { + return storageManager + .getShare(share) + .map(s -> { + var authorization = whitefoxAuthorization.authorize(s, currentPrincipal); + if (!authorization) throw new ForbiddenException(); + Integer finalMaxResults = maxResults.orElse(defaultMaxResults); + Integer start = nextPageToken.map(ContentAndToken.Token::value).orElse(0); + var optPageContent = storageManager.listSchemas(share, start, finalMaxResults); + int end = start + finalMaxResults; - return optPageContent.map(pageContent -> { - Optional optionalToken = - end < pageContent.size() ? Optional.of(new ContentAndToken.Token(end)) : Optional.empty(); - var content = pageContent.result(); - return optionalToken - .map(t -> ContentAndToken.of(content, t)) - .orElse(ContentAndToken.withoutToken(content)); - }); + return optPageContent.map(pageContent -> { + Optional optionalToken = end < pageContent.size() + ? Optional.of(new ContentAndToken.Token(end)) + : Optional.empty(); + var content = pageContent.result(); + return optionalToken + .map(t -> ContentAndToken.of(content, t)) + .orElse(ContentAndToken.withoutToken(content)); + }); + }) + .orElse(Optional.empty()); } @Override @@ -102,35 +139,57 @@ public Optional>> listTables( String share, String schema, Optional nextPageToken, - Optional maxResults) { - Integer finalMaxResults = maxResults.orElse(defaultMaxResults); - Integer start = nextPageToken.map(ContentAndToken.Token::value).orElse(0); - var optPageContent = storageManager.listTables(share, schema, start, finalMaxResults); - int end = start + finalMaxResults; - return optPageContent.map(pageContent -> { - Optional optionalToken = - end < pageContent.size() ? Optional.of(new ContentAndToken.Token(end)) : Optional.empty(); - var content = pageContent.result(); - return optionalToken - .map(t -> ContentAndToken.of(content, t)) - .orElse(ContentAndToken.withoutToken(content)); - }); + Optional maxResults, + Principal currentPrincipal) { + return storageManager + .getShare(share) + .map(s -> { + var authorization = whitefoxAuthorization.authorize(s, currentPrincipal); + if (!authorization) throw new ForbiddenException(); + Integer finalMaxResults = maxResults.orElse(defaultMaxResults); + Integer start = nextPageToken.map(ContentAndToken.Token::value).orElse(0); + var optPageContent = storageManager.listTables(share, schema, start, finalMaxResults); + int end = start + finalMaxResults; + return optPageContent.map(pageContent -> { + Optional optionalToken = end < pageContent.size() + ? Optional.of(new ContentAndToken.Token(end)) + : Optional.empty(); + var content = pageContent.result(); + return optionalToken + .map(t -> ContentAndToken.of(content, t)) + .orElse(ContentAndToken.withoutToken(content)); + }); + }) + .orElse(Optional.empty()); } @Override public Optional>> listTablesOfShare( - String share, Optional nextPageToken, Optional maxResults) { - Integer finalMaxResults = maxResults.orElse(defaultMaxResults); - Integer start = nextPageToken.map(ContentAndToken.Token::value).orElse(0); - var optPageContent = storageManager.listTablesOfShare(share, start, finalMaxResults); - int end = start + finalMaxResults; - return optPageContent.map(pageContent -> { - Optional optionalToken = - end < pageContent.size() ? Optional.of(new ContentAndToken.Token(end)) : Optional.empty(); - return optionalToken - .map(t -> ContentAndToken.of(pageContent.result(), t)) - .orElse(ContentAndToken.withoutToken(pageContent.result())); - }); + String share, + Optional nextPageToken, + Optional maxResults, + Principal currentPrincipal) { + return storageManager + .getShare(share) + .map(s -> { + var authorization = whitefoxAuthorization.authorize(s, currentPrincipal); + if (!authorization) + throw new ForbiddenException( + "Principal: " + currentPrincipal + " cannot access share: " + share); + Integer finalMaxResults = maxResults.orElse(defaultMaxResults); + Integer start = nextPageToken.map(ContentAndToken.Token::value).orElse(0); + var optPageContent = storageManager.listTablesOfShare(share, start, finalMaxResults); + int end = start + finalMaxResults; + return optPageContent.map(pageContent -> { + Optional optionalToken = end < pageContent.size() + ? Optional.of(new ContentAndToken.Token(end)) + : Optional.empty(); + return optionalToken + .map(t -> ContentAndToken.of(pageContent.result(), t)) + .orElse(ContentAndToken.withoutToken(pageContent.result())); + }); + }) + .orElse(Optional.empty()); } @Override @@ -140,31 +199,44 @@ public ReadTableResult queryTable( String schema, String tableName, ReadTableRequest queryRequest, - ClientCapabilities clientCapabilities) { - SharedTable sharedTable = storageManager - .getSharedTable(share, schema, tableName) - .orElseThrow(() -> new TableNotFound(String.format( - "Table %s not found in share %s with schema %s", tableName, share, schema))); + ClientCapabilities clientCapabilities, + Principal currentPrincipal) { + return storageManager + .getShare(share) + .map(s -> { + var authorization = whitefoxAuthorization.authorize(s, currentPrincipal); + if (!authorization) + throw new ForbiddenException( + "Principal: " + currentPrincipal + " cannot access share: " + share); - try (FileSigner fileSigner = - fileSignerFactory.newFileSigner(sharedTable.internalTable().provider().storage())) { - var readTableResultToBeSigned = tableLoaderFactory - .newTableLoader(sharedTable.internalTable()) - .loadTable(sharedTable) - .queryTable(queryRequest); - return checkResponseFormat( - clientCapabilities, - ReadTableResult::responseFormat, - new ReadTableResult( - readTableResultToBeSigned.protocol(), - readTableResultToBeSigned.metadata(), - readTableResultToBeSigned.other().stream() - .map(fileSigner::sign) - .collect(Collectors.toList()), - readTableResultToBeSigned.version(), - ResponseFormat.parquet), - tableName); - } + SharedTable sharedTable = storageManager + .getSharedTable(share, schema, tableName) + .orElseThrow(() -> new TableNotFound(String.format( + "Table %s not found in share %s with schema %s", tableName, share, schema))); + + var readTableResultToBeSigned = tableLoaderFactory + .newTableLoader(sharedTable.internalTable()) + .loadTable(sharedTable) + .queryTable(queryRequest); + try (FileSigner fileSigner = fileSignerFactory.newFileSigner( + sharedTable.internalTable().provider().storage())) { + return checkResponseFormat( + clientCapabilities, + ReadTableResult::responseFormat, + new ReadTableResult( + readTableResultToBeSigned.protocol(), + readTableResultToBeSigned.metadata(), + readTableResultToBeSigned.other().stream() + .map(fileSigner::sign) + .collect(Collectors.toList()), + readTableResultToBeSigned.version(), + ResponseFormat.parquet), + tableName); + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .orElseThrow(() -> new ShareNotFound(String.format("Share %s not found", share))); } private A checkResponseFormat( diff --git a/server/core/src/main/java/io/whitefox/core/services/ShareService.java b/server/core/src/main/java/io/whitefox/core/services/ShareService.java index a535dfbd4..438100a8d 100644 --- a/server/core/src/main/java/io/whitefox/core/services/ShareService.java +++ b/server/core/src/main/java/io/whitefox/core/services/ShareService.java @@ -6,10 +6,7 @@ import io.whitefox.persistence.StorageManager; import jakarta.enterprise.context.ApplicationScoped; import java.time.Clock; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @@ -37,10 +34,10 @@ public Share createShare(CreateShare createShare, Principal currentUser) { .collect(Collectors.toMap(Schema::name, schema -> schema)); Share share = new Share( createShare.name(), - createShare.name(), // TODO + createShare.name(), // TODO: UUID creation newSchemas, createShare.comment(), - Set.of(), + new HashSet<>(createShare.recipients()), clock.millis(), currentUser, clock.millis(), diff --git a/server/core/src/test/java/io/whitefox/core/services/DeltaShareServiceTest.java b/server/core/src/test/java/io/whitefox/core/services/DeltaShareServiceTest.java index e20697748..7d3347c2b 100644 --- a/server/core/src/test/java/io/whitefox/core/services/DeltaShareServiceTest.java +++ b/server/core/src/test/java/io/whitefox/core/services/DeltaShareServiceTest.java @@ -1,15 +1,19 @@ package io.whitefox.core.services; +import static org.junit.jupiter.api.Assertions.assertThrows; + import io.whitefox.DeltaTestUtils; import io.whitefox.core.*; import io.whitefox.core.Principal; import io.whitefox.core.Schema; import io.whitefox.core.Share; import io.whitefox.core.SharedTable; +import io.whitefox.core.WhitefoxAuthorization.WhitefoxSimpleAuthorization; import io.whitefox.core.services.capabilities.ClientCapabilities; import io.whitefox.core.services.exceptions.TableNotFound; import io.whitefox.persistence.StorageManager; import io.whitefox.persistence.memory.InMemoryStorageManager; +import jakarta.ws.rs.ForbiddenException; import java.util.Collections; import java.util.List; import java.util.Map; @@ -25,7 +29,10 @@ public class DeltaShareServiceTest { Integer defaultMaxResults = 10; FileSignerFactory fileSignerFactory = new FileSignerFactoryImpl(new S3ClientFactoryImpl()); + WhitefoxAuthorization whitefoxAuthorization = new WhitefoxSimpleAuthorization(); + private static final Principal testPrincipal = new Principal("Mr. Fox"); + private static final Principal badPrincipal = new Principal("Mr. White"); private static Share createShare(String name, String key, Map schemas) { return new Share(name, key, schemas, testPrincipal, 0L); @@ -36,19 +43,44 @@ public void listShares() { var shares = List.of(createShare("name", "key", Collections.emptyMap())); StorageManager storageManager = new InMemoryStorageManager(shares); DeltaSharesService deltaSharesService = new DeltaSharesServiceImpl( - storageManager, defaultMaxResults, tableLoaderFactory, fileSignerFactory); - var sharesWithNextToken = deltaSharesService.listShares(Optional.empty(), Optional.of(30)); + storageManager, + defaultMaxResults, + tableLoaderFactory, + fileSignerFactory, + whitefoxAuthorization); + var sharesWithNextToken = + deltaSharesService.listShares(Optional.empty(), Optional.of(30), testPrincipal); Assertions.assertEquals(1, sharesWithNextToken.getContent().size()); Assertions.assertTrue(sharesWithNextToken.getToken().isEmpty()); } + @Test + public void listSharesUnauthorized() { + var shares = List.of(new Share("name", "key", Collections.emptyMap(), badPrincipal, 0L)); + StorageManager storageManager = new InMemoryStorageManager(shares); + DeltaSharesService deltaSharesService = new DeltaSharesServiceImpl( + storageManager, + defaultMaxResults, + tableLoaderFactory, + fileSignerFactory, + whitefoxAuthorization); + var sharesWithNextToken = + deltaSharesService.listShares(Optional.empty(), Optional.of(30), testPrincipal); + Assertions.assertEquals(0, sharesWithNextToken.getContent().size()); + } + @Test public void listSharesWithToken() { var shares = List.of(createShare("name", "key", Collections.emptyMap())); StorageManager storageManager = new InMemoryStorageManager(shares); DeltaSharesService deltaSharesService = new DeltaSharesServiceImpl( - storageManager, defaultMaxResults, tableLoaderFactory, fileSignerFactory); - var sharesWithNextToken = deltaSharesService.listShares(Optional.empty(), Optional.of(30)); + storageManager, + defaultMaxResults, + tableLoaderFactory, + fileSignerFactory, + whitefoxAuthorization); + var sharesWithNextToken = + deltaSharesService.listShares(Optional.empty(), Optional.of(30), testPrincipal); Assertions.assertEquals(1, sharesWithNextToken.getContent().size()); Assertions.assertTrue(sharesWithNextToken.getToken().isEmpty()); } @@ -57,22 +89,36 @@ public void listSharesWithToken() { public void listSchemasOfEmptyShare() { var shares = List.of(createShare("name", "key", Collections.emptyMap())); StorageManager storageManager = new InMemoryStorageManager(shares); - DeltaSharesService deltaSharesService = - new DeltaSharesServiceImpl(storageManager, 100, tableLoaderFactory, fileSignerFactory); - var resultSchemas = deltaSharesService.listSchemas("name", Optional.empty(), Optional.empty()); + DeltaSharesService deltaSharesService = new DeltaSharesServiceImpl( + storageManager, 100, tableLoaderFactory, fileSignerFactory, whitefoxAuthorization); + var resultSchemas = + deltaSharesService.listSchemas("name", Optional.empty(), Optional.empty(), testPrincipal); Assertions.assertTrue(resultSchemas.isPresent()); Assertions.assertTrue(resultSchemas.get().getContent().isEmpty()); Assertions.assertTrue(resultSchemas.get().getToken().isEmpty()); } + @Test + public void listSchemasNoAuth() { + var shares = List.of(new Share("name", "key", Collections.emptyMap(), badPrincipal, 0L)); + StorageManager storageManager = new InMemoryStorageManager(shares); + DeltaSharesService deltaSharesService = new DeltaSharesServiceImpl( + storageManager, 100, tableLoaderFactory, fileSignerFactory, whitefoxAuthorization); + assertThrows( + ForbiddenException.class, + () -> deltaSharesService.listSchemas( + "name", Optional.empty(), Optional.empty(), testPrincipal)); + } + @Test public void listSchemas() { var shares = List.of(createShare( "name", "key", Map.of("default", new Schema("default", Collections.emptyList(), "name")))); StorageManager storageManager = new InMemoryStorageManager(shares); - DeltaSharesService deltaSharesService = - new DeltaSharesServiceImpl(storageManager, 100, tableLoaderFactory, fileSignerFactory); - var resultSchemas = deltaSharesService.listSchemas("name", Optional.empty(), Optional.empty()); + DeltaSharesService deltaSharesService = new DeltaSharesServiceImpl( + storageManager, 100, tableLoaderFactory, fileSignerFactory, whitefoxAuthorization); + var resultSchemas = + deltaSharesService.listSchemas("name", Optional.empty(), Optional.empty(), testPrincipal); Assertions.assertTrue(resultSchemas.isPresent()); Assertions.assertEquals(1, resultSchemas.get().getContent().size()); Assertions.assertEquals( @@ -86,10 +132,10 @@ public void listSchemasOfUnknownShare() { var shares = List.of(createShare( "name", "key", Map.of("default", new Schema("default", Collections.emptyList(), "name")))); StorageManager storageManager = new InMemoryStorageManager(shares); - DeltaSharesService deltaSharesService = - new DeltaSharesServiceImpl(storageManager, 100, tableLoaderFactory, fileSignerFactory); + DeltaSharesService deltaSharesService = new DeltaSharesServiceImpl( + storageManager, 100, tableLoaderFactory, fileSignerFactory, whitefoxAuthorization); var resultSchemas = - deltaSharesService.listSchemas("notKey", Optional.empty(), Optional.empty()); + deltaSharesService.listSchemas("notKey", Optional.empty(), Optional.empty(), testPrincipal); Assertions.assertTrue(resultSchemas.isEmpty()); } @@ -106,10 +152,10 @@ public void listTables() { "table1", "default", "name", DeltaTestUtils.deltaTable("location1"))), "name")))); StorageManager storageManager = new InMemoryStorageManager(shares); - DeltaSharesService deltaSharesService = - new DeltaSharesServiceImpl(storageManager, 100, tableLoaderFactory, fileSignerFactory); - var resultSchemas = - deltaSharesService.listTables("name", "default", Optional.empty(), Optional.empty()); + DeltaSharesService deltaSharesService = new DeltaSharesServiceImpl( + storageManager, 100, tableLoaderFactory, fileSignerFactory, whitefoxAuthorization); + var resultSchemas = deltaSharesService.listTables( + "name", "default", Optional.empty(), Optional.empty(), testPrincipal); Assertions.assertTrue(resultSchemas.isPresent()); Assertions.assertTrue(resultSchemas.get().getToken().isEmpty()); Assertions.assertEquals(1, resultSchemas.get().getContent().size()); @@ -137,10 +183,10 @@ public void listAllTables() { "table2", "default", "name", DeltaTestUtils.deltaTable("location2"))), "name")))); StorageManager storageManager = new InMemoryStorageManager(shares); - DeltaSharesService deltaSharesService = - new DeltaSharesServiceImpl(storageManager, 100, tableLoaderFactory, fileSignerFactory); - var resultSchemas = - deltaSharesService.listTablesOfShare("name", Optional.empty(), Optional.empty()); + DeltaSharesService deltaSharesService = new DeltaSharesServiceImpl( + storageManager, 100, tableLoaderFactory, fileSignerFactory, whitefoxAuthorization); + var resultSchemas = deltaSharesService.listTablesOfShare( + "name", Optional.empty(), Optional.empty(), testPrincipal); Assertions.assertTrue(resultSchemas.isPresent()); Assertions.assertTrue(resultSchemas.get().getToken().isEmpty()); Matchers.containsInAnyOrder(List.of( @@ -170,10 +216,10 @@ public void listAllTablesEmpty() { "name"))), createShare("name2", "key2", Map.of())); StorageManager storageManager = new InMemoryStorageManager(shares); - DeltaSharesService deltaSharesService = - new DeltaSharesServiceImpl(storageManager, 100, tableLoaderFactory, fileSignerFactory); - var resultSchemas = - deltaSharesService.listTablesOfShare("name2", Optional.empty(), Optional.empty()); + DeltaSharesService deltaSharesService = new DeltaSharesServiceImpl( + storageManager, 100, tableLoaderFactory, fileSignerFactory, whitefoxAuthorization); + var resultSchemas = deltaSharesService.listTablesOfShare( + "name2", Optional.empty(), Optional.empty(), testPrincipal); Assertions.assertTrue(resultSchemas.isPresent()); Assertions.assertTrue(resultSchemas.get().getToken().isEmpty()); Assertions.assertTrue(resultSchemas.get().getContent().isEmpty()); @@ -182,10 +228,10 @@ public void listAllTablesEmpty() { @Test public void listAllTablesNoShare() { StorageManager storageManager = new InMemoryStorageManager(); - DeltaSharesService deltaSharesService = - new DeltaSharesServiceImpl(storageManager, 100, tableLoaderFactory, fileSignerFactory); - var resultSchemas = - deltaSharesService.listTablesOfShare("name2", Optional.empty(), Optional.empty()); + DeltaSharesService deltaSharesService = new DeltaSharesServiceImpl( + storageManager, 100, tableLoaderFactory, fileSignerFactory, whitefoxAuthorization); + var resultSchemas = deltaSharesService.listTablesOfShare( + "name2", Optional.empty(), Optional.empty(), testPrincipal); Assertions.assertTrue(resultSchemas.isEmpty()); } @@ -203,10 +249,10 @@ public void getDeltaTableMetadata() { "table1", "default", "name", DeltaTestUtils.deltaTable("delta-table"))), "name")))); StorageManager storageManager = new InMemoryStorageManager(shares); - DeltaSharesService deltaSharesService = - new DeltaSharesServiceImpl(storageManager, 100, tableLoaderFactory, fileSignerFactory); + DeltaSharesService deltaSharesService = new DeltaSharesServiceImpl( + storageManager, 100, tableLoaderFactory, fileSignerFactory, whitefoxAuthorization); var tableMetadata = deltaSharesService.getTableMetadata( - "name", "default", "table1", Optional.empty(), ClientCapabilities.parquet()); + "name", "default", "table1", Optional.empty(), ClientCapabilities.parquet(), testPrincipal); Assertions.assertTrue(tableMetadata.isPresent()); Assertions.assertEquals( "56d48189-cdbc-44f2-9b0e-2bded4c79ed7", tableMetadata.get().id()); @@ -225,10 +271,16 @@ public void tableMetadataNotFound() { "table1", "default", "name", DeltaTestUtils.deltaTable("location1"))), "name")))); StorageManager storageManager = new InMemoryStorageManager(shares); - DeltaSharesService deltaSharesService = - new DeltaSharesServiceImpl(storageManager, 100, tableLoaderFactory, fileSignerFactory); + DeltaSharesService deltaSharesService = new DeltaSharesServiceImpl( + storageManager, 100, tableLoaderFactory, fileSignerFactory, whitefoxAuthorization); var resultTable = deltaSharesService.getTableMetadata( - "name", "default", "tableNotFound", Optional.empty(), ClientCapabilities.parquet()); + "name", + "default", + "tableNotFound", + Optional.empty(), + ClientCapabilities.parquet(), + testPrincipal); + Assertions.assertTrue(resultTable.isEmpty()); } @@ -249,15 +301,16 @@ public void queryExistingTable() { DeltaTestUtils.deltaTable("partitioned-delta-table"))), "name")))); StorageManager storageManager = new InMemoryStorageManager(shares); - DeltaSharesService deltaSharesService = - new DeltaSharesServiceImpl(storageManager, 100, tableLoaderFactory, fileSignerFactory); + DeltaSharesService deltaSharesService = new DeltaSharesServiceImpl( + storageManager, 100, tableLoaderFactory, fileSignerFactory, whitefoxAuthorization); var resultTable = deltaSharesService.queryTable( "name", "default", "partitioned-delta-table", new ReadTableRequest.ReadTableCurrentVersion( Optional.empty(), Optional.empty(), Optional.empty()), - ClientCapabilities.parquet()); + ClientCapabilities.parquet(), + testPrincipal); Assertions.assertEquals(9, resultTable.files().size()); } @@ -274,11 +327,11 @@ public void queryNonExistingTable() { "table1", "default", "name", DeltaTestUtils.deltaTable("location1"))), "name")))); StorageManager storageManager = new InMemoryStorageManager(shares); - DeltaSharesService deltaSharesService = - new DeltaSharesServiceImpl(storageManager, 100, tableLoaderFactory, fileSignerFactory); - Assertions.assertThrows( + DeltaSharesService deltaSharesService = new DeltaSharesServiceImpl( + storageManager, 100, tableLoaderFactory, fileSignerFactory, whitefoxAuthorization); + assertThrows( TableNotFound.class, () -> deltaSharesService.queryTable( - "name", "default", "tableNotFound", null, ClientCapabilities.parquet())); + "name", "default", "tableNotFound", null, ClientCapabilities.parquet(), testPrincipal)); } } diff --git a/server/core/src/test/java/io/whitefox/core/services/ShareServiceTest.java b/server/core/src/test/java/io/whitefox/core/services/ShareServiceTest.java index ccbaadac1..c1f9f6567 100644 --- a/server/core/src/test/java/io/whitefox/core/services/ShareServiceTest.java +++ b/server/core/src/test/java/io/whitefox/core/services/ShareServiceTest.java @@ -23,6 +23,9 @@ public class ShareServiceTest { Principal testPrincipal = new Principal("Mr. Fox"); Clock testClock = Clock.fixed(Instant.ofEpochMilli(7), ZoneOffset.UTC); + WhitefoxAuthorization whitefoxAuthorization = + new WhitefoxAuthorization.WhitefoxSimpleAuthorization(); + @Test void createShare() { var storage = new InMemoryStorageManager(); @@ -35,7 +38,7 @@ void createShare() { "share1", Collections.emptyMap(), Optional.empty(), - Set.of(), + Set.of(testPrincipal), 7, testPrincipal, 7, @@ -67,7 +70,11 @@ void addRecipientsToShare() { "share1", Collections.emptyMap(), Optional.empty(), - Set.of(new Principal("Antonio"), new Principal("Marco"), new Principal("Aleksandar")), + Set.of( + new Principal("Mr. Fox"), + new Principal("Antonio"), + new Principal("Marco"), + new Principal("Aleksandar")), 7, testPrincipal, 7, @@ -88,7 +95,11 @@ void addSameRecipientTwice() { "share1", Collections.emptyMap(), Optional.empty(), - Set.of(new Principal("Antonio"), new Principal("Marco"), new Principal("Aleksandar")), + Set.of( + new Principal("Mr. Fox"), + new Principal("Antonio"), + new Principal("Marco"), + new Principal("Aleksandar")), 7, testPrincipal, 7, @@ -140,7 +151,7 @@ public void createSchema() { "share1", Map.of("schema1", new Schema("schema1", Collections.emptyList(), "share1")), Optional.empty(), - Set.of(), + Set.of(testPrincipal), 7, testPrincipal, 7, @@ -176,7 +187,7 @@ public void createSecondSchema() { "schema2", new Schema("schema2", Collections.emptyList(), "share1")), Optional.empty(), - Set.of(), + Set.of(testPrincipal), 7, testPrincipal, 7, @@ -229,8 +240,9 @@ public void addTableToSchema() { storage, 100, new TableLoaderFactoryImpl(), - new FileSignerFactoryImpl(new S3ClientFactoryImpl())) - .listTablesOfShare("share1", Optional.empty(), Optional.empty()) + new FileSignerFactoryImpl(new S3ClientFactoryImpl()), + whitefoxAuthorization) + .listTablesOfShare("share1", Optional.empty(), Optional.empty(), testPrincipal) .get() .getContent(); assertEquals(1, tablesFromDeltaService.size()); @@ -286,7 +298,7 @@ public void addSameTableTwice() { "schema2", new Schema("schema2", Collections.emptyList(), "share1")), Optional.empty(), - Set.of(), + Set.of(testPrincipal), 7, testPrincipal, 7, @@ -354,7 +366,7 @@ private Share createShare(String name, String key, Map schemas) private CreateShare emptyCreateShare() { return new CreateShare( - "share1", Optional.empty(), Collections.emptyList(), Collections.emptyList()); + "share1", Optional.empty(), List.of(new Principal("Mr. Fox")), Collections.emptyList()); } private ProviderService newProviderService(StorageManager storage, Clock testClock) {