Skip to content

Commit

Permalink
Merge branch 'release/1.3.4'
Browse files Browse the repository at this point in the history
  • Loading branch information
infeo committed May 7, 2024
2 parents edfc62b + 813ed99 commit 194a50c
Show file tree
Hide file tree
Showing 23 changed files with 1,025 additions and 926 deletions.
4 changes: 2 additions & 2 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>hub-backend</artifactId>
<version>1.3.3</version>
<version>1.3.4</version>

<properties>
<compiler-plugin.version>3.11.0</compiler-plugin.version>
Expand All @@ -13,7 +13,7 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.container-image.group>cryptomator</quarkus.container-image.group>
<quarkus.container-image.name>hub</quarkus.container-image.name>
<quarkus.platform.version>3.2.10.Final</quarkus.platform.version>
<quarkus.platform.version>3.2.12.Final</quarkus.platform.version>
<quarkus.jib.base-jvm-image>eclipse-temurin:17-jre</quarkus.jib.base-jvm-image> <!-- irrelevant for -Pnative -->
<jwt.version>4.4.0</jwt.version>
<surefire-plugin.version>3.1.2</surefire-plugin.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public class AuditLogResource {
@APIResponse(responseCode = "200", description = "Body contains list of events in the specified time interval")
@APIResponse(responseCode = "400", description = "startDate or endDate not specified, startDate > endDate, order specified and not in ['asc','desc'] or pageSize not in [1 .. 100]")
@APIResponse(responseCode = "402", description = "Community license used or license expired")
@APIResponse(responseCode = "403", description = "requesting user is does not have admin role")
@APIResponse(responseCode = "403", description = "requesting user does not have admin role")
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) {
if (!license.isSet() || license.isExpired()) {
throw new PaymentRequiredException("Community license used or license expired");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.time.Instant;
import java.util.Optional;

//TODO: redirect ot /license path
@Path("/billing")
public class BillingResource {

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

public static BillingDto create(String hubId, LicenseHolder licenseHolder) {
var licensedSeats = licenseHolder.getNoLicenseSeats();
var licensedSeats = licenseHolder.getSeats();
var usedSeats = EffectiveVaultAccess.countSeatOccupyingUsers();
var managedInstance = licenseHolder.isManagedInstance();
return new BillingDto(hubId, false, null, (int) licensedSeats, (int) usedSeats, null, null, managedInstance);
Expand Down
49 changes: 49 additions & 0 deletions backend/src/main/java/org/cryptomator/hub/api/LicenseResource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.cryptomator.hub.api;

import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.cryptomator.hub.entities.EffectiveVaultAccess;
import org.cryptomator.hub.license.LicenseHolder;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;

import java.time.Instant;
import java.util.Optional;

@Path("/license")
public class LicenseResource {

@Inject
LicenseHolder licenseHolder;

@GET
@Path("/user-info")
@Produces(MediaType.APPLICATION_JSON)
@RolesAllowed("user")
@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.")
@APIResponse(responseCode = "200")
public LicenseUserInfoDto get() {
return LicenseUserInfoDto.create(licenseHolder);
}


public record LicenseUserInfoDto(@JsonProperty("licensedSeats") Integer licensedSeats,
@JsonProperty("usedSeats") Integer usedSeats,
@JsonProperty("expiresAt") Instant expiresAt) {

public static LicenseUserInfoDto create(LicenseHolder licenseHolder) {
var licensedSeats = (int) licenseHolder.getSeats();
var usedSeats = (int) EffectiveVaultAccess.countSeatOccupyingUsers();
var expiresAt = Optional.ofNullable(licenseHolder.get()).map(DecodedJWT::getExpiresAtAsInstant).orElse(null);
return new LicenseUserInfoDto(licensedSeats, usedSeats, expiresAt);
}

}

}
54 changes: 25 additions & 29 deletions backend/src/main/java/org/cryptomator/hub/api/VaultResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -152,22 +152,24 @@ public List<MemberDto> getDirectMembers(@PathParam("vaultId") UUID vaultId) {
@Parameter(name = "role", in = ParameterIn.QUERY, description = "the role to grant to this user (defaults to MEMBER)")
@APIResponse(responseCode = "200", description = "user's role updated")
@APIResponse(responseCode = "201", description = "user added")
@APIResponse(responseCode = "402", description = "all seats in license used")
@APIResponse(responseCode = "402", description = "license is expired or licensed seats would be exceeded after the operation")
@APIResponse(responseCode = "403", description = "not a vault owner")
@APIResponse(responseCode = "404", description = "user not found")
@ActiveLicense
public Response addUser(@PathParam("vaultId") UUID vaultId, @PathParam("userId") @ValidId String userId, @QueryParam("role") @DefaultValue("MEMBER") VaultAccess.Role role) {
var vault = Vault.<Vault>findById(vaultId); // // should always be found, since @VaultRole filter would have triggered
var vault = Vault.<Vault>findById(vaultId); // should always be found, since @VaultRole filter would have triggered
var user = User.<User>findByIdOptional(userId).orElseThrow(NotFoundException::new);
if (!EffectiveVaultAccess.isUserOccupyingSeat(userId)) {
//for new user, we need to check if a license seat is available
var usedSeats = EffectiveVaultAccess.countSeatOccupyingUsers();
if (usedSeats >= license.getAvailableSeats()) {
throw new PaymentRequiredException("Number of effective vault users greater than or equal to the available license seats");
}
var usedSeats = EffectiveVaultAccess.countSeatOccupyingUsers();
//check if license seats are free
if (usedSeats < license.getSeats()) {
return addAuthority(vault, user, role);
}

return addAuthority(vault, user, role);
// else check, if all seats are taken, but the person to add is already sitting
if (usedSeats == license.getSeats() && EffectiveVaultAccess.isUserOccupyingSeat(userId)) {
return addAuthority(vault, user, role);
}
//otherwise block
throw new PaymentRequiredException("Number of effective vault users greater than or equal to the available license seats");
}

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

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

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

var usedSeats = EffectiveVaultAccess.countSeatOccupyingUsersWithAccessToken();
if (usedSeats > license.getAvailableSeats()) {
var accessTokenSeats = EffectiveVaultAccess.countSeatOccupyingUsersWithAccessToken();
if (accessTokenSeats > license.getSeats()) {
throw new PaymentRequiredException("Number of effective vault users exceeds available license seats");
}

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

var usedSeats = EffectiveVaultAccess.countSeatOccupyingUsersWithAccessToken();
if (usedSeats > license.getAvailableSeats()) {
var accessTokenSeats = EffectiveVaultAccess.countSeatOccupyingUsersWithAccessToken();
if (accessTokenSeats > license.getSeats()) {
throw new PaymentRequiredException("Number of effective vault users exceeds available license seats");
}

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

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

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

Expand All @@ -375,7 +373,7 @@ public Response grantAccess(@PathParam("vaultId") UUID vaultId, @NotEmpty Map<St
@Transactional
@Operation(summary = "gets a vault")
@APIResponse(responseCode = "200")
@APIResponse(responseCode = "403", description = "requesting user is not member of the vault")
@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 = Vault.<Vault>findByIdOptional(vaultId).orElseThrow(NotFoundException::new);
if (vault.effectiveMembers.stream().noneMatch(u -> u.id.equals(jwt.getSubject())) && !identity.getRoles().contains("admin")) {
Expand All @@ -395,7 +393,7 @@ public VaultDto get(@PathParam("vaultId") UUID vaultId) {
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.")
@APIResponse(responseCode = "200", description = "existing vault updated")
@APIResponse(responseCode = "201", description = "new vault created")
@APIResponse(responseCode = "402", description = "all seats in licence in use during creation of new vault")
@APIResponse(responseCode = "402", description = "number of licensed seats is exceeded")
public Response createOrUpdate(@PathParam("vaultId") UUID vaultId, @Valid @NotNull VaultDto vaultDto) {
User currentUser = User.findById(jwt.getSubject());
Optional<Vault> existingVault = Vault.findByIdOptional(vaultId);
Expand All @@ -404,12 +402,10 @@ public Response createOrUpdate(@PathParam("vaultId") UUID vaultId, @Valid @NotNu
// load existing vault:
vault = existingVault.get();
} else {
if (!EffectiveVaultAccess.isUserOccupyingSeat(currentUser.id)) {
//for new vaults, we need to check that a licence seat is available if the user does not already have access to a vault.
var usedSeats = EffectiveVaultAccess.countSeatOccupyingUsers();
if (usedSeats >= license.getAvailableSeats()) {
throw new PaymentRequiredException("Number of effective vault users exceeds available license seats");
}
//if license is exceeded block vault creation, independent if the user is already sitting
var usedSeats = EffectiveVaultAccess.countSeatOccupyingUsers();
if (usedSeats > license.getSeats()) {
throw new PaymentRequiredException("Number of effective vault users exceeds available license seats");
}
// create new vault:
vault = new Vault();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ void applyInitialHubIdAndLicense(String initialId, String initialLicense) {
settings.hubId = initialId;
settings.persistAndFlush();
} catch (JWTVerificationException e) {
LOG.warn("Provided initial license is invalid.");
LOG.warn("Provided initial license is invalid.", e);
}
}

Expand Down Expand Up @@ -168,18 +168,19 @@ public boolean isExpired() {
}

/**
* Gets the number of available seats of the license
* Gets the number of seats in the license
*
* @return Number of available seats, if license is not null. Otherwise {@value SelfHostedNoLicenseConstants#SEATS}.
* @return Number of seats of the license, if license is not null. Otherwise {@value SelfHostedNoLicenseConstants#SEATS}.
*/
public long getAvailableSeats() {
public long getSeats() {
return Optional.ofNullable(license) //
.map(l -> l.getClaim("seats")) //
.map(Claim::asLong) //
.orElseGet(this::getNoLicenseSeats);
.orElseGet(this::seatsOnNotExisingLicense);
}

public long getNoLicenseSeats() {
//visible for testing
public long seatsOnNotExisingLicense() {
if (!managedInstance) {
return SelfHostedNoLicenseConstants.SEATS;
} else {
Expand Down
2 changes: 1 addition & 1 deletion backend/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ hub.keycloak.oidc.cryptomator-client-id=cryptomator
%dev.quarkus.keycloak.devservices.start-command=start-dev
%dev.quarkus.keycloak.devservices.port=8180
%dev.quarkus.keycloak.devservices.service-name=quarkus-cryptomator-hub
%dev.quarkus.keycloak.devservices.image-name=ghcr.io/cryptomator/keycloak:23.0.6
%dev.quarkus.keycloak.devservices.image-name=ghcr.io/cryptomator/keycloak:23.0.7
%dev.quarkus.oidc.devui.grant.type=code
# OIDC will be mocked during unit tests. Use fake auth url to prevent dev services to start:
%test.quarkus.oidc.auth-server-url=http://localhost:43210/dev/null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ public class AsAdmin {
@DisplayName("GET /billing returns 200 with empty license self-hosted")
public void testGetEmptySelfHosted() {
Mockito.when(licenseHolder.get()).thenReturn(null);
Mockito.when(licenseHolder.getNoLicenseSeats()).thenReturn(5L);
Mockito.when(licenseHolder.getAvailableSeats()).thenReturn(3L);
Mockito.when(licenseHolder.getSeats()).thenReturn(5L);
when().get("/billing")
.then().statusCode(200)
.body("hubId", is("42"))
Expand Down
Loading

0 comments on commit 194a50c

Please sign in to comment.