Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,21 @@ public List<SharedDataObjectEntity> list(ShareEntity share) {
.getResultList());
}

/** One page of the schemas recipients see in {@code share}, ordered by name. */
public List<String> listSchemas(ShareEntity share, int offset, int limit) {
return tx.inTransaction(
true,
em ->
em.createQuery(
"select distinct o.sharedAsSchema from SharedDataObjectEntity o"
+ " where o.share.id = :shareId order by o.sharedAsSchema",
String.class)
.setParameter("shareId", share.getId())
.setFirstResult(offset)
.setMaxResults(limit)
.getResultList());
}

/** Removes the object shared under the alias. Fails unless it exists with the given type. */
public void removeByAlias(
EntityManager em,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.opensharing.asset.schema;

import io.opensharing.asset.SharedDataObjectStore;
import io.opensharing.asset.share.ProtocolShares;
import io.opensharing.http.ListResponse;
import io.opensharing.http.Pagination;
import io.opensharing.recipient.RecipientPrincipal;
import io.opensharing.share.ShareEntity;

/**
* The schemas a recipient sees through the sharing protocol: the distinct schema aliases of the
* objects in a share it holds SELECT on.
*/
public class ProtocolSchemas {

private final ProtocolShares shares;
private final SharedDataObjectStore objects;

public ProtocolSchemas(ProtocolShares shares, SharedDataObjectStore objects) {
this.shares = shares;
this.objects = objects;
}

/** Lists one page of the share's schemas by name. */
public ListResponse<SchemaResponse> list(
RecipientPrincipal recipient, String share, Integer maxResults, String pageToken) {
ShareEntity granted = shares.requireGranted(recipient, share);
return Pagination.protocolPage(
maxResults,
pageToken,
(offset, limit) -> objects.listSchemas(granted, offset, limit),
name -> new SchemaResponse(name, granted.getName()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.opensharing.asset.schema;

/**
* Protocol schema listed under a share:
*
* <pre>{@code
* {"name": "<schema name>", "share": "<share name>"}
* }</pre>
*/
public record SchemaResponse(String name, String share) {}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.opensharing.http.ListResponse;
import io.opensharing.http.Pagination;
import io.opensharing.recipient.RecipientPrincipal;
import io.opensharing.share.ShareEntity;
import io.opensharing.share.ShareResponse;
import io.opensharing.share.SharePermissionStore;
import io.opensharing.share.ShareStore;
Expand Down Expand Up @@ -34,10 +35,16 @@ public ListResponse<ShareResponse> list(

/** Gets one of the recipient's shares by name in any case. */
public GetShareResponse get(RecipientPrincipal recipient, String share) {
return new GetShareResponse(ShareResponse.from(requireGranted(recipient, share)));
}

/**
* Looks up a share by name in any case. Fails with not-found unless the recipient holds SELECT.
*/
public ShareEntity requireGranted(RecipientPrincipal recipient, String share) {
return shares
.find(share)
.filter(candidate -> permissions.hasSelect(candidate, recipient.recipientId()))
.map(granted -> new GetShareResponse(ShareResponse.from(granted)))
.orElseThrow(() -> ApiException.notFound("share '" + share + "' does not exist"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import com.linecorp.armeria.server.annotation.Get;
import com.linecorp.armeria.server.annotation.Param;
import com.linecorp.armeria.server.annotation.ProducesJson;
import io.opensharing.asset.schema.ProtocolSchemas;
import io.opensharing.asset.schema.SchemaResponse;
import io.opensharing.asset.share.GetShareResponse;
import io.opensharing.asset.share.ProtocolShares;
import io.opensharing.http.ListResponse;
Expand All @@ -20,9 +22,11 @@
public final class OpenSharingProtocolService {

private final ProtocolShares shares;
private final ProtocolSchemas schemas;

public OpenSharingProtocolService(ProtocolShares shares) {
public OpenSharingProtocolService(ProtocolShares shares, ProtocolSchemas schemas) {
this.shares = shares;
this.schemas = schemas;
}

/** {@code GET /shares}: lists one page of the shares the recipient holds SELECT on. */
Expand All @@ -39,4 +43,14 @@ public ListResponse<ShareResponse> listShares(
public GetShareResponse getShare(ServiceRequestContext ctx, @Param String share) {
return shares.get(RecipientTokenDecorator.recipient(ctx), share);
}

/** {@code GET /shares/{share}/schemas}: lists one page of the schemas in a granted share. */
@Get("/shares/{share}/schemas")
public ListResponse<SchemaResponse> listSchemas(
ServiceRequestContext ctx,
@Param String share,
@Param @Nullable Integer maxResults,
@Param @Nullable String pageToken) {
return schemas.list(RecipientTokenDecorator.recipient(ctx), share, maxResults, pageToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ public static void mount(
.exceptionHandlers(errors)
.decorator(new RecipientTokenDecorator(openSharing.recipients()))
.decorator(errors)
.build(new OpenSharingProtocolService(openSharing.protocolShares()));
.build(
new OpenSharingProtocolService(
openSharing.protocolShares(), openSharing.protocolSchemas()));
builder
.annotatedService()
.pathPrefix(activationBasePath)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.opensharing.Transactions;
import io.opensharing.asset.SharedDataObjectEntity;
import io.opensharing.asset.SharedDataObjectStore;
import io.opensharing.asset.schema.ProtocolSchemas;
import io.opensharing.asset.share.ProtocolShares;
import io.opensharing.catalog.CatalogConnector;
import io.opensharing.recipient.RecipientEntity;
Expand All @@ -27,22 +28,19 @@ public final class OpenSharing {
private final ShareService shares;
private final RecipientService recipients;
private final ProtocolShares protocolShares;
private final ProtocolSchemas protocolSchemas;

private OpenSharing(CatalogConnector catalog, Transactions tx) {
this.catalog = catalog;
ShareStore shareStore = new ShareStore(tx);
SharedDataObjectStore objectStore = new SharedDataObjectStore(tx);
SharePermissionStore permissionStore = new SharePermissionStore(tx);
RecipientStore recipientStore = new RecipientStore(tx);
this.shares =
new ShareService(
catalog,
tx,
shareStore,
new SharedDataObjectStore(tx),
permissionStore,
recipientStore);
new ShareService(catalog, tx, shareStore, objectStore, permissionStore, recipientStore);
this.recipients = new RecipientService(catalog, recipientStore);
this.protocolShares = new ProtocolShares(shareStore, permissionStore);
this.protocolSchemas = new ProtocolSchemas(protocolShares, objectStore);
}

public static Builder builder() {
Expand Down Expand Up @@ -76,6 +74,11 @@ public ProtocolShares protocolShares() {
return protocolShares;
}

/** The schemas recipients see in their shares through the sharing protocol. */
public ProtocolSchemas protocolSchemas() {
return protocolSchemas;
}

public static final class Builder {

private CatalogConnector catalog;
Expand Down
10 changes: 10 additions & 0 deletions server/opensharing-server/src/main/resources/local-catalog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,13 @@ assets:
subtype: MANAGED
storageLocation: s3://test/main.sales.orders/
format: delta

- identifier: main.hr
type: SCHEMA

- identifier: main.ops.people
type: TABLE
attributes:
subtype: MANAGED
storageLocation: s3://test/main.ops.people/
format: delta
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,26 @@ static void stopServer() {
server.close();
}

protected static void createShare(String name) {
String body = "{\"name\":\"%s\"}".formatted(name);
assertEquals(201, server.post(PROVIDER + "/shares", ALICE, body).status());
}

protected static void createShare(String name, String displayName) {
String body = "{\"name\":\"%s\",\"displayName\":\"%s\"}".formatted(name, displayName);
assertEquals(201, server.post(PROVIDER + "/shares", ALICE, body).status());
}

/** Adds the catalog asset {@code name} of {@code type} to the share as {@code sharedAs}. */
protected static void addObject(String share, String type, String name, String sharedAs) {
String body =
"{\"updates\":[{\"action\":\"ADD\",\"dataObject\":"
+ "{\"name\":\"%s\",\"type\":\"%s\",\"sharedAs\":\"%s\"}}]}";
Response added =
server.patch(PROVIDER + "/shares/" + share, ALICE, body.formatted(name, type, sharedAs));
assertEquals(200, added.status());
}

/** Creates a TOKEN recipient, redeems its activation URL, and returns the bearer token. */
protected static String createAndActivateRecipient(String name) {
Response created =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package io.opensharing.protocol;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.fasterxml.jackson.databind.JsonNode;
import com.linecorp.armeria.common.MediaType;
import io.opensharing.TestServer.Response;
import io.opensharing.http.ErrorCodes;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;

class ProtocolSchemasTest extends ProtocolApiSupport {

@Test
void listsDistinctSchemasInAGrantedShare() {
createShare("schema-share");
addObject("schema-share", "TABLE", "main.sales.table1", "alpha.t1");
addObject("schema-share", "TABLE", "main.sales.orders", "alpha.t2");
addObject("schema-share", "SCHEMA", "main.hr", "beta");
addObject("schema-share", "TABLE", "main.ops.people", "gamma.t3");
createShare("hidden-share");
addObject("hidden-share", "TABLE", "main.sales.table1", "hidden.t");
String bearer = createAndActivateRecipient("schema-partner");
grant("schema-share", "schema-partner");

// Distinct shared-as schemas, ordered by name; the share lookup is case-insensitive.
Response list = server.get(PROTOCOL + "/shares/SCHEMA-SHARE/schemas", bearer);
assertEquals(200, list.status());
MediaType contentType = list.headers().contentType();
assertTrue(contentType.is(MediaType.JSON), contentType.toString());
assertEquals(StandardCharsets.UTF_8, contentType.charset());
JsonNode items = list.body().path("items");
assertEquals(3, items.size());
assertEquals("alpha", items.path(0).path("name").asText());
assertEquals("schema-share", items.path(0).path("share").asText());
assertEquals("beta", items.path(1).path("name").asText());
assertEquals("gamma", items.path(2).path("name").asText());
assertTrue(list.body().path("nextPageToken").isMissingNode());

// Ungranted and missing shares are both reported as missing.
Response hidden = server.get(PROTOCOL + "/shares/hidden-share/schemas", bearer);
assertEquals(404, hidden.status());
assertEquals(ErrorCodes.RESOURCE_DOES_NOT_EXIST, hidden.errorCode());
Response missing = server.get(PROTOCOL + "/shares/missing/schemas", bearer);
assertEquals(404, missing.status());
assertEquals(ErrorCodes.RESOURCE_DOES_NOT_EXIST, missing.errorCode());
}

@Test
void paginatesSchemasAndAllowsAnEmptyGrantedShare() {
createShare("paged-schemas");
addObject("paged-schemas", "TABLE", "main.sales.table1", "alpha.t");
addObject("paged-schemas", "TABLE", "main.sales.orders", "beta.t");
createShare("empty-share");
String bearer = createAndActivateRecipient("schema-paged");
grant("paged-schemas", "schema-paged");
grant("empty-share", "schema-paged");

String schemas = PROTOCOL + "/shares/paged-schemas/schemas";
Response first = server.get(page(schemas, 1, null), bearer);
assertEquals(200, first.status());
assertEquals(1, first.body().path("items").size());
assertEquals("alpha", first.body().path("items").path(0).path("name").asText());
String pageToken = first.body().path("nextPageToken").asText();
assertFalse(pageToken.isEmpty());
Response second = server.get(page(schemas, 1, pageToken), bearer);
assertEquals(200, second.status());
assertEquals(1, second.body().path("items").size());
assertEquals("beta", second.body().path("items").path(0).path("name").asText());
assertTrue(second.body().path("nextPageToken").isMissingNode());

// A page size of 0 returns no schemas, but a token to resume from.
Response zero = server.get(page(schemas, 0, null), bearer);
assertEquals(0, zero.body().path("items").size());
assertFalse(zero.body().path("nextPageToken").isMissingNode());

// A granted share with no objects lists no schemas.
Response empty = server.get(PROTOCOL + "/shares/empty-share/schemas", bearer);
assertEquals(200, empty.status());
assertEquals(0, empty.body().path("items").size());
assertTrue(empty.body().path("nextPageToken").isMissingNode());
}
}
Loading