Skip to content

Commit 16ed8a6

Browse files
committed
Merge branch 'develop' in 'feature/fix-admin-emergency-access-unsaved-changes-indicator-'
2 parents f032898 + 593e4d9 commit 16ed8a6

142 files changed

Lines changed: 2533 additions & 1842 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
working-directory: frontend
3737
run: npm run dist
3838
- name: SonarCloud Scan Frontend
39-
uses: SonarSource/sonarqube-scan-action@a31c9398be7ace6bbfaf30c0bd5d415f843d45e9 # v7.0.0
39+
uses: SonarSource/sonarqube-scan-action@299e4b793aaa83bf2aba7c9c14bedbb485688ec4 # v7.1.0
4040
with:
4141
projectBaseDir: frontend
4242
args: >
@@ -55,7 +55,7 @@ jobs:
5555
java-version: ${{ env.JAVA_VERSION }}
5656
cache: 'maven'
5757
- name: Cache SonarCloud packages
58-
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
58+
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
5959
with:
6060
path: ~/.sonar/cache
6161
key: ${{ runner.os }}-sonar

.github/workflows/helm-chart.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
3030

3131
- name: Setup Helm
32-
uses: Azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
32+
uses: Azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0
3333

3434
- name: Lint chart
3535
run: helm lint charts/cryptomator-hub
@@ -50,10 +50,10 @@ jobs:
5050
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
5151

5252
- name: Setup Helm
53-
uses: Azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
53+
uses: Azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0
5454

5555
- name: Setup Cosign
56-
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
56+
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
5757

5858
- name: Compute publish version
5959
id: version

CHANGELOG.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- User and group management (#376)
1313
- Emergency Access: Allow a council to restore access to a orphaned vault (#390)
1414
- Show pictures of the groups in the Vaults member list (#375)
15+
- Allow admins to archive and unarchive any vault (#283, #430)
16+
- Disable users to exclude them from license seat count (#427, #428)
17+
- Display a banner to indicate that legacy devices are still in use, since these will be removed in the next major release (#420)
1518

1619
### Changed
1720

18-
- Updated Keycloak to 26.5.5
19-
- Update Quarkus to 3.27.2 LTS
21+
- Updated Keycloak to 26.5.7
22+
- Update Quarkus to 3.27.3 LTS
2023
- Improved browser locale detection (#371)
2124
- Improved efficiency of keycloak-to-hub data sync (#377)
2225
- Improved efficiency of group-based access permission checks (#372)
@@ -29,6 +32,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2932

3033
### Fixed
3134
- Hide Archive/Reactivate Vault actions for admins without ownership rights (#379)
35+
- Check seat limit before unarchiving a vault, ensuring smooth operations without unexpectedly exceeding license limits (#431)
36+
- Creating user with sole role `create-vault` was unable to login due to missing role `user`
37+
- Fix disabling users via user and group management
3238

3339
## [1.4.6](https://github.com/cryptomator/hub/compare/1.4.5...1.4.6)
3440

backend/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
1111
<project.jdk.version>21</project.jdk.version>
1212
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
13-
<quarkus.platform.version>3.27.2</quarkus.platform.version>
13+
<quarkus.platform.version>3.27.3</quarkus.platform.version>
1414
<jwt.version>4.5.1</jwt.version>
1515
<compiler-plugin.version>3.15.0</compiler-plugin.version>
1616
<dependency-plugin.version>3.10.0</dependency-plugin.version>

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import jakarta.ws.rs.QueryParam;
1111
import jakarta.ws.rs.core.MediaType;
1212
import org.cryptomator.hub.entities.Authority;
13+
import org.cryptomator.hub.entities.User;
1314
import org.eclipse.microprofile.openapi.annotations.Operation;
1415
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
1516
import org.jboss.resteasy.reactive.NoCache;
@@ -31,7 +32,7 @@ public class AuthorityResource {
3132
@Operation(summary = "search authority by name")
3233
@Transactional
3334
public List<AuthorityDto> search(@QueryParam("query") @NotBlank String query, @QueryParam("withMemberSize") boolean withMemberSize) {
34-
return authorityRepo.byName(query).map(authority -> AuthorityDto.fromEntity(authority, withMemberSize)).toList();
35+
return authorityRepo.byName(query).filter(a -> !(a instanceof User u) || u.isEnabled()).map(authority -> AuthorityDto.fromEntity(authority, withMemberSize)).toList();
3536
}
3637

3738
@GET

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,21 @@ public List<DeviceDto> getSomeLegacy(@QueryParam("ids") List<String> deviceIds)
102102
return legacyDeviceRepo.findAllInList(deviceIds).map(DeviceDto::fromEntity).toList();
103103
}
104104

105+
/**
106+
* @deprecated to be removed in <a href="https://github.com/cryptomator/hub/issues/333">#333</a>
107+
*/
108+
@Deprecated(since = "1.3.0", forRemoval = true)
109+
@GET
110+
@Path("/has-legacy-devices")
111+
@RolesAllowed("admin")
112+
@Produces(MediaType.APPLICATION_JSON)
113+
@Transactional
114+
@Operation(summary = "checks if any user has legacy devices")
115+
@APIResponse(responseCode = "200")
116+
public boolean hasAnyLegacyDevices() {
117+
return legacyDeviceRepo.existsAny();
118+
}
119+
105120
@PUT
106121
@Path("/{deviceId}")
107122
@RolesAllowed("user")

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public final class UserDto extends AuthorityDto {
2121
private final String lastName;
2222
private final String language;
2323
private final Set<String> realmRoles;
24+
private final boolean enabled;
2425
private final Set<DeviceResource.DeviceDto> devices;
2526
private final String ecdhPublicKey;
2627
private final String ecdsaPublicKey;
@@ -37,6 +38,7 @@ public UserDto(
3738
@JsonProperty("lastName") String lastName,
3839
@JsonProperty("language") String language,
3940
@JsonProperty("realmRoles") @NotNull Set<String> realmRoles,
41+
@JsonProperty("enabled") boolean enabled,
4042
@JsonProperty("devices") Set<DeviceResource.DeviceDto> devices,
4143
// Accept either "ecdhPublicKey" or the legacy "publicKey" on input
4244
@Nullable @JsonProperty("ecdhPublicKey") @OnlyBase64Chars String ecdhPublicKey,
@@ -52,6 +54,7 @@ public UserDto(
5254
this.lastName = lastName;
5355
this.language = language;
5456
this.realmRoles = realmRoles;
57+
this.enabled = enabled;
5558
this.devices = devices;
5659
this.ecdhPublicKey = ecdhPublicKey != null ? ecdhPublicKey : publicKey;
5760
this.ecdsaPublicKey = ecdsaPublicKey;
@@ -68,12 +71,13 @@ public UserDto(
6871
String lastName,
6972
String language,
7073
Set<String> realmRoles,
74+
boolean enabled,
7175
Set<DeviceResource.DeviceDto> devices,
7276
String ecdhPublicKey,
7377
String ecdsaPublicKey,
7478
String privateKeys,
7579
String setupCode) {
76-
this(id, name, pictureUrl, email, firstName, lastName, language, realmRoles, devices, ecdhPublicKey, ecdhPublicKey, ecdsaPublicKey, privateKeys, privateKeys, setupCode);
80+
this(id, name, pictureUrl, email, firstName, lastName, language, realmRoles, enabled, devices, ecdhPublicKey, ecdhPublicKey, ecdsaPublicKey, privateKeys, privateKeys, setupCode);
7781
}
7882

7983
@JsonProperty("email")
@@ -101,6 +105,11 @@ public Set<String> getRealmRoles() {
101105
return realmRoles;
102106
}
103107

108+
@JsonProperty("enabled")
109+
public boolean isEnabled() {
110+
return enabled;
111+
}
112+
104113
@JsonProperty("devices")
105114
public Set<DeviceResource.DeviceDto> getDevices() {
106115
return devices;
@@ -158,6 +167,7 @@ public static UserDto justPublicInfo(User user) {
158167
user.getLastName(),
159168
user.getLanguage(),
160169
Set.of(user.getRealmRoles()),
170+
user.isEnabled(),
161171
Set.of(),
162172
user.getEcdhPublicKey(),
163173
user.getEcdsaPublicKey(),

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ public UserDto getMe(@QueryParam("withDevices") boolean withDevices, @QueryParam
190190
} else {
191191
deviceDtos = Set.of();
192192
}
193-
return new UserDto(user.getId(), user.getName(), user.getPictureUrl(), user.getEmail(), user.getFirstName(), user.getLastName(), user.getLanguage(), Set.of(user.getRealmRoles()), deviceDtos, user.getEcdhPublicKey(), user.getEcdsaPublicKey(), user.getPrivateKeys(), user.getSetupCode());
193+
return new UserDto(user.getId(), user.getName(), user.getPictureUrl(), user.getEmail(), user.getFirstName(), user.getLastName(), user.getLanguage(), Set.of(user.getRealmRoles()), user.isEnabled(), deviceDtos, user.getEcdhPublicKey(), user.getEcdsaPublicKey(), user.getPrivateKeys(), user.getSetupCode());
194194
}
195195

196196
/**
@@ -214,7 +214,7 @@ public UserDto getMeWithLegacyDevicesAndAccess() {
214214
var event = events.get(d.getId());
215215
return DeviceResource.DeviceDto.fromEntity(d, event);
216216
}).collect(Collectors.toSet());
217-
return new UserDto(user.getId(), user.getName(), user.getPictureUrl(), user.getEmail(), user.getFirstName(), user.getLastName(), user.getLanguage(), Set.of(user.getRealmRoles()), deviceDtos, user.getEcdhPublicKey(), user.getEcdsaPublicKey(), user.getPrivateKeys(), user.getSetupCode());
217+
return new UserDto(user.getId(), user.getName(), user.getPictureUrl(), user.getEmail(), user.getFirstName(), user.getLastName(), user.getLanguage(), Set.of(user.getRealmRoles()), user.isEnabled(), deviceDtos, user.getEcdhPublicKey(), user.getEcdsaPublicKey(), user.getPrivateKeys(), user.getSetupCode());
218218
}
219219

220220
@POST
@@ -424,6 +424,18 @@ public UserDto updateUser(@PathParam("id") String userId, @Valid @NotNull Update
424424
return UserDto.justPublicInfo(user);
425425
}
426426

427+
@PUT
428+
@Path("/{id}/enabled")
429+
@RolesAllowed("admin")
430+
@Consumes(MediaType.TEXT_PLAIN)
431+
@Transactional
432+
@Operation(summary = "enable or disable a user")
433+
@APIResponse(responseCode = "204", description = "user updated")
434+
public Response setUserEnabled(@PathParam("id") String userId, boolean enabled) {
435+
keycloakAdminService.setUserEnabled(userId, enabled);
436+
return Response.noContent().build();
437+
}
438+
427439
@DELETE
428440
@Path("/{id}")
429441
@RolesAllowed("admin")

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

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ public List<VaultDto> getAllVaults() {
184184
@APIResponse(responseCode = "200")
185185
@APIResponse(responseCode = "403", description = "not a vault owner")
186186
public List<MemberDto> getDirectMembers(@PathParam("vaultId") UUID vaultId) {
187-
return vaultAccessRepo.forVault(vaultId).map(access -> switch (access.getAuthority()) {
187+
return vaultAccessRepo.forVault(vaultId).filter(access -> !(access.getAuthority() instanceof User u) || u.isEnabled()).map(access -> switch (access.getAuthority()) {
188188
case User u -> MemberDto.fromEntity(u, access.getRole());
189189
case Group g -> MemberDto.fromEntity(g, access.getRole());
190190
default -> throw new IllegalStateException();
@@ -501,7 +501,7 @@ public Response grantAccess(@PathParam("vaultId") UUID vaultId, @NotEmpty Map<St
501501
@GET
502502
@Path("/{vaultId}")
503503
@RolesAllowed("user")
504-
@VaultRole(value = {VaultAccess.Role.MEMBER, VaultAccess.Role.OWNER}, bypassForRealmRole = true, realmRole = RealmRole.ADMIN, onMissingVault = VaultRole.OnMissingVault.NOT_FOUND)
504+
@VaultRole(value = {VaultAccess.Role.MEMBER, VaultAccess.Role.OWNER}, bypassForRealmRole = { RealmRole.ADMIN }, onMissingVault = @VaultRole.OnMissingVault(VaultRole.OnMissingVault.Action.NOT_FOUND))
505505
@Produces(MediaType.APPLICATION_JSON)
506506
@Transactional
507507
@Operation(summary = "gets a vault")
@@ -512,15 +512,49 @@ public VaultDto get(@PathParam("vaultId") UUID vaultId) {
512512
return VaultDto.fromEntity(vault);
513513
}
514514

515+
@PUT
516+
@Path("/{vaultId}/archived")
517+
@RolesAllowed("user")
518+
@VaultRole(value = VaultAccess.Role.OWNER, bypassForRealmRole = { RealmRole.ADMIN }, onMissingVault = @VaultRole.OnMissingVault(VaultRole.OnMissingVault.Action.NOT_FOUND))
519+
@Consumes(MediaType.TEXT_PLAIN)
520+
@Produces(MediaType.APPLICATION_JSON)
521+
@Transactional
522+
@Operation(summary = "sets the archived flag of a vault")
523+
@APIResponse(responseCode = "200", description = "archived flag updated")
524+
@APIResponse(responseCode = "402", description = "number of licensed seats would be exceeded after unarchiving")
525+
@APIResponse(responseCode = "403", description = "requesting user is neither a vault owner nor has the admin role")
526+
@APIResponse(responseCode = "404", description = "vault not found")
527+
public VaultDto setArchived(@PathParam("vaultId") UUID vaultId, @NotNull Boolean archived) {
528+
Vault vault = vaultRepo.findByIdOptional(vaultId).orElseThrow(NotFoundException::new);
529+
if (vault.isArchived() && !archived) {
530+
// resolve group members and simulate new seat count:
531+
var authorityIds = vaultAccessRepo.forVault(vaultId)
532+
.map(va -> va.getId().authorityId())
533+
.collect(Collectors.toSet());
534+
var effectiveUsers = new HashSet<User>();
535+
effectiveUsers.addAll(userRepo.getEffectiveGroupUsers(authorityIds));
536+
effectiveUsers.addAll(userRepo.findByIds(authorityIds).toList());
537+
var projectedSeatUsers = new HashSet<>(effectiveVaultAccessRepo.usersSeatedOnOtherVaults(vaultId).toList());
538+
projectedSeatUsers.addAll(effectiveUsers.stream().map(User::getId).toList());
539+
if (projectedSeatUsers.size() > license.getEntitlements().seats()) {
540+
throw new PaymentRequiredException("Number of effective vault users exceeds available license seats");
541+
}
542+
}
543+
vault.setArchived(archived);
544+
vaultRepo.persistAndFlush(vault);
545+
eventLogger.logVaultUpdated(jwt.getSubject(), vault.getId(), vault.getName(), vault.getDescription(), vault.isArchived());
546+
return VaultDto.fromEntity(vault);
547+
}
548+
515549
@PUT
516550
@Path("/{vaultId}")
517551
@RolesAllowed("user") // general authentication. VaultRole filter will check for specific access rights
518-
@VaultRole(value = VaultAccess.Role.OWNER, onMissingVault = VaultRole.OnMissingVault.REQUIRE_REALM_ROLE, realmRole = RealmRole.CREATE_VAULTS, bypassForEmergencyAccess = true)
552+
@VaultRole(value = VaultAccess.Role.OWNER, onMissingVault = @VaultRole.OnMissingVault(value = VaultRole.OnMissingVault.Action.REQUIRE_REALM_ROLE, realmRole = RealmRole.CREATE_VAULTS), bypassForEmergencyAccess = true)
519553
@Consumes(MediaType.APPLICATION_JSON)
520554
@Produces(MediaType.APPLICATION_JSON)
521555
@Transactional
522556
@Operation(summary = "creates or updates a vault",
523-
description = "Creates or updates a vault with the given vault id. The creationTime in the vaultDto is always ignored. On creation, the current server time is used and the archived field is ignored. On update, only the name, description, and archived fields are considered.")
557+
description = "Creates or updates a vault with the given vault id. The creationTime in the vaultDto is always ignored. The archived field is always ignored (use the dedicated endpoint). On creation, the current server time is used. On update, only the name and description fields are considered.")
524558
@APIResponse(responseCode = "200", description = "existing vault updated")
525559
@APIResponse(responseCode = "201", description = "new vault created")
526560
@APIResponse(responseCode = "402", description = "number of licensed seats is exceeded")
@@ -545,7 +579,6 @@ public Response createOrUpdate(@PathParam("vaultId") UUID vaultId, @Valid @NotNu
545579
// set regardless of whether vault is new or existing:
546580
vault.setName(vaultDto.name);
547581
vault.setDescription(vaultDto.description);
548-
vault.setArchived(existingVault.isEmpty() ? false : vaultDto.archived);
549582
var oldEmergencyKeyShares = vault.getEmergencyKeyShares().values();
550583
vault.setRequiredEmergencyKeyShares(vaultDto.requiredEmergencyKeyShares);
551584
vault.setEmergencyKeyShares(vaultDto.emergencyKeyShares);

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,44 +31,44 @@
3131
FROM User u
3232
INNER JOIN EffectiveVaultAccess eva ON u.id = eva.id.authorityId
3333
INNER JOIN Vault v ON eva.id.vaultId = v.id
34-
WHERE u.id = :userId AND NOT v.archived
34+
WHERE u.id = :userId AND NOT v.archived AND u.enabled
3535
""")
3636
@NamedQuery(name = "EffectiveVaultAccess.countSeatsOccupiedByUsers", query = """
3737
SELECT COUNT(DISTINCT u.id)
3838
FROM User u
3939
INNER JOIN EffectiveVaultAccess eva ON u.id = eva.id.authorityId
4040
INNER JOIN Vault v ON eva.id.vaultId = v.id
41-
WHERE u.id IN :userIds AND NOT v.archived
41+
WHERE u.id IN :userIds AND NOT v.archived AND u.enabled
4242
""")
4343
@NamedQuery(name = "EffectiveVaultAccess.usersSeatedOnOtherVaults", query = """
4444
SELECT DISTINCT u.id
4545
FROM User u
4646
INNER JOIN EffectiveVaultAccess eva ON u.id = eva.id.authorityId
4747
INNER JOIN Vault v ON eva.id.vaultId = v.id
48-
WHERE NOT v.archived AND v.id <> :vaultId
48+
WHERE NOT v.archived AND v.id <> :vaultId AND u.enabled
4949
""")
5050
@NamedQuery(name = "EffectiveVaultAccess.countSeatOccupyingUsers", query = """
5151
SELECT COUNT(DISTINCT u.id)
5252
FROM User u
5353
INNER JOIN EffectiveVaultAccess eva ON u.id = eva.id.authorityId
5454
INNER JOIN Vault v ON eva.id.vaultId = v.id
55-
WHERE NOT v.archived
55+
WHERE NOT v.archived AND u.enabled
5656
""")
5757
@NamedQuery(name = "EffectiveVaultAccess.countSeatOccupyingUsersWithAccessToken", query = """
5858
SELECT COUNT(DISTINCT u.id)
5959
FROM User u
6060
INNER JOIN EffectiveVaultAccess eva ON u.id = eva.id.authorityId
6161
INNER JOIN Vault v ON eva.id.vaultId = v.id
6262
INNER JOIN AccessToken at ON eva.id.vaultId = at.id.vaultId AND eva.id.authorityId = at.id.userId
63-
WHERE NOT v.archived
63+
WHERE NOT v.archived AND u.enabled
6464
""")
6565
@NamedQuery(name = "EffectiveVaultAccess.countSeatOccupyingUsersOfGroup", query = """
6666
SELECT COUNT(DISTINCT u.id)
6767
FROM User u
6868
INNER JOIN EffectiveVaultAccess eva ON u.id = eva.id.authorityId
6969
INNER JOIN EffectiveGroupMembership egm ON u.id = egm.id.memberId
7070
INNER JOIN Vault v ON eva.id.vaultId = v.id
71-
WHERE egm.id.groupId = :groupId AND NOT v.archived
71+
WHERE egm.id.groupId = :groupId AND NOT v.archived AND u.enabled
7272
""")
7373
@NamedQuery(name = "EffectiveVaultAccess.findByAuthorityAndVault", query = """
7474
SELECT eva

0 commit comments

Comments
 (0)