Skip to content

Commit 194a50c

Browse files
committed
Merge branch 'release/1.3.4'
2 parents edfc62b + 813ed99 commit 194a50c

23 files changed

+1025
-926
lines changed

Diff for: backend/pom.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<modelVersion>4.0.0</modelVersion>
55
<groupId>org.cryptomator</groupId>
66
<artifactId>hub-backend</artifactId>
7-
<version>1.3.3</version>
7+
<version>1.3.4</version>
88

99
<properties>
1010
<compiler-plugin.version>3.11.0</compiler-plugin.version>
@@ -13,7 +13,7 @@
1313
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
1414
<quarkus.container-image.group>cryptomator</quarkus.container-image.group>
1515
<quarkus.container-image.name>hub</quarkus.container-image.name>
16-
<quarkus.platform.version>3.2.10.Final</quarkus.platform.version>
16+
<quarkus.platform.version>3.2.12.Final</quarkus.platform.version>
1717
<quarkus.jib.base-jvm-image>eclipse-temurin:17-jre</quarkus.jib.base-jvm-image> <!-- irrelevant for -Pnative -->
1818
<jwt.version>4.4.0</jwt.version>
1919
<surefire-plugin.version>3.1.2</surefire-plugin.version>

Diff for: backend/src/main/java/org/cryptomator/hub/api/AuditLogResource.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public class AuditLogResource {
5353
@APIResponse(responseCode = "200", description = "Body contains list of events in the specified time interval")
5454
@APIResponse(responseCode = "400", description = "startDate or endDate not specified, startDate > endDate, order specified and not in ['asc','desc'] or pageSize not in [1 .. 100]")
5555
@APIResponse(responseCode = "402", description = "Community license used or license expired")
56-
@APIResponse(responseCode = "403", description = "requesting user is does not have admin role")
56+
@APIResponse(responseCode = "403", description = "requesting user does not have admin role")
5757
public List<AuditEventDto> getAllEvents(@QueryParam("startDate") Instant startDate, @QueryParam("endDate") Instant endDate, @QueryParam("paginationId") Long paginationId, @QueryParam("order") @DefaultValue("desc") String order, @QueryParam("pageSize") @DefaultValue("20") int pageSize) {
5858
if (!license.isSet() || license.isExpired()) {
5959
throw new PaymentRequiredException("Community license used or license expired");

Diff for: backend/src/main/java/org/cryptomator/hub/api/BillingResource.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.time.Instant;
2525
import java.util.Optional;
2626

27+
//TODO: redirect ot /license path
2728
@Path("/billing")
2829
public class BillingResource {
2930

@@ -69,7 +70,7 @@ public record BillingDto(@JsonProperty("hubId") String hubId, @JsonProperty("has
6970
@JsonProperty("issuedAt") Instant issuedAt, @JsonProperty("expiresAt") Instant expiresAt, @JsonProperty("managedInstance") Boolean managedInstance) {
7071

7172
public static BillingDto create(String hubId, LicenseHolder licenseHolder) {
72-
var licensedSeats = licenseHolder.getNoLicenseSeats();
73+
var licensedSeats = licenseHolder.getSeats();
7374
var usedSeats = EffectiveVaultAccess.countSeatOccupyingUsers();
7475
var managedInstance = licenseHolder.isManagedInstance();
7576
return new BillingDto(hubId, false, null, (int) licensedSeats, (int) usedSeats, null, null, managedInstance);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package org.cryptomator.hub.api;
2+
3+
import com.auth0.jwt.interfaces.DecodedJWT;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import jakarta.annotation.security.RolesAllowed;
6+
import jakarta.inject.Inject;
7+
import jakarta.ws.rs.GET;
8+
import jakarta.ws.rs.Path;
9+
import jakarta.ws.rs.Produces;
10+
import jakarta.ws.rs.core.MediaType;
11+
import org.cryptomator.hub.entities.EffectiveVaultAccess;
12+
import org.cryptomator.hub.license.LicenseHolder;
13+
import org.eclipse.microprofile.openapi.annotations.Operation;
14+
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
15+
16+
import java.time.Instant;
17+
import java.util.Optional;
18+
19+
@Path("/license")
20+
public class LicenseResource {
21+
22+
@Inject
23+
LicenseHolder licenseHolder;
24+
25+
@GET
26+
@Path("/user-info")
27+
@Produces(MediaType.APPLICATION_JSON)
28+
@RolesAllowed("user")
29+
@Operation(summary = "Get license information for regular users", description = "Information includes the licensed seats, the already used seats and if defined, the license expiration date.")
30+
@APIResponse(responseCode = "200")
31+
public LicenseUserInfoDto get() {
32+
return LicenseUserInfoDto.create(licenseHolder);
33+
}
34+
35+
36+
public record LicenseUserInfoDto(@JsonProperty("licensedSeats") Integer licensedSeats,
37+
@JsonProperty("usedSeats") Integer usedSeats,
38+
@JsonProperty("expiresAt") Instant expiresAt) {
39+
40+
public static LicenseUserInfoDto create(LicenseHolder licenseHolder) {
41+
var licensedSeats = (int) licenseHolder.getSeats();
42+
var usedSeats = (int) EffectiveVaultAccess.countSeatOccupyingUsers();
43+
var expiresAt = Optional.ofNullable(licenseHolder.get()).map(DecodedJWT::getExpiresAtAsInstant).orElse(null);
44+
return new LicenseUserInfoDto(licensedSeats, usedSeats, expiresAt);
45+
}
46+
47+
}
48+
49+
}

Diff for: backend/src/main/java/org/cryptomator/hub/api/VaultResource.java

+25-29
Original file line numberDiff line numberDiff line change
@@ -152,22 +152,24 @@ public List<MemberDto> getDirectMembers(@PathParam("vaultId") UUID vaultId) {
152152
@Parameter(name = "role", in = ParameterIn.QUERY, description = "the role to grant to this user (defaults to MEMBER)")
153153
@APIResponse(responseCode = "200", description = "user's role updated")
154154
@APIResponse(responseCode = "201", description = "user added")
155-
@APIResponse(responseCode = "402", description = "all seats in license used")
155+
@APIResponse(responseCode = "402", description = "license is expired or licensed seats would be exceeded after the operation")
156156
@APIResponse(responseCode = "403", description = "not a vault owner")
157157
@APIResponse(responseCode = "404", description = "user not found")
158158
@ActiveLicense
159159
public Response addUser(@PathParam("vaultId") UUID vaultId, @PathParam("userId") @ValidId String userId, @QueryParam("role") @DefaultValue("MEMBER") VaultAccess.Role role) {
160-
var vault = Vault.<Vault>findById(vaultId); // // should always be found, since @VaultRole filter would have triggered
160+
var vault = Vault.<Vault>findById(vaultId); // should always be found, since @VaultRole filter would have triggered
161161
var user = User.<User>findByIdOptional(userId).orElseThrow(NotFoundException::new);
162-
if (!EffectiveVaultAccess.isUserOccupyingSeat(userId)) {
163-
//for new user, we need to check if a license seat is available
164-
var usedSeats = EffectiveVaultAccess.countSeatOccupyingUsers();
165-
if (usedSeats >= license.getAvailableSeats()) {
166-
throw new PaymentRequiredException("Number of effective vault users greater than or equal to the available license seats");
167-
}
162+
var usedSeats = EffectiveVaultAccess.countSeatOccupyingUsers();
163+
//check if license seats are free
164+
if (usedSeats < license.getSeats()) {
165+
return addAuthority(vault, user, role);
168166
}
169-
170-
return addAuthority(vault, user, role);
167+
// else check, if all seats are taken, but the person to add is already sitting
168+
if (usedSeats == license.getSeats() && EffectiveVaultAccess.isUserOccupyingSeat(userId)) {
169+
return addAuthority(vault, user, role);
170+
}
171+
//otherwise block
172+
throw new PaymentRequiredException("Number of effective vault users greater than or equal to the available license seats");
171173
}
172174

173175
@PUT
@@ -180,7 +182,7 @@ public Response addUser(@PathParam("vaultId") UUID vaultId, @PathParam("userId")
180182
@Parameter(name = "role", in = ParameterIn.QUERY, description = "the role to grant to this group (defaults to MEMBER)")
181183
@APIResponse(responseCode = "200", description = "group's role updated")
182184
@APIResponse(responseCode = "201", description = "group added")
183-
@APIResponse(responseCode = "402", description = "used seats + (number of users in group not occupying a seats) exceeds number of total avaible seats in license")
185+
@APIResponse(responseCode = "402", description = "license is expired or licensed seats would be exceeded after the operation")
184186
@APIResponse(responseCode = "403", description = "not a vault owner")
185187
@APIResponse(responseCode = "404", description = "group not found")
186188
@ActiveLicense
@@ -189,7 +191,7 @@ public Response addGroup(@PathParam("vaultId") UUID vaultId, @PathParam("groupId
189191
var group = Group.<Group>findByIdOptional(groupId).orElseThrow(NotFoundException::new);
190192

191193
//usersInGroup - usersInGroupAndPartOfAtLeastOneVault + usersOfAtLeastOneVault
192-
if (EffectiveGroupMembership.countEffectiveGroupUsers(groupId) - EffectiveVaultAccess.countSeatOccupyingUsersOfGroup(groupId) + EffectiveVaultAccess.countSeatOccupyingUsers() > license.getAvailableSeats()) {
194+
if (EffectiveGroupMembership.countEffectiveGroupUsers(groupId) - EffectiveVaultAccess.countSeatOccupyingUsersOfGroup(groupId) + EffectiveVaultAccess.countSeatOccupyingUsers() > license.getSeats()) {
193195
throw new PaymentRequiredException("Number of effective vault users greater than or equal to the available license seats");
194196
}
195197

@@ -265,8 +267,8 @@ public Response legacyUnlock(@PathParam("vaultId") UUID vaultId, @PathParam("dev
265267
throw new GoneException("Vault is archived.");
266268
}
267269

268-
var usedSeats = EffectiveVaultAccess.countSeatOccupyingUsersWithAccessToken();
269-
if (usedSeats > license.getAvailableSeats()) {
270+
var accessTokenSeats = EffectiveVaultAccess.countSeatOccupyingUsersWithAccessToken();
271+
if (accessTokenSeats > license.getSeats()) {
270272
throw new PaymentRequiredException("Number of effective vault users exceeds available license seats");
271273
}
272274

@@ -302,8 +304,8 @@ public Response unlock(@PathParam("vaultId") UUID vaultId, @QueryParam("evenIfAr
302304
throw new GoneException("Vault is archived.");
303305
}
304306

305-
var usedSeats = EffectiveVaultAccess.countSeatOccupyingUsersWithAccessToken();
306-
if (usedSeats > license.getAvailableSeats()) {
307+
var accessTokenSeats = EffectiveVaultAccess.countSeatOccupyingUsersWithAccessToken();
308+
if (accessTokenSeats > license.getSeats()) {
307309
throw new PaymentRequiredException("Number of effective vault users exceeds available license seats");
308310
}
309311

@@ -337,18 +339,14 @@ public Response unlock(@PathParam("vaultId") UUID vaultId, @QueryParam("evenIfAr
337339
@APIResponse(responseCode = "402", description = "number of users granted access exceeds available license seats")
338340
@APIResponse(responseCode = "403", description = "not a vault owner")
339341
@APIResponse(responseCode = "404", description = "at least one user has not been found")
340-
@APIResponse(responseCode = "410", description = "vault is archived")
341342
public Response grantAccess(@PathParam("vaultId") UUID vaultId, @NotEmpty Map<String, String> tokens) {
342343
var vault = Vault.<Vault>findById(vaultId); // should always be found, since @VaultRole filter would have triggered
343-
if (vault.archived) {
344-
throw new GoneException("Vault is archived.");
345-
}
346344

347345
// check number of available seats
348346
long occupiedSeats = EffectiveVaultAccess.countSeatOccupyingUsers();
349347
long usersWithoutSeat = tokens.size() - EffectiveVaultAccess.countSeatsOccupiedByUsers(tokens.keySet().stream().toList());
350348

351-
if (occupiedSeats + usersWithoutSeat > license.getAvailableSeats()) {
349+
if (occupiedSeats + usersWithoutSeat > license.getSeats()) {
352350
throw new PaymentRequiredException("Number of effective vault users greater than or equal to the available license seats");
353351
}
354352

@@ -375,7 +373,7 @@ public Response grantAccess(@PathParam("vaultId") UUID vaultId, @NotEmpty Map<St
375373
@Transactional
376374
@Operation(summary = "gets a vault")
377375
@APIResponse(responseCode = "200")
378-
@APIResponse(responseCode = "403", description = "requesting user is not member of the vault")
376+
@APIResponse(responseCode = "403", description = "requesting user is neither a vault member nor has the admin role")
379377
public VaultDto get(@PathParam("vaultId") UUID vaultId) {
380378
Vault vault = Vault.<Vault>findByIdOptional(vaultId).orElseThrow(NotFoundException::new);
381379
if (vault.effectiveMembers.stream().noneMatch(u -> u.id.equals(jwt.getSubject())) && !identity.getRoles().contains("admin")) {
@@ -395,7 +393,7 @@ public VaultDto get(@PathParam("vaultId") UUID vaultId) {
395393
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.")
396394
@APIResponse(responseCode = "200", description = "existing vault updated")
397395
@APIResponse(responseCode = "201", description = "new vault created")
398-
@APIResponse(responseCode = "402", description = "all seats in licence in use during creation of new vault")
396+
@APIResponse(responseCode = "402", description = "number of licensed seats is exceeded")
399397
public Response createOrUpdate(@PathParam("vaultId") UUID vaultId, @Valid @NotNull VaultDto vaultDto) {
400398
User currentUser = User.findById(jwt.getSubject());
401399
Optional<Vault> existingVault = Vault.findByIdOptional(vaultId);
@@ -404,12 +402,10 @@ public Response createOrUpdate(@PathParam("vaultId") UUID vaultId, @Valid @NotNu
404402
// load existing vault:
405403
vault = existingVault.get();
406404
} else {
407-
if (!EffectiveVaultAccess.isUserOccupyingSeat(currentUser.id)) {
408-
//for new vaults, we need to check that a licence seat is available if the user does not already have access to a vault.
409-
var usedSeats = EffectiveVaultAccess.countSeatOccupyingUsers();
410-
if (usedSeats >= license.getAvailableSeats()) {
411-
throw new PaymentRequiredException("Number of effective vault users exceeds available license seats");
412-
}
405+
//if license is exceeded block vault creation, independent if the user is already sitting
406+
var usedSeats = EffectiveVaultAccess.countSeatOccupyingUsers();
407+
if (usedSeats > license.getSeats()) {
408+
throw new PaymentRequiredException("Number of effective vault users exceeds available license seats");
413409
}
414410
// create new vault:
415411
vault = new Vault();

Diff for: backend/src/main/java/org/cryptomator/hub/license/LicenseHolder.java

+7-6
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ void applyInitialHubIdAndLicense(String initialId, String initialLicense) {
8383
settings.hubId = initialId;
8484
settings.persistAndFlush();
8585
} catch (JWTVerificationException e) {
86-
LOG.warn("Provided initial license is invalid.");
86+
LOG.warn("Provided initial license is invalid.", e);
8787
}
8888
}
8989

@@ -168,18 +168,19 @@ public boolean isExpired() {
168168
}
169169

170170
/**
171-
* Gets the number of available seats of the license
171+
* Gets the number of seats in the license
172172
*
173-
* @return Number of available seats, if license is not null. Otherwise {@value SelfHostedNoLicenseConstants#SEATS}.
173+
* @return Number of seats of the license, if license is not null. Otherwise {@value SelfHostedNoLicenseConstants#SEATS}.
174174
*/
175-
public long getAvailableSeats() {
175+
public long getSeats() {
176176
return Optional.ofNullable(license) //
177177
.map(l -> l.getClaim("seats")) //
178178
.map(Claim::asLong) //
179-
.orElseGet(this::getNoLicenseSeats);
179+
.orElseGet(this::seatsOnNotExisingLicense);
180180
}
181181

182-
public long getNoLicenseSeats() {
182+
//visible for testing
183+
public long seatsOnNotExisingLicense() {
183184
if (!managedInstance) {
184185
return SelfHostedNoLicenseConstants.SEATS;
185186
} else {

Diff for: backend/src/main/resources/application.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ hub.keycloak.oidc.cryptomator-client-id=cryptomator
3333
%dev.quarkus.keycloak.devservices.start-command=start-dev
3434
%dev.quarkus.keycloak.devservices.port=8180
3535
%dev.quarkus.keycloak.devservices.service-name=quarkus-cryptomator-hub
36-
%dev.quarkus.keycloak.devservices.image-name=ghcr.io/cryptomator/keycloak:23.0.6
36+
%dev.quarkus.keycloak.devservices.image-name=ghcr.io/cryptomator/keycloak:23.0.7
3737
%dev.quarkus.oidc.devui.grant.type=code
3838
# OIDC will be mocked during unit tests. Use fake auth url to prevent dev services to start:
3939
%test.quarkus.oidc.auth-server-url=http://localhost:43210/dev/null

Diff for: backend/src/test/java/org/cryptomator/hub/api/BillingResourceTest.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@ public class AsAdmin {
5555
@DisplayName("GET /billing returns 200 with empty license self-hosted")
5656
public void testGetEmptySelfHosted() {
5757
Mockito.when(licenseHolder.get()).thenReturn(null);
58-
Mockito.when(licenseHolder.getNoLicenseSeats()).thenReturn(5L);
59-
Mockito.when(licenseHolder.getAvailableSeats()).thenReturn(3L);
58+
Mockito.when(licenseHolder.getSeats()).thenReturn(5L);
6059
when().get("/billing")
6160
.then().statusCode(200)
6261
.body("hubId", is("42"))

0 commit comments

Comments
 (0)