Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -384,17 +384,14 @@ public Response grantAccess(@PathParam("vaultId") UUID vaultId, @NotEmpty Map<St
@GET
@Path("/{vaultId}")
@RolesAllowed("user")
// @VaultRole(VaultAccess.Role.MEMBER) // TODO: members and admin may do this...
@VaultRole(value = {VaultAccess.Role.MEMBER, VaultAccess.Role.OWNER}, bypassForRealmRole = true, realmRole = "admin", onMissingVault = VaultRole.OnMissingVault.NOT_FOUND)
@Produces(MediaType.APPLICATION_JSON)
@Transactional
@Operation(summary = "gets a vault")
@APIResponse(responseCode = "200")
@APIResponse(responseCode = "403", description = "requesting user is neither a vault member nor has the admin role")
public VaultDto get(@PathParam("vaultId") UUID vaultId) {
Vault vault = vaultRepo.findByIdOptional(vaultId).orElseThrow(NotFoundException::new);
if (vault.getEffectiveMembers().stream().noneMatch(u -> u.getId().equals(jwt.getSubject())) && !identity.getRoles().contains("admin")) {
throw new ForbiddenException("Requesting user is not a member of the vault");
}
return VaultDto.fromEntity(vault);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import jakarta.persistence.NamedQuery;
import jakarta.persistence.Table;

import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
Expand Down Expand Up @@ -99,7 +100,10 @@ public Stream<Authority> byName(String name) {
}

public Stream<Authority> findAllInList(List<String> ids) {
return find("#Authority.allInList", Parameters.with("ids", ids)).stream();
return Batch.of(200).run(ids, Stream.empty(), (batch, result) -> {
var partial = find("#Authority.allInList", Parameters.with("ids", batch));
return Stream.concat(result, partial.stream());
});
}
}
}
50 changes: 50 additions & 0 deletions backend/src/main/java/org/cryptomator/hub/entities/Batch.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package org.cryptomator.hub.entities;

import java.util.Collection;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Consumer;

public class Batch {

private final int size;

private Batch(int size) {
if (size <= 0) {
throw new IllegalArgumentException("Batch size must be positive");
}
this.size = size;
}

public static Batch of(int size) {
return new Batch(size);
}

public <T> void run(Collection<T> collection, Consumer<List<T>> job) {
List<T> list = collection instanceof List<T> l ? l : List.copyOf(collection);
for(int i = 0; i < list.size(); i += size) {
List<T> sublist = list.subList(i, Math.min(i + size, list.size()));
job.accept(sublist);
}
}

public <T, R> R run(Collection<T> collection, R initialValue, BiFunction<List<T>, R, R> job) {
List<T> list = collection instanceof List<T> l ? l : List.copyOf(collection);
R result = initialValue;
for(int i = 0; i < list.size(); i += size) {
List<T> sublist = list.subList(i, Math.min(i + size, list.size()));
result = job.apply(sublist, result);
}
return result;
}

public <T, R> List<R> run(Collection<T> collection, BiFunction<List<T>, List<R>, List<R>> job) {
List<T> list = collection instanceof List<T> l ? l : List.copyOf(collection);
List<R> result = List.of();
for(int i = 0; i < list.size(); i += size) {
List<T> sublist = list.subList(i, Math.min(i + size, list.size()));
result = job.apply(sublist, result);
}
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,10 @@ public Device findByIdAndUser(String deviceId, String userId) throws NoResultExc
}

public Stream<Device> findAllInList(List<String> ids) {
return find("#Device.allInList", Parameters.with("ids", ids)).stream();
return Batch.of(200).run(ids, Stream.empty(), (batch, result) -> {
var partial = find("#Device.allInList", Parameters.with("ids", batch));
return Stream.concat(result, partial.stream());
});
}

public void deleteByOwner(String userId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,72 @@
package org.cryptomator.hub.entities;

import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
import io.quarkus.panache.common.Parameters;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.NamedNativeQuery;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.Table;
import org.hibernate.annotations.Immutable;

import java.io.Serializable;
import java.util.Collection;
import java.util.Objects;

@NamedNativeQuery(name = "EffectiveGroupMembership.fullUpdate", query = """
INSERT INTO "effective_group_membership" ("group_id", "intermediate_group_ids", "member_id")
WITH RECURSIVE "members" ("root","intermediate_group_ids","member_id", "depth") AS (
SELECT "group_id", ARRAY["group_id"]::varchar[], "member_id", 0
FROM "group_membership"
UNION
SELECT "parent"."root", array_append("parent"."intermediate_group_ids", "child"."group_id"), "child"."member_id", "parent"."depth" + 1
FROM "group_membership" "child"
INNER JOIN "members" "parent" ON "child"."group_id" = "parent"."member_id"
WHERE "parent"."depth" < 10
) SELECT "root", "intermediate_group_ids", "member_id" FROM "members"
ON CONFLICT DO NOTHING
""")
@NamedNativeQuery(name = "EffectiveGroupMembership.deleteGroups", query = """
DELETE
FROM "effective_group_membership"
WHERE "intermediate_group_ids" && :groupIds
""")
@NamedNativeQuery(name = "EffectiveGroupMembership.updateGroups", query = """
INSERT INTO "effective_group_membership" ("group_id", "intermediate_group_ids", "member_id")
WITH RECURSIVE "members" ("root", "intermediate_group_ids", "member_id", "depth") AS (
SELECT "group_id", ARRAY["group_id"]::varchar[], "member_id", 0
FROM "group_membership"
WHERE "group_id" IN :groupIds
UNION
SELECT "parent"."root", array_append("parent"."intermediate_group_ids", "child"."group_id"), "child"."member_id", "parent"."depth" + 1
FROM "group_membership" "child"
INNER JOIN "members" "parent" ON "child"."group_id" = "parent"."member_id"
WHERE "parent"."depth" < 10
) SELECT "root", "intermediate_group_ids", "member_id" FROM "members"
ON CONFLICT DO NOTHING
""")
@NamedQuery(name = "EffectiveGroupMembership.deleteUsers", query = """
DELETE
FROM EffectiveGroupMembership egm
WHERE egm.id.memberId IN :userIds
""")
@NamedNativeQuery(name = "EffectiveGroupMembership.updateUsers", query = """
INSERT INTO "effective_group_membership" ("group_id", "intermediate_group_ids", "member_id")
WITH RECURSIVE "members" ("group_id", "intermediate_group_ids", "member_id", "depth") AS (
SELECT "group_id", ARRAY["group_id"]::varchar[], "member_id", 0
FROM "group_membership"
WHERE "member_id" IN :userIds
UNION
SELECT "parent"."group_id", array_prepend("parent"."group_id", "child"."intermediate_group_ids"), "child"."member_id", "child"."depth" + 1
FROM "group_membership" "parent"
INNER JOIN "members" "child" ON "child"."group_id" = "parent"."member_id"
WHERE "child"."depth" < 10
) SELECT "group_id", "intermediate_group_ids", "member_id" FROM "members"
ON CONFLICT DO NOTHING
""")
@Entity
@Immutable
@Table(name = "effective_group_membership")
Expand All @@ -19,8 +75,6 @@ public class EffectiveGroupMembership {
@EmbeddedId
private Id id;

private String path;

@Embeddable
public static class Id implements Serializable {

Expand Down Expand Up @@ -53,4 +107,47 @@ public String toString() {
'}';
}
}

@ApplicationScoped
public static class Repository implements PanacheRepositoryBase<EffectiveGroupMembership, EffectiveGroupMembership.Id> {

public void fullUpdate() {
deleteAll();
getEntityManager()
.createNamedQuery("EffectiveGroupMembership.fullUpdate")
.executeUpdate();
}

public void updateGroups(Collection<String> groupIds) {
Batch.of(200).run(groupIds, (batch) -> {
getEntityManager()
.createNamedQuery("EffectiveGroupMembership.deleteGroups")
.setParameter("groupIds", batch.toArray(new String[0])) // explicit cast to array, so JPA maps to VARCHAR[] instead of VARCHAR
.executeUpdate();
getEntityManager()
.createNamedQuery("EffectiveGroupMembership.updateGroups")
.setParameter("groupIds", batch)
.executeUpdate();
});
}

public void updateUsers(Collection<String> userIds) {
Batch.of(200).run(userIds, (batch) -> {
delete("#EffectiveGroupMembership.deleteUsers", Parameters.with("userIds", batch));
getEntityManager()
.createNamedQuery("EffectiveGroupMembership.updateUsers")
.setParameter("userIds", batch)
.executeUpdate();
});
}

// visible for testing
boolean isMember(String groupId, String memberId) {
EffectiveGroupMembership.Id id = new EffectiveGroupMembership.Id();
id.groupId = groupId;
id.memberId = memberId;
return findById(id) != null;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,45 +17,49 @@
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

@Entity
@Immutable
@Table(name = "effective_vault_access")
@NamedQuery(name = "EffectiveVaultAccess.countSeatsOccupiedBySingleUser", query = """
SELECT count(u)
@NamedQuery(name = "EffectiveVaultAccess.isUserOccupyingSeat", query = """
SELECT 1
FROM User u
INNER JOIN EffectiveVaultAccess eva ON u.id = eva.id.authorityId
WHERE eva.id.authorityId = :userId
INNER JOIN Vault v ON eva.id.vaultId = v.id
WHERE u.id = :userId AND NOT v.archived
""")
@NamedQuery(name = "EffectiveVaultAccess.countSeatsOccupiedByUsers", query = """
SELECT COUNT(DISTINCT u.id)
FROM User u
INNER JOIN EffectiveVaultAccess eva ON u.id = eva.id.authorityId
INNER JOIN Vault v ON eva.id.vaultId = v.id AND NOT v.archived
WHERE u.id IN :userIds
INNER JOIN Vault v ON eva.id.vaultId = v.id
WHERE u.id IN :userIds AND NOT v.archived
""")
@NamedQuery(name = "EffectiveVaultAccess.countSeatOccupyingUsers", query = """
SELECT count(DISTINCT u)
SELECT COUNT(DISTINCT u.id)
FROM User u
INNER JOIN EffectiveVaultAccess eva ON u.id = eva.id.authorityId
INNER JOIN Vault v ON eva.id.vaultId = v.id AND NOT v.archived
INNER JOIN Vault v ON eva.id.vaultId = v.id
WHERE NOT v.archived
""")
@NamedQuery(name = "EffectiveVaultAccess.countSeatOccupyingUsersWithAccessToken", query = """
SELECT count(DISTINCT u)
SELECT COUNT(DISTINCT u.id)
FROM User u
INNER JOIN EffectiveVaultAccess eva ON u.id = eva.id.authorityId
INNER JOIN Vault v ON eva.id.vaultId = v.id AND NOT v.archived
INNER JOIN Vault v ON eva.id.vaultId = v.id
INNER JOIN AccessToken at ON eva.id.vaultId = at.id.vaultId AND eva.id.authorityId = at.id.userId
WHERE NOT v.archived
""")
@NamedQuery(name = "EffectiveVaultAccess.countSeatOccupyingUsersOfGroup", query = """
SELECT count(DISTINCT u)
SELECT COUNT(DISTINCT u.id)
FROM User u
INNER JOIN EffectiveVaultAccess eva ON u.id = eva.id.authorityId
INNER JOIN EffectiveGroupMembership egm ON u.id = egm.id.memberId
INNER JOIN Vault v ON eva.id.vaultId = v.id AND NOT v.archived
WHERE egm.id.groupId = :groupId
INNER JOIN Vault v ON eva.id.vaultId = v.id
WHERE egm.id.groupId = :groupId AND NOT v.archived
""")
@NamedQuery(name = "EffectiveVaultAccess.findByAuthorityAndVault", query = """
SELECT eva
Expand Down Expand Up @@ -149,11 +153,14 @@ public String toString() {
public static class Repository implements PanacheRepositoryBase<EffectiveVaultAccess, Id> {

public boolean isUserOccupyingSeat(String userId) {
return count("#EffectiveVaultAccess.countSeatsOccupiedBySingleUser", Parameters.with("userId", userId)) > 0;
return find("#EffectiveVaultAccess.isUserOccupyingSeat", Parameters.with("userId", userId)).page(0, 1).firstResult() != null;
}

public long countSeatsOccupiedByUsers(List<String> userIds) {
return count("#EffectiveVaultAccess.countSeatsOccupiedByUsers", Parameters.with("userIds", userIds));
public long countSeatsOccupiedByUsers(Collection<String> userIds) {
return Batch.of(200).run(Set.copyOf(userIds), 0L, (batch, result) -> {
long partialCount = count("#EffectiveVaultAccess.countSeatsOccupiedByUsers", Parameters.with("userIds", batch));
return result + partialCount;
});
}

public long countSeatOccupyingUsers() {
Expand Down
7 changes: 7 additions & 0 deletions backend/src/main/java/org/cryptomator/hub/entities/Group.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.cryptomator.hub.entities;

import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
import io.quarkus.panache.common.Parameters;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.CascadeType;
import jakarta.persistence.DiscriminatorValue;
Expand All @@ -11,6 +12,7 @@
import jakarta.persistence.Table;
import jakarta.persistence.Transient;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

Expand Down Expand Up @@ -41,5 +43,10 @@ public int getMemberSize() {

@ApplicationScoped
public static class Repository implements PanacheRepositoryBase<Group, String> {

public long deleteByIds(Collection<String> ids) {
return Batch.of(200).run(ids, 0L, (batch, result) -> result + delete("id IN :ids", Parameters.with("ids", batch)));
}

}
}
5 changes: 5 additions & 0 deletions backend/src/main/java/org/cryptomator/hub/entities/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;

import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
Expand Down Expand Up @@ -163,6 +164,10 @@ public int hashCode() {
@ApplicationScoped
public static class Repository implements PanacheRepositoryBase<User, String> {

public long deleteByIds(Collection<String> ids) {
return Batch.of(200).run(ids, 0L, (batch, result) -> result + delete("id IN :ids", Parameters.with("ids", batch)));
}

public Stream<User> findRequiringAccessGrant(UUID vaultId) {
return find("#User.requiringAccessGrant", Parameters.with("vaultId", vaultId)).stream();
}
Expand Down
21 changes: 4 additions & 17 deletions backend/src/main/java/org/cryptomator/hub/entities/Vault.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,6 @@ public class Vault {
)
private Set<Authority> directMembers = new HashSet<>();

@ManyToMany
@Immutable
@JoinTable(name = "effective_vault_access",
joinColumns = @JoinColumn(name = "vault_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "authority_id", referencedColumnName = "id")
)
private Set<Authority> effectiveMembers = new HashSet<>();

@OneToMany(mappedBy = "vault", fetch = FetchType.LAZY)
private Set<AccessToken> accessTokens = new HashSet<>();

Expand Down Expand Up @@ -142,14 +134,6 @@ public void setDirectMembers(Set<Authority> directMembers) {
this.directMembers = directMembers;
}

public Set<Authority> getEffectiveMembers() {
return effectiveMembers;
}

public void setEffectiveMembers(Set<Authority> effectiveMembers) {
this.effectiveMembers = effectiveMembers;
}

public Set<AccessToken> getAccessTokens() {
return accessTokens;
}
Expand Down Expand Up @@ -276,7 +260,10 @@ public Stream<Vault> findAccessibleByUser(String userId, VaultAccess.Role role)
}

public Stream<Vault> findAllInList(List<UUID> ids) {
return find("#Vault.allInList", Parameters.with("ids", ids)).stream();
return Batch.of(200).run(ids, Stream.of(), (batch, result) -> {
Stream<Vault> partialResult = find("#Vault.allInList", Parameters.with("ids", batch)).stream();
return Stream.concat(result, partialResult);
});
}
}
}
Loading