Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Last changes for Cryptomator Hub CLI #243

Merged
merged 9 commits into from
Nov 14, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public List<AuthorityDto> search(@QueryParam("query") @NotBlank String query) {

@GET
@Path("/")
@RolesAllowed("admin")
@RolesAllowed("user")
@Produces(MediaType.APPLICATION_JSON)
@NoCache
@Operation(summary = "lists all authorities matching the given ids", description = "lists for each id in the list its corresponding authority. Ignores all id's where an authority cannot be found")
Expand Down
37 changes: 37 additions & 0 deletions backend/src/main/java/org/cryptomator/hub/api/GroupsResource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.cryptomator.hub.api;

import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.cryptomator.hub.entities.EffectiveGroupMembership;
import org.cryptomator.hub.entities.Group;
import org.cryptomator.hub.validation.ValidId;
import org.eclipse.microprofile.openapi.annotations.Operation;

import java.util.List;

@Path("/groups")
public class GroupsResource {

@GET
@Path("/")
@RolesAllowed("user")
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "list all groups")
public List<GroupDto> getAll() {
return Group.findAll().<Group>stream().map(GroupDto::fromEntity).toList();
}

@GET
@Path("/{groupId}/effective-members")
@RolesAllowed("user")
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "list all effective group members")
public List<UserDto> getEffectiveMembers(@PathParam("groupId") @ValidId String groupId) {
return EffectiveGroupMembership.getEffectiveGroupUsers(groupId).map(UserDto::justPublicInfo).toList();
}

}
25 changes: 4 additions & 21 deletions backend/src/main/java/org/cryptomator/hub/api/VaultResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -218,32 +218,15 @@ private Response addAuthority(Vault vault, Authority authority, VaultAccess.Role
}

@DELETE
@Path("/{vaultId}/users/{userId}")
@RolesAllowed("user")
@VaultRole(VaultAccess.Role.OWNER) // may throw 403
@Transactional
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "remove a member from this vault", description = "revokes the given user's access rights from this vault. If the given user is no member, the request is a no-op.")
@APIResponse(responseCode = "204", description = "member removed")
@APIResponse(responseCode = "403", description = "not a vault owner")
public Response removeUser(@PathParam("vaultId") UUID vaultId, @PathParam("userId") @ValidId String userId) {
return removeAuthority(vaultId, userId);
}

@DELETE
@Path("/{vaultId}/groups/{groupId}")
@Path("/{vaultId}/authority/{authorityId}")
@RolesAllowed("user")
@VaultRole(VaultAccess.Role.OWNER) // may throw 403
@Transactional
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "remove a group from this vault", description = "revokes the given group's access rights from this vault. If the given group is no member, the request is a no-op.")
@APIResponse(responseCode = "204", description = "member removed")
@Operation(summary = "remove a user or group from this vault", description = "revokes the given authority's access rights from this vault. If the given authority is no member, the request is a no-op.")
@APIResponse(responseCode = "204", description = "authority removed")
@APIResponse(responseCode = "403", description = "not a vault owner")
public Response removeGroup(@PathParam("vaultId") UUID vaultId, @PathParam("groupId") @ValidId String groupId) {
return removeAuthority(vaultId, groupId);
}

private Response removeAuthority(UUID vaultId, String authorityId) {
public Response removeAuthority(@PathParam("vaultId") UUID vaultId, @PathParam("authorityId") @ValidId String authorityId) {
if (VaultAccess.deleteById(new VaultAccess.Id(vaultId, authorityId))) {
AuditEventVaultMemberRemove.log(jwt.getSubject(), vaultId, authorityId);
return Response.status(Response.Status.NO_CONTENT).build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import java.io.Serializable;
import java.util.Objects;
import java.util.stream.Stream;

@Entity
@Immutable
Expand All @@ -22,6 +23,12 @@ SELECT count( DISTINCT u)
INNER JOIN EffectiveGroupMembership egm ON u.id = egm.id.memberId
WHERE egm.id.groupId = :groupId
""")
@NamedQuery(name = "EffectiveGroupMembership.getEGUs", query = """
SELECT DISTINCT u
FROM User u
INNER JOIN EffectiveGroupMembership egm ON u.id = egm.id.memberId
WHERE egm.id.groupId = :groupId
""")
public class EffectiveGroupMembership extends PanacheEntityBase {

@EmbeddedId
Expand All @@ -33,6 +40,10 @@ public static long countEffectiveGroupUsers(String groupdId) {
return EffectiveGroupMembership.count("#EffectiveGroupMembership.countEGUs", Parameters.with("groupId", groupdId));
}

public static Stream<User> getEffectiveGroupUsers(String groupdId) {
return EffectiveGroupMembership.find("#EffectiveGroupMembership.getEGUs", Parameters.with("groupId", groupdId)).stream();
}

@Embeddable
public static class EffectiveGroupMembershipId implements Serializable {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,17 +118,5 @@ public void testGetSome() {
.then().statusCode(200)
.body("id", containsInAnyOrder("user1", "group2"));
}

@Test
@DisplayName("GET /authorities?ids=user1&ids=group2 as user returns 403")
@TestSecurity(user = "User Name 1", roles = {"user"})
@OidcSecurity(claims = {
@Claim(key = "sub", value = "user1")
})
public void testGetSomeAsUser() {
given().param("ids", "user1", "group2")
.when().get("/authorities")
.then().statusCode(403);
}
}
}
103 changes: 103 additions & 0 deletions backend/src/test/java/org/cryptomator/hub/api/GroupsResourceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package org.cryptomator.hub.api;

import io.agroal.api.AgroalDataSource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.quarkus.test.security.oidc.Claim;
import io.quarkus.test.security.oidc.OidcSecurity;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import jakarta.inject.Inject;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import java.sql.SQLException;

import static io.restassured.RestAssured.given;
import static io.restassured.RestAssured.when;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.empty;

@QuarkusTest
@DisplayName("Resource /groups")
public class GroupsResourceTest {

@Inject
AgroalDataSource dataSource;

@BeforeAll
public static void beforeAll() {
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
}

@Nested
@DisplayName("As user1")
@TestSecurity(user = "User Name 1", roles = {"user"})
@OidcSecurity(claims = {
@Claim(key = "sub", value = "user1")
})
public class AsAuthorzedUser1 {

@Test
@DisplayName("GET /groups returns 200")
public void testGetAll() {
when().get("/groups")
.then().statusCode(200)
.body("id", hasItems("group1", "group2"));
}

@Test
@DisplayName("GET /groups/group1/effective-members contains direct and subgroup members")
public void testGetEffectiveUsers() throws SQLException {
try (var c = dataSource.getConnection(); var s = c.createStatement()) {
s.execute("""
INSERT INTO "authority" ("id", "type", "name")
VALUES
('user999', 'USER', 'User 999'),
('group999', 'GROUP', 'Group 999');

INSERT INTO "user_details" ("id") VALUES ('user999');
INSERT INTO "group_details" ("id") VALUES ('group999');

INSERT INTO "group_membership" ("group_id", "member_id")
VALUES
('group999', 'user999'),
('group1', 'group999');
""");
}

when().get("/groups/{groupId}/effective-members", "group1")
.then().statusCode(200)
.body("id", hasItems("user1", "user999"));

try (var c = dataSource.getConnection(); var s = c.createStatement()) {
s.execute("""
DELETE FROM "authority" WHERE "id" = 'user999' OR "id" = 'group999';
""");
}
}

}

@Nested
@DisplayName("As unauthenticated user")
public class AsAnonymous {

@DisplayName("401 Unauthorized")
@ParameterizedTest(name = "{0} {1}")
@CsvSource(value = {
"GET, /users"
})
public void testGet(String method, String path) {
when().request(method, path)
.then().statusCode(401);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ public void testGetUsersRequiringAccess3() {
@Order(13)
@DisplayName("DELETE /vaults/7E57C0DE-0000-4000-8000-000100002222/members/user2 returns 204")
public void testRevokeAccess() { // previously added in testGrantAccess()
given().when().delete("/vaults/{vaultId}/users/{userId}", "7E57C0DE-0000-4000-8000-000100002222", "user2")
given().when().delete("/vaults/{vaultId}/authority/{userId}", "7E57C0DE-0000-4000-8000-000100002222", "user2")
.then().statusCode(204);
}

Expand Down Expand Up @@ -636,7 +636,7 @@ public void testGetUsersRequiringAccess4() {
@Order(7)
@DisplayName("DELETE /vaults/7E57C0DE-0000-4000-8000-000100001111/groups/group2 returns 204")
public void removeGroup2() {
given().when().delete("/vaults/{vaultId}/groups/{groupId}", "7E57C0DE-0000-4000-8000-000100001111", "group2")
given().when().delete("/vaults/{vaultId}/authority/{groupId}", "7E57C0DE-0000-4000-8000-000100001111", "group2")
.then().statusCode(204);
}

Expand Down Expand Up @@ -1034,7 +1034,7 @@ public class AsAnonymous {
"GET, /vaults/7E57C0DE-0000-4000-8000-000100001111",
"GET, /vaults/7E57C0DE-0000-4000-8000-000100001111/members",
"PUT, /vaults/7E57C0DE-0000-4000-8000-000100001111/users/user1",
"DELETE, /vaults/7E57C0DE-0000-4000-8000-000100001111/users/user1",
"DELETE, /vaults/7E57C0DE-0000-4000-8000-000100001111/authority/user1",
"GET, /vaults/7E57C0DE-0000-4000-8000-000100001111/users-requiring-access-grant",
"GET, /vaults/7E57C0DE-0000-4000-8000-000100001111/access-token"
})
Expand Down Expand Up @@ -1124,4 +1124,4 @@ public void testListSomeVaultsAsUser() {
.then().statusCode(403);
}
}
}
}
4 changes: 2 additions & 2 deletions frontend/src/common/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,8 @@ class VaultService {
.catch((error) => rethrowAndConvertIfExpected(error, 404, 409));
}

public async removeUser(vaultId: string, userId: string) {
await axiosAuth.delete(`/vaults/${vaultId}/users/${userId}`)
public async removeAuthority(vaultId: string, authorityId: string) {
await axiosAuth.delete(`/vaults/${vaultId}/authority/${authorityId}`)
.catch((error) => rethrowAndConvertIfExpected(error, 404));
}
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/VaultDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ async function updateMemberRole(member: MemberDto, role: VaultRole) {
async function removeMember(memberId: string) {
delete onUpdateVaultMembershipError.value[memberId];
try {
await backend.vaults.removeUser(props.vaultId, memberId);
await backend.vaults.removeAuthority(props.vaultId, memberId);
members.value.delete(memberId);
usersRequiringAccessGrant.value = await backend.vaults.getUsersRequiringAccessGrant(props.vaultId);
} catch (error) {
Expand Down
Loading