Skip to content

Commit

Permalink
Implement sharing vaults with groups and unsharing with users/groups;…
Browse files Browse the repository at this point in the history
… token-exchange into separate client (#10 #41).
  • Loading branch information
chenkins committed Jan 10, 2024
1 parent 049a54b commit 363ae87
Show file tree
Hide file tree
Showing 13 changed files with 600 additions and 76 deletions.
9 changes: 4 additions & 5 deletions backend/config/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,10 @@ backends.backends[0].region=eu-central-1
backends.backends[0].regions=eu-west-1,eu-west-2,eu-west-3,eu-north-1,eu-south-1,eu-south-2,eu-central-1,eu-central-2
backends.backends[0].with-path-style-access-enabled=true

# TODO we currently use same client for hub and for Cipherduck desktop client in staging and testing Keycloak - distinguish again?
# role for cryptomatorhub client
#backends.backends[0].sts-role-arn=arn:minio:iam:::role/HGKdlY4eFFsXVvJmwlMYMhmbnDE
# role for cryptomator client
backends.backends[0].sts-role-arn=arn:minio:iam:::role/IqZpDC5ahW_DCAvZPZA4ACjEnDE
backends.backends[0].sts-role-arn=arn:minio:iam:::role/HGKdlY4eFFsXVvJmwlMYMhmbnDE
# role for cryptomator client (TODO: https://github.com/chenkins/cipherduck-hub/issues/12 required for client-side vault creation)
#backends.backends[0].sts-role-arn=arn:minio:iam:::role/IqZpDC5ahW_DCAvZPZA4ACjEnDE
backends.backends[0].sts-endpoint=http://minio:9000
#
# (1) protocol
Expand Down Expand Up @@ -93,7 +92,7 @@ backends.backends[2].jwe.vendor=s3
backends.backends[2].jwe.hostname=minio
backends.backends[2].jwe.port=9000
backends.backends[2].jwe.scheme=http
# TODO https://github.com/chenkins/cipherduck-hub/issues/28 extract profile client-side?
# TODO https://github.com/chenkins/cipherduck-hub/issues/28 extract profile client-side instead of cluttering application.properties here.
backends.backends[2].jwe.username-configurable=true
backends.backends[2].jwe.password-configurable=true
backends.backends[2].jwe.token-configurable=false
22 changes: 15 additions & 7 deletions backend/src/main/java/org/cryptomator/hub/api/VaultResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
import java.util.stream.Stream;

import static org.cryptomator.hub.cipherduck.KeycloakGrantAccessToVault.keycloakGrantAccessToVault;
import static org.cryptomator.hub.cipherduck.KeycloakGrantAccessToVault.keycloakRemoveAccessToVault;

@Path("/vaults")
public class VaultResource {
Expand Down Expand Up @@ -186,8 +187,8 @@ public Response addUser(@PathParam("vaultId") UUID vaultId, @PathParam("userId")
}

// / start cipherduck extension
// TODO https://github.com/chenkins/cipherduck-hub/issues/10 delete?
keycloakGrantAccessToVault(syncerConfig, vaultId.toString(), userId, List.of(keycloakClientIdHub, keycloakClientIdCryptomator));

keycloakGrantAccessToVault(syncerConfig, vaultId.toString(), userId, "cryptomatorvaults");
// \ end cipherduck extension

return addAuthority(vault, user, role);
Expand Down Expand Up @@ -216,6 +217,10 @@ public Response addGroup(@PathParam("vaultId") UUID vaultId, @PathParam("groupId
throw new PaymentRequiredException("Number of effective vault users greater than or equal to the available license seats");
}

// / start cipherduck extension
keycloakGrantAccessToVault(syncerConfig, vaultId.toString(), groupId, "cryptomatorvaults");
// \ end cipherduck extension

return addAuthority(vault, group, role);
}

Expand Down Expand Up @@ -250,6 +255,9 @@ private Response addAuthority(Vault vault, Authority authority, VaultAccess.Role
@APIResponse(responseCode = "403", description = "not a vault owner")
public Response removeAuthority(@PathParam("vaultId") UUID vaultId, @PathParam("authorityId") @ValidId String authorityId) {
if (VaultAccess.deleteById(new VaultAccess.Id(vaultId, authorityId))) {
// / start cipherduck extension
keycloakRemoveAccessToVault(syncerConfig, vaultId.toString(), authorityId, "cryptomatorvaults");
// \ end cipherduck extension
AuditEventVaultMemberRemove.log(jwt.getSubject(), vaultId, authorityId);
return Response.status(Response.Status.NO_CONTENT).build();
} else {
Expand Down Expand Up @@ -375,11 +383,6 @@ public Response grantAccess(@PathParam("vaultId") UUID vaultId, @NotEmpty Map<St
token.vaultKey = entry.getValue();
token.persist();

// / start cipherduck extension
// TODO https://github.com/chenkins/cipherduck-hub/issues/10 check remove upon DELETE operations?
keycloakGrantAccessToVault(syncerConfig, vaultId.toString(), userId, List.of(keycloakClientIdHub, keycloakClientIdCryptomator));
// \ end cipherduck extension

AuditEventVaultAccessGrant.log(jwt.getSubject(), vaultId, userId);
}
return Response.ok().build();
Expand Down Expand Up @@ -448,6 +451,11 @@ public Response createOrUpdate(@PathParam("vaultId") UUID vaultId, @Valid @NotNu
access.persist();
AuditEventVaultCreate.log(currentUser.id, vault.id, vault.name, vault.description);
AuditEventVaultMemberAdd.log(currentUser.id, vaultId, currentUser.id, VaultAccess.Role.OWNER);

// / start cipherduck extension
keycloakGrantAccessToVault(syncerConfig, vaultId.toString(), currentUser.id, "cryptomatorvaults");
// \ end cipherduck extension

return Response.created(URI.create(".")).contentLocation(URI.create(".")).entity(VaultDto.fromEntity(vault)).type(MediaType.APPLICATION_JSON).build();
} else {
AuditEventVaultUpdate.log(currentUser.id, vault.id, vault.name, vault.description, vault.archived);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ public class BackendsConfigResource {
@Inject
BackendsConfig backendsConfig;

@Inject
CipherduckConfig cipherduckConfig;


@GET
@Path("/")
Expand All @@ -29,8 +32,10 @@ public class BackendsConfigResource {
@Operation(summary = "get configs for storage backends", description = "get list of configs for storage backends")
@APIResponse(responseCode = "200", description = "uploaded storage configuration")
public BackendsConfigDto getBackendsConfig() {
// workaround for defaultValue not working as expected
return new BackendsConfigDto(Settings.get().hubId, backendsConfig.backends().stream().map(b -> new StorageConfigDto(b)).collect(Collectors.toList()));
return new BackendsConfigDto(Settings.get().hubId, backendsConfig.backends().stream()
// TODO https://github.com/chenkins/cipherduck-hub/issues/41 hard-coded cryptomatorvaults
.map(b -> new StorageConfigDto(b, new VaultJWEBackendDto(b.jwe(), cipherduckConfig.authEndpoint(), cipherduckConfig.tokenEndpoint(), cipherduckConfig.keycloakClientIdCryptomator(), "cryptomatorvaults")))
.collect(Collectors.toList()));
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.cryptomator.hub.api.cipherduck;

import io.quarkus.oidc.OidcConfigurationMetadata;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty;

@ApplicationScoped
public class CipherduckConfig {
@Inject
@ConfigProperty(name = "hub.keycloak.public-url", defaultValue = "")
String keycloakPublicUrl;

@Inject
@ConfigProperty(name = "hub.keycloak.realm", defaultValue = "")
String keycloakRealm;



@Inject
@ConfigProperty(name = "quarkus.oidc.client-id", defaultValue = "")
String keycloakClientIdHub;

@Inject
@ConfigProperty(name = "hub.keycloak.oidc.cryptomator-client-id", defaultValue = "")
String keycloakClientIdCryptomator;

@Inject
@ConfigProperty(name = "quarkus.oidc.auth-server-url")
String internalRealmUrl;

@Inject
OidcConfigurationMetadata oidcConfData;

String replacePrefix(String str, String prefix, String replacement) {
int index = str.indexOf(prefix);
if (index == 0) {
return replacement + str.substring(prefix.length());
} else {
return str;
}
}

String trimTrailingSlash(String str) {
if (str.endsWith("/")) {
return str.substring(0, str.length() - 1);
} else {
return str;
}

}
public String keycloakClientIdHub() {
return keycloakClientIdHub;
}

public String keycloakClientIdCryptomator() {
return keycloakClientIdCryptomator;
}
public String publicRealmUri() {
return trimTrailingSlash(keycloakPublicUrl + "/realms/" + keycloakRealm);
}

public String authEndpoint() {
return replacePrefix(oidcConfData.getAuthorizationUri(), trimTrailingSlash(internalRealmUrl), publicRealmUri());
}

public String tokenEndpoint() {
return replacePrefix(oidcConfData.getTokenUri(), trimTrailingSlash(internalRealmUrl), publicRealmUri());
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public record StorageConfigDto(
Optional<Boolean> withPathStyleAccessEnabled

) implements StorageConfig {
public StorageConfigDto(StorageConfig s) {
public StorageConfigDto(final StorageConfig s, final VaultJWEBackend jwe) {
// workaround for defaultValue in JSONPrroperty not working as expected
this(s.id(), s.name(), s.bucketPrefix(), s.stsRoleArn(), s.stsEndpoint(),
s.region().isPresent() ? s.region() : Optional.of("us-east-1"),
Expand Down Expand Up @@ -50,8 +50,7 @@ public StorageConfigDto(StorageConfig s) {
"us-west-1",
"us-west-2"
)),
s.jwe(), s.withPathStyleAccessEnabled());


jwe, s.withPathStyleAccessEnabled());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,26 @@
import jakarta.transaction.Transactional;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.cryptomator.hub.SyncerConfig;
import org.cryptomator.hub.entities.VaultAccess;
import org.cryptomator.hub.filters.VaultRole;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.jboss.logging.Logger;

import java.net.URI;
import java.util.Map;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;

import static org.cryptomator.hub.api.cipherduck.storage.S3Storage.makeS3Bucket;
import static org.cryptomator.hub.cipherduck.KeycloakGrantAccessToVault.keycloakPrepareVault;

@Path("/storage")
public class StorageResource {
Expand All @@ -26,21 +33,34 @@ public class StorageResource {
@Inject
BackendsConfig backendsConfig;

@Inject
SyncerConfig syncerConfig;

@Inject
JsonWebToken jwt;


@PUT
@Path("/")
@Path("/{vaultId}")
@RolesAllowed("user")
@Produces(MediaType.APPLICATION_JSON)
@VaultRole(VaultAccess.Role.OWNER) // may throw 403
@Transactional
@Operation(summary = "creates bucket and policy", description = "creates an S3 bucket and uploads policy for it.")
@APIResponse(responseCode = "200", description = "uploaded storage configuration")
@APIResponse(responseCode = "400", description = "Could not create bucket")
public Response createBucket(StorageDto dto) {
public Response createBucket(@PathParam("vaultId") UUID vaultId, StorageDto dto) {

// TODO https://github.com/chenkins/cipherduck-hub/issues/41 prevent overwriting?

final Map<String, StorageConfig> storageConfigs = backendsConfig.backends().stream().collect(Collectors.toMap(StorageConfig::id, Function.identity()));
final StorageConfig storageConfig = storageConfigs.get(dto.storageConfigId());

makeS3Bucket(storageConfig, dto);

// TODO https://github.com/chenkins/cipherduck-hub/issues/41 hard-coded cryptomatorvaults
keycloakPrepareVault(syncerConfig, vaultId.toString(), storageConfig, jwt.getSubject(), "cryptomatorvaults");

return Response.created(URI.create(".")).build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ public interface VaultJWEBackend {
@JsonProperty("parentUUID")
Optional<String> parentUUID();

@JsonProperty("oAuthTokenExchangeAudience")
Optional<String> oAuthTokenExchangeAudience();


// (3) keychain credentials
@JsonProperty("username")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package org.cryptomator.hub.api.cipherduck;

import com.fasterxml.jackson.annotation.JsonProperty;

import java.util.Optional;

public record VaultJWEBackendDto(

// (1) protocol
// (1a) protocol hub-independent
Optional<String> authorization,


Optional<String> oauthRedirectUrl,

Optional<String> usernameConfigurable,

Optional<String> passwordConfigurable,

Optional<String> tokenConfigurable,


// (1b) protocol hub-specific
Optional<String> oauthAuthorizationUrl,

@JsonProperty("oAuthTokenUrl")
Optional<String> oauthTokenUrl,

Optional<String> oauthClientId,


// (1c) protocol storage-specific
Optional<String> protocol,

Optional<String> vendor,

Optional<String> region,

Optional<String> stsEndpoint,

Optional<String> scheme,


// (2) bookmark aka. Host
// (2a) bookmark direct fields
Optional<String> hostname,

Optional<Integer> port,

Optional<String> defaultPath,

Optional<String> nickname,

Optional<String> uuid,


// (2b) boookmark custom properties
Optional<String> stsRoleArn,

Optional<String> stsRoleArn2,

Optional<Integer> stsDurationSeconds,

Optional<String> parentUUID,

Optional<String> oAuthTokenExchangeAudience,


// (3) keychain credentials
Optional<String> username,

Optional<String> password) implements VaultJWEBackend {


public VaultJWEBackendDto(VaultJWEBackend s, final String oAuthAuthorizationUrl, final String oAuthTokenUrl, final String oAuthClientId, final String oAuthTokenExchangeAudience) {
this(s.authorization(),
s.oauthRedirectUrl(),
s.usernameConfigurable(),
s.passwordConfigurable(),
s.tokenConfigurable(),
Optional.of(oAuthAuthorizationUrl),
Optional.of(oAuthTokenUrl),
Optional.of(oAuthClientId),
s.protocol(),
s.vendor(),
s.region(),
s.stsEndpoint(),
s.scheme(),
s.hostname(),
s.port(),
s.defaultPath(),
s.nickname(),
s.uuid(),
s.stsRoleArn(),
s.stsRoleArn2(),
s.stsDurationSeconds(),
s.parentUUID(),
Optional.of(oAuthTokenExchangeAudience),
s.username(),
s.password());
}
}
Loading

0 comments on commit 363ae87

Please sign in to comment.