Skip to content

Commit edfc62b

Browse files
committed
Merge branch 'release/1.3.3'
2 parents 84549cd + 371ba0c commit edfc62b

19 files changed

+671
-399
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[![Build and test](https://github.com/cryptomator/hub/actions/workflows/buildAndTest.yml/badge.svg)](https://github.com/cryptomator/hub/actions/workflows/buildAndTest.yml)
1+
[![CI Build](https://github.com/cryptomator/hub/actions/workflows/build.yml/badge.svg)](https://github.com/cryptomator/hub/actions/workflows/build.yml)
22

33
# Cryptomator Hub
44

backend/pom.xml

+1-1
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.2</version>
7+
<version>1.3.3</version>
88

99
<properties>
1010
<compiler-plugin.version>3.11.0</compiler-plugin.version>

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

+7-7
Original file line numberDiff line numberDiff line change
@@ -65,25 +65,25 @@ public Response setToken(@NotNull @ValidJWS String token) {
6565
}
6666

6767
public record BillingDto(@JsonProperty("hubId") String hubId, @JsonProperty("hasLicense") Boolean hasLicense, @JsonProperty("email") String email,
68-
@JsonProperty("totalSeats") Integer totalSeats, @JsonProperty("remainingSeats") Integer remainingSeats,
68+
@JsonProperty("licensedSeats") Integer licensedSeats, @JsonProperty("usedSeats") Integer usedSeats,
6969
@JsonProperty("issuedAt") Instant issuedAt, @JsonProperty("expiresAt") Instant expiresAt, @JsonProperty("managedInstance") Boolean managedInstance) {
7070

7171
public static BillingDto create(String hubId, LicenseHolder licenseHolder) {
72-
var seats = licenseHolder.getNoLicenseSeats();
73-
var remainingSeats = Math.max(seats - EffectiveVaultAccess.countSeatOccupyingUsers(), 0);
72+
var licensedSeats = licenseHolder.getNoLicenseSeats();
73+
var usedSeats = EffectiveVaultAccess.countSeatOccupyingUsers();
7474
var managedInstance = licenseHolder.isManagedInstance();
75-
return new BillingDto(hubId, false, null, (int) seats, (int) remainingSeats, null, null, managedInstance);
75+
return new BillingDto(hubId, false, null, (int) licensedSeats, (int) usedSeats, null, null, managedInstance);
7676
}
7777

7878
public static BillingDto fromDecodedJwt(DecodedJWT jwt, LicenseHolder licenseHolder) {
7979
var id = jwt.getId();
8080
var email = jwt.getSubject();
81-
var totalSeats = jwt.getClaim("seats").asInt();
82-
var remainingSeats = Math.max(totalSeats - (int) EffectiveVaultAccess.countSeatOccupyingUsers(), 0);
81+
var licensedSeats = jwt.getClaim("seats").asInt();
82+
var usedSeats = (int) EffectiveVaultAccess.countSeatOccupyingUsers();
8383
var issuedAt = jwt.getIssuedAt().toInstant();
8484
var expiresAt = jwt.getExpiresAt().toInstant();
8585
var managedInstance = licenseHolder.isManagedInstance();
86-
return new BillingDto(id, true, email, totalSeats, remainingSeats, issuedAt, expiresAt, managedInstance);
86+
return new BillingDto(id, true, email, licensedSeats, usedSeats, issuedAt, expiresAt, managedInstance);
8787
}
8888

8989
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public ConfigDto getConfig() {
4949
var authUri = replacePrefix(oidcConfData.getAuthorizationUri(), trimTrailingSlash(internalRealmUrl), publicRealmUri);
5050
var tokenUri = replacePrefix(oidcConfData.getTokenUri(), trimTrailingSlash(internalRealmUrl), publicRealmUri);
5151

52-
return new ConfigDto(keycloakPublicUrl, keycloakRealm, keycloakClientIdHub, keycloakClientIdCryptomator, authUri, tokenUri, Instant.now().truncatedTo(ChronoUnit.MILLIS), 2);
52+
return new ConfigDto(keycloakPublicUrl, keycloakRealm, keycloakClientIdHub, keycloakClientIdCryptomator, authUri, tokenUri, Instant.now().truncatedTo(ChronoUnit.MILLIS), 3);
5353
}
5454

5555
//visible for testing

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

+12-4
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@
5959
import org.eclipse.microprofile.openapi.annotations.enums.ParameterIn;
6060
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
6161
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
62-
import org.hibernate.exception.ConstraintViolationException;
6362

6463
import java.net.URI;
6564
import java.time.Instant;
@@ -266,7 +265,7 @@ public Response legacyUnlock(@PathParam("vaultId") UUID vaultId, @PathParam("dev
266265
throw new GoneException("Vault is archived.");
267266
}
268267

269-
var usedSeats = EffectiveVaultAccess.countSeatOccupyingUsers();
268+
var usedSeats = EffectiveVaultAccess.countSeatOccupyingUsersWithAccessToken();
270269
if (usedSeats > license.getAvailableSeats()) {
271270
throw new PaymentRequiredException("Number of effective vault users exceeds available license seats");
272271
}
@@ -291,7 +290,7 @@ public Response legacyUnlock(@PathParam("vaultId") UUID vaultId, @PathParam("dev
291290
@Produces(MediaType.TEXT_PLAIN)
292291
@Operation(summary = "get the user-specific vault key", description = "retrieves a jwe containing the vault key, encrypted for the current user")
293292
@APIResponse(responseCode = "200")
294-
@APIResponse(responseCode = "402", description = "number of effective vault users exceeds available license seats")
293+
@APIResponse(responseCode = "402", description = "license expired or number of effective vault users that have a token exceeds available license seats")
295294
@APIResponse(responseCode = "403", description = "not a vault member")
296295
@APIResponse(responseCode = "404", description = "unknown vault")
297296
@APIResponse(responseCode = "410", description = "Vault is archived. Only returned if evenIfArchived query param is false or not set, otherwise the archived flag is ignored")
@@ -303,7 +302,7 @@ public Response unlock(@PathParam("vaultId") UUID vaultId, @QueryParam("evenIfAr
303302
throw new GoneException("Vault is archived.");
304303
}
305304

306-
var usedSeats = EffectiveVaultAccess.countSeatOccupyingUsers();
305+
var usedSeats = EffectiveVaultAccess.countSeatOccupyingUsersWithAccessToken();
307306
if (usedSeats > license.getAvailableSeats()) {
308307
throw new PaymentRequiredException("Number of effective vault users exceeds available license seats");
309308
}
@@ -335,6 +334,7 @@ public Response unlock(@PathParam("vaultId") UUID vaultId, @QueryParam("evenIfAr
335334
@Consumes(MediaType.APPLICATION_JSON)
336335
@Operation(summary = "adds user-specific vault keys", description = "Stores one or more user-vaultkey-tuples, as defined in the request body ({user1: token1, user2: token2, ...}).")
337336
@APIResponse(responseCode = "200", description = "all keys stored")
337+
@APIResponse(responseCode = "402", description = "number of users granted access exceeds available license seats")
338338
@APIResponse(responseCode = "403", description = "not a vault owner")
339339
@APIResponse(responseCode = "404", description = "at least one user has not been found")
340340
@APIResponse(responseCode = "410", description = "vault is archived")
@@ -344,6 +344,14 @@ public Response grantAccess(@PathParam("vaultId") UUID vaultId, @NotEmpty Map<St
344344
throw new GoneException("Vault is archived.");
345345
}
346346

347+
// check number of available seats
348+
long occupiedSeats = EffectiveVaultAccess.countSeatOccupyingUsers();
349+
long usersWithoutSeat = tokens.size() - EffectiveVaultAccess.countSeatsOccupiedByUsers(tokens.keySet().stream().toList());
350+
351+
if (occupiedSeats + usersWithoutSeat > license.getAvailableSeats()) {
352+
throw new PaymentRequiredException("Number of effective vault users greater than or equal to the available license seats");
353+
}
354+
347355
for (var entry : tokens.entrySet()) {
348356
var userId = entry.getKey();
349357
var token = AccessToken.<AccessToken>findById(new AccessToken.AccessId(userId, vaultId));

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

+29-5
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,41 @@
1414

1515
import java.io.Serializable;
1616
import java.util.Collection;
17+
import java.util.List;
1718
import java.util.Objects;
1819
import java.util.UUID;
1920
import java.util.stream.Collectors;
21+
import java.util.stream.Stream;
2022

2123
@Entity
2224
@Immutable
2325
@Table(name = "effective_vault_access")
2426
@NamedQuery(name = "EffectiveVaultAccess.countSeatsOccupiedByUser", query = """
25-
SELECT count(eva)
26-
FROM EffectiveVaultAccess eva
27-
INNER JOIN Vault v ON eva.id.vaultId = v.id AND NOT v.archived
27+
SELECT count(u)
28+
FROM User u
29+
INNER JOIN EffectiveVaultAccess eva ON u.id = eva.id.authorityId
2830
WHERE eva.id.authorityId = :userId
2931
""")
32+
@NamedQuery(name = "EffectiveVaultAccess.countSeatsOccupiedByUsers", query = """
33+
SELECT COUNT(DISTINCT u.id)
34+
FROM User u
35+
INNER JOIN EffectiveVaultAccess eva ON u.id = eva.id.authorityId
36+
INNER JOIN Vault v ON eva.id.vaultId = v.id AND NOT v.archived
37+
WHERE u.id IN :userIds
38+
""")
3039
@NamedQuery(name = "EffectiveVaultAccess.countSeatOccupyingUsers", query = """
3140
SELECT count(DISTINCT u)
3241
FROM User u
3342
INNER JOIN EffectiveVaultAccess eva ON u.id = eva.id.authorityId
3443
INNER JOIN Vault v ON eva.id.vaultId = v.id AND NOT v.archived
3544
""")
45+
@NamedQuery(name = "EffectiveVaultAccess.countSeatOccupyingUsersWithAccessToken", query = """
46+
SELECT count(DISTINCT u)
47+
FROM User u
48+
INNER JOIN EffectiveVaultAccess eva ON u.id = eva.id.authorityId
49+
INNER JOIN Vault v ON eva.id.vaultId = v.id AND NOT v.archived
50+
INNER JOIN AccessToken at ON eva.id.vaultId = at.id.vaultId AND eva.id.authorityId = at.id.userId
51+
""")
3652
@NamedQuery(name = "EffectiveVaultAccess.countSeatOccupyingUsersOfGroup", query = """
3753
SELECT count(DISTINCT u)
3854
FROM User u
@@ -41,7 +57,7 @@ SELECT count(DISTINCT u)
4157
INNER JOIN Vault v ON eva.id.vaultId = v.id AND NOT v.archived
4258
WHERE egm.id.groupId = :groupId
4359
""")
44-
@NamedQuery(name = "EffectiveVaultAccess.findByUserAndVault", query = """
60+
@NamedQuery(name = "EffectiveVaultAccess.findByAuthorityAndVault", query = """
4561
SELECT eva
4662
FROM EffectiveVaultAccess eva
4763
WHERE eva.id.vaultId = :vaultId AND eva.id.authorityId = :authorityId
@@ -55,16 +71,24 @@ public static boolean isUserOccupyingSeat(String userId) {
5571
return EffectiveVaultAccess.count("#EffectiveVaultAccess.countSeatsOccupiedByUser", Parameters.with("userId", userId)) > 0;
5672
}
5773

74+
public static long countSeatsOccupiedByUsers(List<String> userIds) {
75+
return EffectiveVaultAccess.count("#EffectiveVaultAccess.countSeatsOccupiedByUsers", Parameters.with("userIds", userIds));
76+
}
77+
5878
public static long countSeatOccupyingUsers() {
5979
return EffectiveVaultAccess.count("#EffectiveVaultAccess.countSeatOccupyingUsers");
6080
}
6181

82+
public static long countSeatOccupyingUsersWithAccessToken() {
83+
return EffectiveVaultAccess.count("#EffectiveVaultAccess.countSeatOccupyingUsersWithAccessToken");
84+
}
85+
6286
public static long countSeatOccupyingUsersOfGroup(String groupId) {
6387
return EffectiveVaultAccess.count("#EffectiveVaultAccess.countSeatOccupyingUsersOfGroup", Parameters.with("groupId", groupId));
6488
}
6589

6690
public static Collection<VaultAccess.Role> listRoles(UUID vaultId, String authorityId) {
67-
return EffectiveVaultAccess.<EffectiveVaultAccess>find("#EffectiveVaultAccess.findByUserAndVault", Parameters.with("vaultId", vaultId).and("authorityId", authorityId)).stream()
91+
return EffectiveVaultAccess.<EffectiveVaultAccess>find("#EffectiveVaultAccess.findByAuthorityAndVault", Parameters.with("vaultId", vaultId).and("authorityId", authorityId)).stream()
6892
.map(eva -> eva.id.role)
6993
.collect(Collectors.toUnmodifiableSet());
7094
}

backend/src/main/resources/dev-realm.json

+43
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,39 @@
6464
"admin"
6565
]
6666
},
67+
{
68+
"username": "alice",
69+
"enabled": true,
70+
"credentials": [{"type": "password", "value": "asd"}],
71+
"realmRoles": ["user"]
72+
},
73+
{
74+
"username": "bob",
75+
"enabled": true,
76+
"credentials": [{"type": "password", "value": "asd"}],
77+
"realmRoles": ["user"]
78+
},
79+
{
80+
"username": "carol",
81+
"enabled": true,
82+
"credentials": [{"type": "password", "value": "asd"}],
83+
"realmRoles": ["user"],
84+
"groups" : [ "/groupies" ]
85+
},
86+
{
87+
"username": "dave",
88+
"enabled": true,
89+
"credentials": [{"type": "password", "value": "asd"}],
90+
"realmRoles": ["user"],
91+
"groups" : [ "/groupies" ]
92+
},
93+
{
94+
"username": "erin",
95+
"enabled": true,
96+
"credentials": [{"type": "password", "value": "asd"}],
97+
"realmRoles": ["user"],
98+
"groups" : [ "/groupies" ]
99+
},
67100
{
68101
"username": "syncer",
69102
"email": "syncer@localhost",
@@ -94,6 +127,16 @@
94127
}
95128
}
96129
],
130+
"groups": [
131+
{
132+
"name": "groupies",
133+
"path": "/groupies",
134+
"subGroups": [],
135+
"attributes": {},
136+
"realmRoles": [],
137+
"clientRoles": {}
138+
}
139+
],
97140
"scopeMappings": [
98141
{
99142
"client": "cryptomatorhub",

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ public void testGetEmptyManagedInstance() throws SQLException {
6363
.body("hubId", is("42"))
6464
.body("hasLicense", is(false))
6565
.body("email", nullValue())
66-
.body("totalSeats", is(0))
67-
.body("remainingSeats", is(0))
66+
.body("licensedSeats", is(0))
67+
.body("usedSeats", is(2))
6868
.body("issuedAt", nullValue())
6969
.body("expiresAt", nullValue());
7070
}

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

+7-7
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ public void testGetEmptySelfHosted() {
6262
.body("hubId", is("42"))
6363
.body("hasLicense", is(false))
6464
.body("email", nullValue())
65-
.body("totalSeats", is(5)) //community license
66-
.body("remainingSeats", is(3)) //depends on the flyway test data migration
65+
.body("licensedSeats", is(5)) //community license
66+
.body("usedSeats", is(2)) //depends on the flyway test data migration
6767
.body("issuedAt", nullValue())
6868
.body("expiresAt", nullValue());
6969
}
@@ -86,8 +86,8 @@ public void testGetInitial() {
8686
.body("hubId", is("42"))
8787
.body("hasLicense", is(true))
8888
.body("email", is("[email protected]"))
89-
.body("totalSeats", is(5))
90-
.body("remainingSeats", is(3))
89+
.body("licensedSeats", is(5))
90+
.body("usedSeats", is(2))
9191
.body("issuedAt", is("2022-03-23T15:29:20Z"))
9292
.body("expiresAt", is("9999-12-31T00:00:00Z"));
9393
}
@@ -109,8 +109,8 @@ public void testGetUpdated() {
109109
.body("hubId", is("42"))
110110
.body("hasLicense", is(true))
111111
.body("email", is("[email protected]"))
112-
.body("totalSeats", is(5))
113-
.body("remainingSeats", is(3))
112+
.body("licensedSeats", is(5))
113+
.body("usedSeats", is(2))
114114
.body("issuedAt", is("2022-03-23T15:43:30Z"))
115115
.body("expiresAt", is("9999-12-31T00:00:00Z"));
116116
}
@@ -189,4 +189,4 @@ public void testPut() {
189189
}
190190

191191
}
192-
}
192+
}

0 commit comments

Comments
 (0)