Skip to content
Merged
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 @@ -97,7 +97,7 @@ public List<RecipientEntity> list() {
.getResultList());
}

/** Deletes a recipient and its tokens. Only the owner may delete it. */
/** Deletes a recipient, its tokens and its permissions. Only the owner may delete it. */
public void delete(String name, UserContext user) {
tx.inTransaction(
false,
Expand All @@ -106,6 +106,10 @@ public void delete(String name, UserContext user) {
em.createQuery("delete from RecipientTokenEntity t where t.recipient.id = :recipientId")
.setParameter("recipientId", recipient.getId())
.executeUpdate();
em.createQuery(
"delete from SharePermissionEntity p where p.recipient.id = :recipientId")
.setParameter("recipientId", recipient.getId())
.executeUpdate();
em.remove(recipient);
return null;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import io.opensharing.recipient.RecipientStore;
import io.opensharing.recipient.RecipientTokenEntity;
import io.opensharing.share.ShareEntity;
import io.opensharing.share.SharePermissionEntity;
import io.opensharing.share.SharePermissionStore;
import io.opensharing.share.ShareService;
import io.opensharing.share.ShareStore;
import java.util.List;
Expand All @@ -26,9 +28,16 @@ public final class OpenSharing {

private OpenSharing(CatalogConnector catalog, Transactions tx) {
this.catalog = catalog;
RecipientStore recipientStore = new RecipientStore(tx);
this.shares =
new ShareService(catalog, tx, new ShareStore(tx), new SharedDataObjectStore(tx));
this.recipients = new RecipientService(catalog, new RecipientStore(tx));
new ShareService(
catalog,
tx,
new ShareStore(tx),
new SharedDataObjectStore(tx),
new SharePermissionStore(tx),
recipientStore);
this.recipients = new RecipientService(catalog, recipientStore);
}

public static Builder builder() {
Expand All @@ -41,7 +50,8 @@ public static List<Class<?>> entities() {
ShareEntity.class,
SharedDataObjectEntity.class,
RecipientEntity.class,
RecipientTokenEntity.class);
RecipientTokenEntity.class,
SharePermissionEntity.class);
}

public CatalogConnector catalog() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package io.opensharing.share;

import io.opensharing.BaseEntity;
import io.opensharing.recipient.RecipientEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;

/**
* One privilege a recipient holds on a share. A recipient holds each privilege on a share at most
* once. {@code ShareStore} and {@code RecipientStore} delete the rows with their share or
* recipient.
*/
@Entity
@Table(
name = "os_share_permissions",
uniqueConstraints =
@UniqueConstraint(
name = "uk_share_permission",
columnNames = {"share_id", "recipient_id", "privilege"}))
public class SharePermissionEntity extends BaseEntity {

/** Loaded eagerly because responses read the share's name after the store returns. */
@ManyToOne(fetch = FetchType.EAGER, optional = false)
@JoinColumn(name = "share_id", nullable = false)
private ShareEntity share;

/** Loaded eagerly because responses read the recipient's name after the store returns. */
@ManyToOne(fetch = FetchType.EAGER, optional = false)
@JoinColumn(name = "recipient_id", nullable = false)
private RecipientEntity recipient;

@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 32)
private SharePrivilege privilege;

public ShareEntity getShare() {
return share;
}

public void setShare(ShareEntity share) {
this.share = share;
}

public RecipientEntity getRecipient() {
return recipient;
}

public void setRecipient(RecipientEntity recipient) {
this.recipient = recipient;
}

public SharePrivilege getPrivilege() {
return privilege;
}

public void setPrivilege(SharePrivilege privilege) {
this.privilege = privilege;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.opensharing.share;

import com.fasterxml.jackson.annotation.JsonInclude;
import java.time.Instant;

/**
* A privilege a recipient holds on a share. {@code grantedAt} is when it was first granted.
*
* <pre>{@code
* {
* "shareId": "<share id>",
* "shareName": "<share name>",
* "recipientId": "<recipient id>",
* "recipientName": "<recipient name>",
* "privilege": "SELECT",
* "grantedAt": "<ISO-8601 instant>"
* }
* }</pre>
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public record SharePermissionResponse(
String shareId,
String shareName,
String recipientId,
String recipientName,
SharePrivilege privilege,
Instant grantedAt) {

public static SharePermissionResponse from(SharePermissionEntity permission) {
return new SharePermissionResponse(
permission.getShare().getId(),
permission.getShare().getName(),
permission.getRecipient().getId(),
permission.getRecipient().getName(),
permission.getPrivilege(),
permission.getCreatedAt());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package io.opensharing.share;

import io.opensharing.Transactions;
import io.opensharing.http.ApiException;
import io.opensharing.recipient.RecipientEntity;
import jakarta.persistence.EntityManager;
import java.util.List;
import java.util.Optional;

/**
* Storage for privileges recipients hold on shares. Callers check share ownership before granting
* or revoking. Grants and revokes run in the caller's transaction, so a batch of them commits or
* rolls back together; {@link #list} runs in its own. Each loaded permission also loads its share
* and recipient.
*/
public class SharePermissionStore {

private final Transactions tx;

public SharePermissionStore(Transactions tx) {
this.tx = tx;
}

/** Granting a privilege the recipient already holds leaves the original row untouched. */
public void grant(
EntityManager em, ShareEntity share, RecipientEntity recipient, SharePrivilege privilege) {
if (find(em, share, recipient, privilege).isEmpty()) {
SharePermissionEntity permission = new SharePermissionEntity();
permission.setShare(em.getReference(ShareEntity.class, share.getId()));
permission.setRecipient(em.getReference(RecipientEntity.class, recipient.getId()));
permission.setPrivilege(privilege);
em.persist(permission);
}
}

/** Revokes a privilege. Fails with not-found when the recipient does not hold it. */
public void revoke(
EntityManager em, ShareEntity share, RecipientEntity recipient, SharePrivilege privilege) {
SharePermissionEntity permission =
find(em, share, recipient, privilege)
.orElseThrow(
() ->
ApiException.notFound(
"recipient '"
+ recipient.getName()
+ "' does not hold "
+ privilege
+ " on share '"
+ share.getName()
+ "'"));
em.remove(permission);
}

/** The share's permissions, ordered by recipient name. */
public List<SharePermissionEntity> list(ShareEntity share) {
return tx.inTransaction(
true,
em ->
em.createQuery(
"select p from SharePermissionEntity p join fetch p.share"
+ " join fetch p.recipient r where p.share.id = :shareId"
+ " order by r.name",
SharePermissionEntity.class)
.setParameter("shareId", share.getId())
.getResultList());
}

private static Optional<SharePermissionEntity> find(
EntityManager em, ShareEntity share, RecipientEntity recipient, SharePrivilege privilege) {
return em.createQuery(
"select p from SharePermissionEntity p where p.share.id = :shareId"
+ " and p.recipient.id = :recipientId and p.privilege = :privilege",
SharePermissionEntity.class)
.setParameter("shareId", share.getId())
.setParameter("recipientId", recipient.getId())
.setParameter("privilege", privilege)
.getResultStream()
.findFirst();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.opensharing.share;

/** What a recipient may do with a share. Reading is all there is so far. */
public enum SharePrivilege {
SELECT
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,42 @@
import io.opensharing.exception.CatalogException;
import io.opensharing.http.ApiException;
import io.opensharing.http.ListResponse;
import io.opensharing.recipient.RecipientEntity;
import io.opensharing.recipient.RecipientStore;
import jakarta.persistence.EntityManager;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;

/**
* Provider share CRUD for a caller the host has authenticated. Creating a share needs CREATE_SHARE
* from the catalog; any caller may read shares, only the owner may change them.
* Provider share CRUD and recipient grants for a caller the host has authenticated. Creating a
* share needs CREATE_SHARE from the catalog; any caller may read shares, only the owner may change
* them.
*/
public class ShareService {

private final CatalogConnector catalog;
private final Transactions tx;
private final ShareStore shares;
private final SharedDataObjectStore objects;
private final SharePermissionStore permissions;
private final RecipientStore recipients;

public ShareService(
CatalogConnector catalog,
Transactions tx,
ShareStore shares,
SharedDataObjectStore objects) {
SharedDataObjectStore objects,
SharePermissionStore permissions,
RecipientStore recipients) {
this.catalog = catalog;
this.tx = tx;
this.shares = shares;
this.objects = objects;
this.permissions = permissions;
this.recipients = recipients;
}

/** Creates a share owned by the caller, who needs CREATE_SHARE from the catalog. */
Expand Down Expand Up @@ -124,6 +135,52 @@ public void delete(UserContext user, String share) {
shares.delete(share, user);
}

/** Lists who holds which privilege on the share, by recipient name, unpaged. */
public ListResponse<SharePermissionResponse> listPermissions(String share) {
// TODO: page with maxResults and pageToken.
return ListResponse.of(
permissions.list(shares.getOrThrow(share)).stream()
.map(SharePermissionResponse::from)
.toList());
}

/**
* Applies the changes in order, each revoking then granting, all in one transaction: either every
* change commits or none does. Returns the share's permissions. Owner only.
*/
public ListResponse<SharePermissionResponse> updatePermissions(
UserContext user, String share, UpdateSharePermissionsRequest request) {
shares.getOwnedOrThrow(share, user);
List<RecipientChange> changes = new ArrayList<>();
for (UpdateSharePermissionsRequest.Change change : request.changes()) {
if (change == null) {
throw ApiException.invalidParameter("changes must not contain null");
}
if (Stream.concat(change.add().stream(), change.remove().stream())
.anyMatch(Objects::isNull)) {
throw ApiException.invalidParameter("changes.add and changes.remove must not contain null");
}
RecipientEntity recipient =
recipients.getOrThrow(requireText(change.recipientName(), "changes.recipientName"));
changes.add(new RecipientChange(recipient, change));
}
tx.inTransaction(
false,
em -> {
ShareEntity entity = shares.getOwnedOrThrow(em, share, user);
for (RecipientChange change : changes) {
for (SharePrivilege privilege : change.change().remove()) {
permissions.revoke(em, entity, change.recipient(), privilege);
}
for (SharePrivilege privilege : change.change().add()) {
permissions.grant(em, entity, change.recipient(), privilege);
}
}
return null;
});
return listPermissions(share);
}

// Resolves the object in the catalog on behalf of the caller and checks the declared type.
private Addition addition(UserContext user, UpdateShareRequest.DataObject dataObject) {
String name = requireText(dataObject.name(), "dataObject.name");
Expand Down Expand Up @@ -207,6 +264,9 @@ private record Alias(String schema, String table) {}

private record Addition(String name, AssetType type, ResolvedAsset resolved, Alias alias) {}

private record RecipientChange(
RecipientEntity recipient, UpdateSharePermissionsRequest.Change change) {}

// Exactly one of alias and name is set.
private record Removal(AssetType type, Alias alias, String name) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public List<ShareEntity> list() {
.getResultList());
}

/** Deletes a share and the objects in it. Only the owner may delete it. */
/** Deletes a share, the objects in it and its permissions. Only the owner may delete it. */
public void delete(String name, UserContext user) {
tx.inTransaction(
false,
Expand All @@ -101,6 +101,9 @@ public void delete(String name, UserContext user) {
em.createQuery("delete from SharedDataObjectEntity o where o.share.id = :shareId")
.setParameter("shareId", share.getId())
.executeUpdate();
em.createQuery("delete from SharePermissionEntity p where p.share.id = :shareId")
.setParameter("shareId", share.getId())
.executeUpdate();
em.remove(share);
return null;
});
Expand Down
Loading
Loading