Skip to content

Commit

Permalink
Merge branch 'release/1.3.3'
Browse files Browse the repository at this point in the history
  • Loading branch information
SailReal committed Feb 19, 2024
2 parents 84549cd + 371ba0c commit edfc62b
Show file tree
Hide file tree
Showing 19 changed files with 671 additions and 399 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[![Build and test](https://github.com/cryptomator/hub/actions/workflows/buildAndTest.yml/badge.svg)](https://github.com/cryptomator/hub/actions/workflows/buildAndTest.yml)
[![CI Build](https://github.com/cryptomator/hub/actions/workflows/build.yml/badge.svg)](https://github.com/cryptomator/hub/actions/workflows/build.yml)

# Cryptomator Hub

Expand Down
2 changes: 1 addition & 1 deletion 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.2</version>
<version>1.3.3</version>

<properties>
<compiler-plugin.version>3.11.0</compiler-plugin.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,25 +65,25 @@ public Response setToken(@NotNull @ValidJWS String token) {
}

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

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

public static BillingDto fromDecodedJwt(DecodedJWT jwt, LicenseHolder licenseHolder) {
var id = jwt.getId();
var email = jwt.getSubject();
var totalSeats = jwt.getClaim("seats").asInt();
var remainingSeats = Math.max(totalSeats - (int) EffectiveVaultAccess.countSeatOccupyingUsers(), 0);
var licensedSeats = jwt.getClaim("seats").asInt();
var usedSeats = (int) EffectiveVaultAccess.countSeatOccupyingUsers();
var issuedAt = jwt.getIssuedAt().toInstant();
var expiresAt = jwt.getExpiresAt().toInstant();
var managedInstance = licenseHolder.isManagedInstance();
return new BillingDto(id, true, email, totalSeats, remainingSeats, issuedAt, expiresAt, managedInstance);
return new BillingDto(id, true, email, licensedSeats, usedSeats, issuedAt, expiresAt, managedInstance);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public ConfigDto getConfig() {
var authUri = replacePrefix(oidcConfData.getAuthorizationUri(), trimTrailingSlash(internalRealmUrl), publicRealmUri);
var tokenUri = replacePrefix(oidcConfData.getTokenUri(), trimTrailingSlash(internalRealmUrl), publicRealmUri);

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

//visible for testing
Expand Down
16 changes: 12 additions & 4 deletions backend/src/main/java/org/cryptomator/hub/api/VaultResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@
import org.eclipse.microprofile.openapi.annotations.enums.ParameterIn;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.hibernate.exception.ConstraintViolationException;

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

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

var usedSeats = EffectiveVaultAccess.countSeatOccupyingUsers();
var usedSeats = EffectiveVaultAccess.countSeatOccupyingUsersWithAccessToken();
if (usedSeats > license.getAvailableSeats()) {
throw new PaymentRequiredException("Number of effective vault users exceeds available license seats");
}
Expand Down Expand Up @@ -335,6 +334,7 @@ public Response unlock(@PathParam("vaultId") UUID vaultId, @QueryParam("evenIfAr
@Consumes(MediaType.APPLICATION_JSON)
@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, ...}).")
@APIResponse(responseCode = "200", description = "all keys stored")
@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")
Expand All @@ -344,6 +344,14 @@ public Response grantAccess(@PathParam("vaultId") UUID vaultId, @NotEmpty Map<St
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()) {
throw new PaymentRequiredException("Number of effective vault users greater than or equal to the available license seats");
}

for (var entry : tokens.entrySet()) {
var userId = entry.getKey();
var token = AccessToken.<AccessToken>findById(new AccessToken.AccessId(userId, vaultId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,41 @@

import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Entity
@Immutable
@Table(name = "effective_vault_access")
@NamedQuery(name = "EffectiveVaultAccess.countSeatsOccupiedByUser", query = """
SELECT count(eva)
FROM EffectiveVaultAccess eva
INNER JOIN Vault v ON eva.id.vaultId = v.id AND NOT v.archived
SELECT count(u)
FROM User u
INNER JOIN EffectiveVaultAccess eva ON u.id = eva.id.authorityId
WHERE eva.id.authorityId = :userId
""")
@NamedQuery(name = "EffectiveVaultAccess.countSeatsOccupiedByUsers", query = """
SELECT COUNT(DISTINCT u.id)
FROM User u
INNER JOIN EffectiveVaultAccess eva ON u.id = eva.id.authorityId
INNER JOIN Vault v ON eva.id.vaultId = v.id AND NOT v.archived
WHERE u.id IN :userIds
""")
@NamedQuery(name = "EffectiveVaultAccess.countSeatOccupyingUsers", query = """
SELECT count(DISTINCT u)
FROM User u
INNER JOIN EffectiveVaultAccess eva ON u.id = eva.id.authorityId
INNER JOIN Vault v ON eva.id.vaultId = v.id AND NOT v.archived
""")
@NamedQuery(name = "EffectiveVaultAccess.countSeatOccupyingUsersWithAccessToken", query = """
SELECT count(DISTINCT u)
FROM User u
INNER JOIN EffectiveVaultAccess eva ON u.id = eva.id.authorityId
INNER JOIN Vault v ON eva.id.vaultId = v.id AND NOT v.archived
INNER JOIN AccessToken at ON eva.id.vaultId = at.id.vaultId AND eva.id.authorityId = at.id.userId
""")
@NamedQuery(name = "EffectiveVaultAccess.countSeatOccupyingUsersOfGroup", query = """
SELECT count(DISTINCT u)
FROM User u
Expand All @@ -41,7 +57,7 @@ SELECT count(DISTINCT u)
INNER JOIN Vault v ON eva.id.vaultId = v.id AND NOT v.archived
WHERE egm.id.groupId = :groupId
""")
@NamedQuery(name = "EffectiveVaultAccess.findByUserAndVault", query = """
@NamedQuery(name = "EffectiveVaultAccess.findByAuthorityAndVault", query = """
SELECT eva
FROM EffectiveVaultAccess eva
WHERE eva.id.vaultId = :vaultId AND eva.id.authorityId = :authorityId
Expand All @@ -55,16 +71,24 @@ public static boolean isUserOccupyingSeat(String userId) {
return EffectiveVaultAccess.count("#EffectiveVaultAccess.countSeatsOccupiedByUser", Parameters.with("userId", userId)) > 0;
}

public static long countSeatsOccupiedByUsers(List<String> userIds) {
return EffectiveVaultAccess.count("#EffectiveVaultAccess.countSeatsOccupiedByUsers", Parameters.with("userIds", userIds));
}

public static long countSeatOccupyingUsers() {
return EffectiveVaultAccess.count("#EffectiveVaultAccess.countSeatOccupyingUsers");
}

public static long countSeatOccupyingUsersWithAccessToken() {
return EffectiveVaultAccess.count("#EffectiveVaultAccess.countSeatOccupyingUsersWithAccessToken");
}

public static long countSeatOccupyingUsersOfGroup(String groupId) {
return EffectiveVaultAccess.count("#EffectiveVaultAccess.countSeatOccupyingUsersOfGroup", Parameters.with("groupId", groupId));
}

public static Collection<VaultAccess.Role> listRoles(UUID vaultId, String authorityId) {
return EffectiveVaultAccess.<EffectiveVaultAccess>find("#EffectiveVaultAccess.findByUserAndVault", Parameters.with("vaultId", vaultId).and("authorityId", authorityId)).stream()
return EffectiveVaultAccess.<EffectiveVaultAccess>find("#EffectiveVaultAccess.findByAuthorityAndVault", Parameters.with("vaultId", vaultId).and("authorityId", authorityId)).stream()
.map(eva -> eva.id.role)
.collect(Collectors.toUnmodifiableSet());
}
Expand Down
43 changes: 43 additions & 0 deletions backend/src/main/resources/dev-realm.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,39 @@
"admin"
]
},
{
"username": "alice",
"enabled": true,
"credentials": [{"type": "password", "value": "asd"}],
"realmRoles": ["user"]
},
{
"username": "bob",
"enabled": true,
"credentials": [{"type": "password", "value": "asd"}],
"realmRoles": ["user"]
},
{
"username": "carol",
"enabled": true,
"credentials": [{"type": "password", "value": "asd"}],
"realmRoles": ["user"],
"groups" : [ "/groupies" ]
},
{
"username": "dave",
"enabled": true,
"credentials": [{"type": "password", "value": "asd"}],
"realmRoles": ["user"],
"groups" : [ "/groupies" ]
},
{
"username": "erin",
"enabled": true,
"credentials": [{"type": "password", "value": "asd"}],
"realmRoles": ["user"],
"groups" : [ "/groupies" ]
},
{
"username": "syncer",
"email": "syncer@localhost",
Expand Down Expand Up @@ -94,6 +127,16 @@
}
}
],
"groups": [
{
"name": "groupies",
"path": "/groupies",
"subGroups": [],
"attributes": {},
"realmRoles": [],
"clientRoles": {}
}
],
"scopeMappings": [
{
"client": "cryptomatorhub",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ public void testGetEmptyManagedInstance() throws SQLException {
.body("hubId", is("42"))
.body("hasLicense", is(false))
.body("email", nullValue())
.body("totalSeats", is(0))
.body("remainingSeats", is(0))
.body("licensedSeats", is(0))
.body("usedSeats", is(2))
.body("issuedAt", nullValue())
.body("expiresAt", nullValue());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ public void testGetEmptySelfHosted() {
.body("hubId", is("42"))
.body("hasLicense", is(false))
.body("email", nullValue())
.body("totalSeats", is(5)) //community license
.body("remainingSeats", is(3)) //depends on the flyway test data migration
.body("licensedSeats", is(5)) //community license
.body("usedSeats", is(2)) //depends on the flyway test data migration
.body("issuedAt", nullValue())
.body("expiresAt", nullValue());
}
Expand All @@ -86,8 +86,8 @@ public void testGetInitial() {
.body("hubId", is("42"))
.body("hasLicense", is(true))
.body("email", is("[email protected]"))
.body("totalSeats", is(5))
.body("remainingSeats", is(3))
.body("licensedSeats", is(5))
.body("usedSeats", is(2))
.body("issuedAt", is("2022-03-23T15:29:20Z"))
.body("expiresAt", is("9999-12-31T00:00:00Z"));
}
Expand All @@ -109,8 +109,8 @@ public void testGetUpdated() {
.body("hubId", is("42"))
.body("hasLicense", is(true))
.body("email", is("[email protected]"))
.body("totalSeats", is(5))
.body("remainingSeats", is(3))
.body("licensedSeats", is(5))
.body("usedSeats", is(2))
.body("issuedAt", is("2022-03-23T15:43:30Z"))
.body("expiresAt", is("9999-12-31T00:00:00Z"));
}
Expand Down Expand Up @@ -189,4 +189,4 @@ public void testPut() {
}

}
}
}
Loading

0 comments on commit edfc62b

Please sign in to comment.