Skip to content

Commit 3c41711

Browse files
add PUT /vaults/{vaultId}/members
1 parent c291eae commit 3c41711

File tree

4 files changed

+138
-4
lines changed

4 files changed

+138
-4
lines changed

backend/src/main/java/org/cryptomator/hub/api/VaultResource.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@
6565
import java.util.List;
6666
import java.util.Map;
6767
import java.util.Optional;
68+
import java.util.Set;
6869
import java.util.UUID;
70+
import java.util.function.Predicate;
6971
import java.util.stream.Collectors;
7072
import java.util.stream.Stream;
7173

@@ -77,20 +79,29 @@ public class VaultResource {
7779

7880
@Inject
7981
AccessToken.Repository accessTokenRepo;
82+
8083
@Inject
8184
Group.Repository groupRepo;
85+
8286
@Inject
8387
User.Repository userRepo;
88+
89+
@Inject
90+
Authority.Repository authorityRepo;
91+
8492
@Inject
8593
EffectiveVaultAccess.Repository effectiveVaultAccessRepo;
94+
8695
/**
8796
* @deprecated to be removed in <a href="https://github.com/cryptomator/hub/issues/333">#333</a>
8897
*/
8998
@Inject
9099
@Deprecated(since = "1.3.0", forRemoval = true)
91100
LegacyAccessToken.Repository legacyAccessTokenRepo;
101+
92102
@Inject
93103
Vault.Repository vaultRepo;
104+
94105
@Inject
95106
VaultAccess.Repository vaultAccessRepo;
96107

@@ -173,6 +184,62 @@ public List<MemberDto> getDirectMembers(@PathParam("vaultId") UUID vaultId) {
173184
}).toList();
174185
}
175186

187+
@PUT
188+
@Path("/{vaultId}/members")
189+
@RolesAllowed("user")
190+
@VaultRole(value = VaultAccess.Role.OWNER, bypassForEmergencyAccess = true) // may throw 403
191+
@Transactional
192+
@Consumes(MediaType.APPLICATION_JSON)
193+
@Operation(summary = "set vault members", description = "replaces all direct vault members with the given ones")
194+
@APIResponse(responseCode = "204", description = "members updated")
195+
@APIResponse(responseCode = "400", description = "invalid members in request body")
196+
@APIResponse(responseCode = "403", description = "not a vault owner")
197+
@APIResponse(responseCode = "404", description = "vault not found")
198+
public Response setDirectMembers(@PathParam("vaultId") UUID vaultId, @NotEmpty Map<String, VaultAccess.Role> memberRoles) {
199+
var vault = vaultRepo.findById(vaultId);
200+
if (vault == null) {
201+
throw new NotFoundException("Vault not found.");
202+
}
203+
var newVaultAccess = authorityRepo.findAllInList(memberRoles.keySet()).map(authority -> {
204+
assert memberRoles.containsKey(authority.getId());
205+
return VaultAccess.create(vault, authority, memberRoles.get(authority.getId()));
206+
}).toList();
207+
if (newVaultAccess.isEmpty()){
208+
throw new BadRequestException("No (valid) members given.");
209+
}
210+
var oldVaultAccess = vaultAccessRepo.forVault(vaultId).toList();
211+
212+
// determine diff:
213+
Set<VaultAccess.Id> newIds = newVaultAccess.stream().map(VaultAccess::getId).collect(Collectors.toSet());
214+
Set<VaultAccess.Id> oldIds = oldVaultAccess.stream().map(VaultAccess::getId).collect(Collectors.toSet());
215+
Predicate<VaultAccess> isNew = va -> newIds.contains(va.getId());
216+
Predicate<VaultAccess> isOld = va -> oldIds.contains(va.getId());
217+
Predicate<VaultAccess> hasChangedRole = va -> memberRoles.get(va.getId().getAuthorityId()) != va.getRole();
218+
var addedMembers = newVaultAccess.stream()
219+
.filter(isOld.negate())
220+
.peek(va -> eventLogger.logVaultMemberAdded(jwt.getSubject(), vaultId, va.getId().getAuthorityId(), va.getRole()));
221+
var removedMemberIds = oldVaultAccess.stream()
222+
.filter(isNew.negate())
223+
.peek(va -> eventLogger.logVaultMemberRemoved(jwt.getSubject(), vaultId, va.getId().getAuthorityId()))
224+
.map(VaultAccess::getId)
225+
.map(VaultAccess.Id::getAuthorityId);
226+
var updatedMembers = oldVaultAccess.stream()
227+
.filter(isNew)
228+
.filter(hasChangedRole)
229+
.peek(va -> va.setRole(memberRoles.get(va.getId().getAuthorityId())))
230+
.peek(va -> eventLogger.logVaultMemberUpdated(jwt.getSubject(), vaultId, va.getId().getAuthorityId(), va.getRole()))
231+
.toList();
232+
233+
// replace all:
234+
vaultAccessRepo.delete(vaultId, removedMemberIds.toList());
235+
vaultAccessRepo.persist(addedMembers);
236+
vaultAccessRepo.persist(updatedMembers);
237+
238+
return Response.noContent().build();
239+
}
240+
241+
242+
176243
@PUT
177244
@Path("/{vaultId}/users/{userId}")
178245
@RolesAllowed("user")

backend/src/main/java/org/cryptomator/hub/entities/Authority.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import jakarta.persistence.NamedQuery;
1313
import jakarta.persistence.Table;
1414

15+
import java.util.Collection;
1516
import java.util.List;
1617
import java.util.Objects;
1718
import java.util.stream.Stream;
@@ -86,7 +87,7 @@ public Stream<Authority> byName(String name) {
8687
return find("#Authority.byName", Parameters.with("name", '%' + name.toLowerCase() + '%')).stream();
8788
}
8889

89-
public Stream<Authority> findAllInList(List<String> ids) {
90+
public Stream<Authority> findAllInList(Collection<String> ids) {
9091
return find("#Authority.allInList", Parameters.with("ids", ids)).stream();
9192
}
9293
}

backend/src/main/java/org/cryptomator/hub/entities/VaultAccess.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@
3030
INNER JOIN FETCH va.authority
3131
WHERE va.id.vaultId = :vaultId
3232
""")
33+
@NamedQuery(name = "VaultAccess.deleteForVault",
34+
query = """
35+
DELETE FROM VaultAccess va
36+
WHERE va.id.vaultId = :vaultId
37+
AND va.id.authorityId IN :authorityIds
38+
""")
3339
public class VaultAccess {
3440

3541
@EmbeddedId
@@ -93,6 +99,15 @@ public void setRole(Role role) {
9399
this.role = role;
94100
}
95101

102+
public static VaultAccess create(Vault vault, Authority authority, Role role) {
103+
VaultAccess entity = new VaultAccess();
104+
entity.setId(new Id(vault.getId(), authority.getId()));
105+
entity.setVault(vault);
106+
entity.setAuthority(authority);
107+
entity.setRole(role);
108+
return entity;
109+
}
110+
96111
@Embeddable
97112
public static class Id implements Serializable {
98113

@@ -155,5 +170,9 @@ public static class Repository implements PanacheRepositoryBase<VaultAccess, Id>
155170
public Stream<VaultAccess> forVault(UUID vaultId) {
156171
return find("#VaultAccess.forVault", Parameters.with("vaultId", vaultId)).stream();
157172
}
173+
174+
public long delete(UUID vaultId, Iterable<String> authorityIds) {
175+
return delete("#VaultAccess.deleteForVault", Parameters.with("vaultId", vaultId).and("authorityIds", authorityIds));
176+
}
158177
}
159178
}

backend/src/test/java/org/cryptomator/hub/api/VaultResourceIT.java

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.auth0.jwt.algorithms.Algorithm;
55
import io.agroal.api.AgroalDataSource;
66
import io.quarkus.narayana.jta.QuarkusTransaction;
7+
import io.quarkus.test.InjectMock;
78
import io.quarkus.test.junit.QuarkusTest;
89
import io.quarkus.test.security.TestSecurity;
910
import io.quarkus.test.security.oidc.Claim;
@@ -14,6 +15,8 @@
1415
import jakarta.validation.Validator;
1516
import org.cryptomator.hub.entities.EffectiveVaultAccess;
1617
import org.cryptomator.hub.entities.Vault;
18+
import org.cryptomator.hub.entities.VaultAccess;
19+
import org.cryptomator.hub.entities.events.EventLogger;
1720
import org.cryptomator.hub.rollback.DBRollbackAfter;
1821
import org.cryptomator.hub.rollback.DBRollbackBefore;
1922
import org.flywaydb.core.Flyway;
@@ -32,6 +35,7 @@
3235
import org.junit.jupiter.api.TestMethodOrder;
3336
import org.junit.jupiter.params.ParameterizedTest;
3437
import org.junit.jupiter.params.provider.CsvSource;
38+
import org.mockito.Mockito;
3539

3640
import java.security.GeneralSecurityException;
3741
import java.security.KeyFactory;
@@ -56,13 +60,15 @@
5660
import static org.hamcrest.Matchers.comparesEqualTo;
5761
import static org.hamcrest.Matchers.equalTo;
5862
import static org.hamcrest.Matchers.hasSize;
59-
import static org.hamcrest.Matchers.nullValue;
6063
import static org.hamcrest.text.IsEqualIgnoringCase.equalToIgnoringCase;
6164

6265
@QuarkusTest
6366
@DisplayName("Resource /vaults")
6467
public class VaultResourceIT {
6568

69+
@InjectMock
70+
EventLogger eventLogger;
71+
6672
@Inject
6773
AgroalDataSource dataSource;
6874
@Inject
@@ -344,7 +350,7 @@ public void testUpdateVault() {
344350
}
345351

346352
@Nested
347-
@DisplayName("As vault admin user1")
353+
@DisplayName("As vault owner user1")
348354
@TestSecurity(user = "User Name 1", roles = {"user"})
349355
@OidcSecurity(claims = {
350356
@Claim(key = "sub", value = "user1")
@@ -565,12 +571,52 @@ public void testRevokeAccess() { // previously added in testGrantAccess()
565571

566572
@Test
567573
@Order(14)
574+
@DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-000100002222/members adds, removes and updates members")
575+
public void setMembersOfVault2() {
576+
given().when().contentType(ContentType.JSON).body("""
577+
{
578+
"user1": "MEMBER",
579+
"user2": "OWNER",
580+
"group2": "MEMBER"
581+
}
582+
""").put("/vaults/{vaultId}/members", "7E57C0DE-0000-4000-8000-000100002222")
583+
.then().statusCode(204);
584+
var vaultId = UUID.fromString("7E57C0DE-0000-4000-8000-000100002222");
585+
Mockito.verify(eventLogger).logVaultMemberAdded("user2", vaultId, "user1", VaultAccess.Role.MEMBER);
586+
Mockito.verify(eventLogger).logVaultMemberAdded("user2", vaultId, "user2", VaultAccess.Role.OWNER);
587+
Mockito.verify(eventLogger).logVaultMemberRemoved("user2", vaultId, "group1");
588+
Mockito.verify(eventLogger).logVaultMemberUpdated("user2", vaultId, "group2", VaultAccess.Role.MEMBER);
589+
}
590+
591+
@Test
592+
@Order(15)
593+
@DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-000100002222/members restores original members")
594+
public void restoreOriginalMembersOfVault2() { // as defined in V9999__Tst_Data.sql
595+
given().when().contentType(ContentType.JSON).body("""
596+
{
597+
"group1": "MEMBER",
598+
"group2": "OWNER"
599+
}
600+
""").put("/vaults/{vaultId}/members", "7E57C0DE-0000-4000-8000-000100002222")
601+
.then().statusCode(204);
602+
var vaultId = UUID.fromString("7E57C0DE-0000-4000-8000-000100002222");
603+
Mockito.verify(eventLogger).logVaultMemberRemoved("user2", vaultId, "user1");
604+
Mockito.verify(eventLogger).logVaultMemberRemoved("user2", vaultId, "user2");
605+
Mockito.verify(eventLogger).logVaultMemberAdded("user2", vaultId, "group1", VaultAccess.Role.MEMBER);
606+
Mockito.verify(eventLogger).logVaultMemberUpdated("user2", vaultId, "group2", VaultAccess.Role.OWNER);
607+
608+
}
609+
610+
@Test
611+
@Order(16)
568612
@DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100002222/members does not contain user2")
569613
@DBRollbackAfter
570614
public void getMembersOfVault2c() {
571615
given().when().get("/vaults/{vaultId}/members", "7E57C0DE-0000-4000-8000-000100002222")
572616
.then().statusCode(200)
573-
.body("id", not(hasItems("user2")));
617+
.body("id", not(hasItems("user2")))
618+
.body("id", hasItems("group1", "group2"))
619+
;
574620
}
575621
}
576622

@@ -1078,6 +1124,7 @@ public class AsAnonymous {
10781124
"GET, /vaults/accessible",
10791125
"GET, /vaults/7E57C0DE-0000-4000-8000-000100001111",
10801126
"GET, /vaults/7E57C0DE-0000-4000-8000-000100001111/members",
1127+
"PUT, /vaults/7E57C0DE-0000-4000-8000-000100001111/members",
10811128
"PUT, /vaults/7E57C0DE-0000-4000-8000-000100001111/users/user1",
10821129
"DELETE, /vaults/7E57C0DE-0000-4000-8000-000100001111/authority/user1",
10831130
"GET, /vaults/7E57C0DE-0000-4000-8000-000100001111/users-requiring-access-grant",

0 commit comments

Comments
 (0)