Skip to content

Commit

Permalink
Merge branch 'release/1.3.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
SailReal committed Nov 30, 2023
2 parents 568103d + f8b1e10 commit 05c6b42
Show file tree
Hide file tree
Showing 117 changed files with 7,469 additions and 3,637 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.2.2</version>
<version>1.3.0</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.6.Final</quarkus.platform.version>
<quarkus.platform.version>3.4.3</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 @@ -44,9 +44,24 @@ List<User> users(RealmResource realm) {
users.addAll(currentRequestedUsers);
} while (currentRequestedUsers.size() == MAX_COUNT_PER_REQUEST);

var cliUser = cryptomatorCliUser(realm);
cliUser.ifPresent(users::add);

return users;
}

//visible for testing
Optional<User> cryptomatorCliUser(RealmResource realm) {
var clients = realm.clients().findByClientId("cryptomatorhub-cli");
if (clients.isEmpty()) {
return Optional.empty();
}
var clientId = clients.get(0).getId();
var client = realm.clients().get(clientId);
var clientUser = client.getServiceAccountUser();
return Optional.of(mapToUser(clientUser));
}

private Predicate<UserRepresentation> notSyncerUser() {
return user -> !user.getUsername().equals(syncerConfig.getUsername());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.cryptomator.hub.api;

import jakarta.ws.rs.ClientErrorException;

class ActionRequiredException extends ClientErrorException {
public static final int STATUS = 449;

public ActionRequiredException() {
super(STATUS);
}

public ActionRequiredException(String message) {
super(message, STATUS);
}

public ActionRequiredException(Throwable cause) {
super(STATUS, cause);
}

public ActionRequiredException(String msg, Throwable cause) {
super(msg, STATUS, cause);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.BadRequestException;
Expand All @@ -21,8 +20,11 @@
import org.cryptomator.hub.entities.AuditEventVaultKeyRetrieve;
import org.cryptomator.hub.entities.AuditEventVaultMemberAdd;
import org.cryptomator.hub.entities.AuditEventVaultMemberRemove;
import org.cryptomator.hub.entities.AuditEventVaultMemberUpdate;
import org.cryptomator.hub.entities.AuditEventVaultOwnershipClaim;
import org.cryptomator.hub.entities.AuditEventVaultUpdate;
import org.cryptomator.hub.entities.Device;
import org.cryptomator.hub.entities.VaultAccess;
import org.cryptomator.hub.license.LicenseHolder;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.enums.ParameterIn;
Expand All @@ -34,7 +36,6 @@
import java.util.UUID;

@Path("/auditlog")
@RegisterForReflection(targets = {UUID[].class})
public class AuditLogResource {

@Inject
Expand Down Expand Up @@ -82,7 +83,9 @@ public List<AuditEventDto> getAllEvents(@QueryParam("startDate") Instant startDa
@JsonSubTypes.Type(value = AuditEventVaultAccessGrantDto.class, name = AuditEventVaultAccessGrant.TYPE), //
@JsonSubTypes.Type(value = AuditEventVaultKeyRetrieveDto.class, name = AuditEventVaultKeyRetrieve.TYPE), //
@JsonSubTypes.Type(value = AuditEventVaultMemberAddDto.class, name = AuditEventVaultMemberAdd.TYPE), //
@JsonSubTypes.Type(value = AuditEventVaultMemberRemoveDto.class, name = AuditEventVaultMemberRemove.TYPE) //
@JsonSubTypes.Type(value = AuditEventVaultMemberRemoveDto.class, name = AuditEventVaultMemberRemove.TYPE), //
@JsonSubTypes.Type(value = AuditEventVaultMemberUpdateDto.class, name = AuditEventVaultMemberUpdate.TYPE), //
@JsonSubTypes.Type(value = AuditEventVaultOwnershipClaimDto.class, name = AuditEventVaultOwnershipClaim.TYPE) //
})
public interface AuditEventDto {

Expand All @@ -107,9 +110,13 @@ static AuditEventDto fromEntity(AuditEvent entity) {
} else if (entity instanceof AuditEventVaultKeyRetrieve evt) {
return new AuditEventVaultKeyRetrieveDto(evt.id, evt.timestamp, AuditEventVaultKeyRetrieve.TYPE, evt.retrievedBy, evt.vaultId, evt.result);
} else if (entity instanceof AuditEventVaultMemberAdd evt) {
return new AuditEventVaultMemberAddDto(evt.id, evt.timestamp, AuditEventVaultMemberAdd.TYPE, evt.addedBy, evt.vaultId, evt.authorityId);
return new AuditEventVaultMemberAddDto(evt.id, evt.timestamp, AuditEventVaultMemberAdd.TYPE, evt.addedBy, evt.vaultId, evt.authorityId, evt.role);
} else if (entity instanceof AuditEventVaultMemberRemove evt) {
return new AuditEventVaultMemberRemoveDto(evt.id, evt.timestamp, AuditEventVaultMemberRemove.TYPE, evt.removedBy, evt.vaultId, evt.authorityId);
} else if (entity instanceof AuditEventVaultMemberUpdate evt) {
return new AuditEventVaultMemberUpdateDto(evt.id, evt.timestamp, AuditEventVaultMemberUpdate.TYPE, evt.updatedBy, evt.vaultId, evt.authorityId, evt.role);
} else if (entity instanceof AuditEventVaultOwnershipClaim evt) {
return new AuditEventVaultOwnershipClaimDto(evt.id, evt.timestamp, AuditEventVaultOwnershipClaim.TYPE, evt.claimedBy, evt.vaultId);
} else {
throw new UnsupportedOperationException("conversion not implemented for event type " + entity.getClass());
}
Expand Down Expand Up @@ -139,12 +146,19 @@ record AuditEventVaultKeyRetrieveDto(long id, Instant timestamp, String type, @J
@JsonProperty("result") AuditEventVaultKeyRetrieve.Result result) implements AuditEventDto {
}

record AuditEventVaultMemberAddDto(long id, Instant timestamp, String type, @JsonProperty("addedBy") String addedBy, @JsonProperty("vaultId") UUID vaultId,
@JsonProperty("authorityId") String authorityId) implements AuditEventDto {
record AuditEventVaultMemberAddDto(long id, Instant timestamp, String type, @JsonProperty("addedBy") String addedBy, @JsonProperty("vaultId") UUID vaultId, @JsonProperty("authorityId") String authorityId,
@JsonProperty("role") VaultAccess.Role role) implements AuditEventDto {
}

record AuditEventVaultMemberRemoveDto(long id, Instant timestamp, String type, @JsonProperty("removedBy") String removedBy, @JsonProperty("vaultId") UUID vaultId,
@JsonProperty("authorityId") String authorityId) implements AuditEventDto {
}

record AuditEventVaultMemberUpdateDto(long id, Instant timestamp, String type, @JsonProperty("updatedBy") String updatedBy, @JsonProperty("vaultId") UUID vaultId, @JsonProperty("authorityId") String authorityId,
@JsonProperty("role") VaultAccess.Role role) implements AuditEventDto {
}

record AuditEventVaultOwnershipClaimDto(long id, Instant timestamp, String type, @JsonProperty("claimedBy") String claimedBy, @JsonProperty("vaultId") UUID vaultId) implements AuditEventDto {
}

}
13 changes: 7 additions & 6 deletions backend/src/main/java/org/cryptomator/hub/api/AuthorityDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import org.cryptomator.hub.entities.Group;
import org.cryptomator.hub.entities.User;

abstract sealed class AuthorityDto permits UserDto, GroupDto {
abstract sealed class AuthorityDto permits UserDto, GroupDto, MemberDto {

public enum Type {
USER, GROUP
Expand All @@ -31,12 +31,13 @@ protected AuthorityDto(String id, Type type, String name, String pictureUrl) {
}

static AuthorityDto fromEntity(Authority a) {
if (a instanceof User u) {
return new UserDto(u.id, u.name, u.pictureUrl, u.email, null);
} else if (a instanceof Group) {
return new GroupDto(a.id, a.name);
// TODO refactor to JEP 441 in JDK 21
if (a instanceof User user) {
return UserDto.justPublicInfo(user);
} else if (a instanceof Group group) {
return GroupDto.fromEntity(group);
} else {
throw new IllegalArgumentException("Authority of this type does not exist");
throw new IllegalStateException("authority is not of type user or group");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ public List<AuthorityDto> search(@QueryParam("query") @NotBlank String query) {

@GET
@Path("/")
@RolesAllowed("admin")
@RolesAllowed("user")
@Produces(MediaType.APPLICATION_JSON)
@NoCache
@Operation(summary = "lists all authorities matching the given ids", description ="lists for each id in the list its corresponding authority. Ignores all id's where an authority cannot be found")
@Operation(summary = "lists all authorities matching the given ids", description = "lists for each id in the list its corresponding authority. Ignores all id's where an authority cannot be found")
@APIResponse(responseCode = "200")
public List<AuthorityDto> getSome(@QueryParam("ids") List<String> authorityIds) {
return Authority.findAllInList(authorityIds).map(AuthorityDto::fromEntity).toList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PUT;
Expand Down Expand Up @@ -51,10 +52,10 @@ public BillingDto get() {
@RolesAllowed("admin")
@Consumes(MediaType.TEXT_PLAIN)
@Operation(summary = "set the token")
@APIResponse(responseCode = "204")
@APIResponse(responseCode = "204", description = "token set")
@APIResponse(responseCode = "400", description = "token is invalid (e.g., expired or invalid signature)")
@APIResponse(responseCode = "403", description = "only admins are allowed to set the token")
public Response setToken(@ValidJWS String token) {
public Response setToken(@NotNull @ValidJWS String token) {
try {
licenseHolder.set(token);
return Response.status(Response.Status.NO_CONTENT).build();
Expand All @@ -69,7 +70,7 @@ public record BillingDto(@JsonProperty("hubId") String hubId, @JsonProperty("has

public static BillingDto create(String hubId, LicenseHolder licenseHolder) {
var seats = licenseHolder.getNoLicenseSeats();
var remainingSeats = Math.max(seats - EffectiveVaultAccess.countEffectiveVaultUsers(), 0);
var remainingSeats = Math.max(seats - EffectiveVaultAccess.countSeatOccupyingUsers(), 0);
var managedInstance = licenseHolder.isManagedInstance();
return new BillingDto(hubId, false, null, (int) seats, (int) remainingSeats, null, null, managedInstance);
}
Expand All @@ -78,7 +79,7 @@ public static BillingDto fromDecodedJwt(DecodedJWT jwt, LicenseHolder licenseHol
var id = jwt.getId();
var email = jwt.getSubject();
var totalSeats = jwt.getClaim("seats").asInt();
var remainingSeats = Math.max(totalSeats - (int) EffectiveVaultAccess.countEffectiveVaultUsers(), 0);
var remainingSeats = Math.max(totalSeats - (int) EffectiveVaultAccess.countSeatOccupyingUsers(), 0);
var issuedAt = jwt.getIssuedAt().toInstant();
var expiresAt = jwt.getExpiresAt().toInstant();
var managedInstance = licenseHolder.isManagedInstance();
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), 0);
return new ConfigDto(keycloakPublicUrl, keycloakRealm, keycloakClientIdHub, keycloakClientIdCryptomator, authUri, tokenUri, Instant.now().truncatedTo(ChronoUnit.MILLIS), 1);
}

//visible for testing
Expand Down
90 changes: 57 additions & 33 deletions backend/src/main/java/org/cryptomator/hub/api/DeviceResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,50 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.persistence.PersistenceException;
import jakarta.persistence.NoResultException;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.ClientErrorException;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.InternalServerErrorException;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.cryptomator.hub.entities.Device;
import org.cryptomator.hub.entities.AuditEventDeviceRegister;
import org.cryptomator.hub.entities.AuditEventDeviceRemove;
import org.cryptomator.hub.entities.AuditEventVaultAccessGrant;
import org.cryptomator.hub.entities.Device;
import org.cryptomator.hub.entities.LegacyDevice;
import org.cryptomator.hub.entities.User;
import org.cryptomator.hub.validation.NoHtmlOrScriptChars;
import org.cryptomator.hub.validation.OnlyBase64UrlChars;
import org.cryptomator.hub.validation.OnlyBase64Chars;
import org.cryptomator.hub.validation.ValidId;
import org.cryptomator.hub.validation.ValidJWE;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.hibernate.exception.ConstraintViolationException;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.NoCache;

import java.net.URI;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Set;

@Path("/devices")
public class DeviceResource {

private static final Logger LOG = Logger.getLogger(DeviceResource.class);

@Inject
JsonWebToken jwt;

Expand All @@ -60,25 +67,53 @@ public List<DeviceDto> getSome(@QueryParam("ids") List<String> deviceIds) {
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Transactional
@Operation(summary = "adds a device", description = "the device will be owned by the currently logged-in user")
@APIResponse(responseCode = "201", description = "device created")
@APIResponse(responseCode = "409", description = "Device already exists")
public Response create(@Valid DeviceDto deviceDto, @PathParam("deviceId") @ValidId String deviceId) {
if (deviceId == null || deviceId.trim().length() == 0 || deviceDto == null) {
return Response.status(Response.Status.BAD_REQUEST).entity("deviceId or deviceDto cannot be empty").build();
@Operation(summary = "creates or updates a device", description = "the device will be owned by the currently logged-in user")
@APIResponse(responseCode = "201", description = "Device created or updated")
@APIResponse(responseCode = "409", description = "Device with this key already exists")
public Response createOrUpdate(@Valid @NotNull DeviceDto dto, @PathParam("deviceId") @ValidId String deviceId) {
Device device;
try {
device = Device.findByIdAndUser(deviceId, jwt.getSubject());
} catch (NoResultException e) {
device = new Device();
device.id = deviceId;
device.owner = User.findById(jwt.getSubject());
device.creationTime = Instant.now().truncatedTo(ChronoUnit.MILLIS);
device.type = dto.type != null ? dto.type : Device.Type.DESKTOP; // default to desktop for backwards compatibility

if (LegacyDevice.deleteById(device.id)) {
assert LegacyDevice.findById(device.id) == null;
LOG.info("Deleted Legacy Device during re-registration of Device " + deviceId);
}
}
User currentUser = User.findById(jwt.getSubject());
var device = deviceDto.toDevice(currentUser, deviceId, Instant.now().truncatedTo(ChronoUnit.MILLIS));
device.name = dto.name;
device.publickey = dto.publicKey;
device.userPrivateKey = dto.userPrivateKey;

try {
device.persistAndFlush();
AuditEventDeviceRegister.log(jwt.getSubject(), deviceId, device.name, device.type);
return Response.created(URI.create(".")).build();
} catch (PersistenceException e) {
if (e instanceof ConstraintViolationException) {
throw new ClientErrorException(Response.Status.CONFLICT, e);
} else {
throw new InternalServerErrorException(e);
}
} catch (ConstraintViolationException e) {
throw new ClientErrorException(Response.Status.CONFLICT, e);
}
}

@GET
@Path("/{deviceId}")
@RolesAllowed("user")
@Produces(MediaType.APPLICATION_JSON)
@NoCache
@Transactional
@Operation(summary = "get the device", description = "the device must be owned by the currently logged-in user")
@APIResponse(responseCode = "200", description = "Device found")
@APIResponse(responseCode = "404", description = "Device not found or owned by a different user")
public DeviceDto get(@PathParam("deviceId") @ValidId String deviceId) {
try {
Device device = Device.findByIdAndUser(deviceId, jwt.getSubject());
return DeviceDto.fromEntity(device);
} catch (NoResultException e) {
throw new NotFoundException(e);
}
}

Expand Down Expand Up @@ -109,24 +144,13 @@ public Response remove(@PathParam("deviceId") @ValidId String deviceId) {
public record DeviceDto(@JsonProperty("id") @ValidId String id,
@JsonProperty("name") @NoHtmlOrScriptChars @NotBlank String name,
@JsonProperty("type") Device.Type type,
@JsonProperty("publicKey") @OnlyBase64UrlChars String publicKey,
@JsonProperty("publicKey") @NotNull @OnlyBase64Chars String publicKey,
@JsonProperty("userPrivateKey") @NotNull @ValidJWE String userPrivateKey,
@JsonProperty("owner") @ValidId String ownerId,
@JsonProperty("accessTo") @Valid Set<VaultResource.VaultDto> accessTo,
@JsonProperty("creationTime") Instant creationTime) {

public Device toDevice(User user, String id, Instant creationTime) {
var device = new Device();
device.id = id;
device.owner = user;
device.name = name;
device.type = type != null ? type : Device.Type.DESKTOP; // default to desktop for backwards compatibility
device.publickey = publicKey;
device.creationTime = creationTime;
return device;
}

public static DeviceDto fromEntity(Device entity) {
return new DeviceDto(entity.id, entity.name, entity.type, entity.publickey, entity.owner.id, Set.of(), entity.creationTime.truncatedTo(ChronoUnit.MILLIS));
return new DeviceDto(entity.id, entity.name, entity.type, entity.publickey, entity.userPrivateKey, entity.owner.id, entity.creationTime.truncatedTo(ChronoUnit.MILLIS));
}

}
Expand Down
Loading

0 comments on commit 05c6b42

Please sign in to comment.