create-vaults role to create vaults for an existing AWS S3 bucket.
+ *
+ * See also: katta docs.
+ */
+@CommandLine.Command(name = "storageProfileAWSStatic",
+ description = "Upload storage profile for AWS Static.",
+ mixinStandardHelpOptions = true)
+public class StorageProfileAwsStaticSetup extends AbstractStorageProfile {
+
+ @CommandLine.Option(names = {"--region"}, description = "Bucket region, e.g. \"eu-west-1\".", required = true)
+ String region;
+
+ @CommandLine.Option(names = {"--regions"}, description = "Bucket regions, e.g. \"--regions eu-west-1 --regions eu-west-2 --regions eu-west-3\"].", required = true)
+ List regions;
+
+ @Override
+ protected void call(final UUID uuid, final String name, final ApiClient apiClient) throws ApiException {
+ final StorageProfileResourceApi storageProfileResourceApi = new StorageProfileResourceApi(apiClient);
+
+ call(uuid, name, storageProfileResourceApi);
+ }
+
+ protected void call(final UUID uuid, final String name, final StorageProfileResourceApi storageProfileResourceApi) throws ApiException {
+ storageProfileResourceApi.apiStorageprofileS3staticPost(new StorageProfileS3StaticDto()
+ .id(uuid)
+ .name(name)
+ .protocol(Protocol.S3_STATIC)
+ .archived(false)
+
+ // -- (1) bucket creation, template upload and client profile
+ .scheme("https")
+ .port(443)
+ .storageClass(S3STORAGECLASSES.STANDARD)
+ .withPathStyleAccessEnabled(false)
+
+ // -- (2) bucket creation only (only relevant for Desktop client)
+ // TODO extract option with default
+ .bucketEncryption(S3SERVERSIDEENCRYPTION.NONE)
+ .bucketVersioning(true)
+ .region(region)
+ .regions(regions)
+ // TODO extract option with default
+ .bucketPrefix("katta-")
+ // TODO bad design smell? not all S3 providers might have STS to create static bucket?
+ .stsRoleCreateBucketClient("")
+ .stsRoleCreateBucketHub("")
+ );
+ System.out.println(storageProfileResourceApi.apiStorageprofileProfileIdGet(uuid));
+ }
+}
diff --git a/admin-cli/src/main/java/cloud/katta/cli/commands/hub/StorageProfileAwsStsSetup.java b/admin-cli/src/main/java/cloud/katta/cli/commands/hub/StorageProfileAwsStsSetup.java
new file mode 100644
index 00000000..2186bdcb
--- /dev/null
+++ b/admin-cli/src/main/java/cloud/katta/cli/commands/hub/StorageProfileAwsStsSetup.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2025 shift7 GmbH. All rights reserved.
+ */
+
+package cloud.katta.cli.commands.hub;
+
+import cloud.katta.client.ApiClient;
+import cloud.katta.client.ApiException;
+import cloud.katta.client.api.StorageProfileResourceApi;
+import cloud.katta.client.model.Protocol;
+import cloud.katta.client.model.S3SERVERSIDEENCRYPTION;
+import cloud.katta.client.model.S3STORAGECLASSES;
+import cloud.katta.client.model.StorageProfileS3STSDto;
+import picocli.CommandLine;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Uploads a storage profile to Katta Server for use with AWS STS. Requires AWS STS setup.
+ *
+ * The storage profile then allows users with create-vaults role to create vaults and their corresponding S3 bucket seamlessly.
+ *
+ * See also: katta docs.
+ */
+@CommandLine.Command(name = "storageProfileAWSSTS",
+ description = "Upload storage profile for AWS STS.",
+ mixinStandardHelpOptions = true)
+public class StorageProfileAwsStsSetup extends AbstractStorageProfile {
+
+ @CommandLine.Option(names = {"--rolePrefix"}, description = "ARN Role Prefix. Example: \"arn:aws:iam::XXXXXXX:role/testing.katta.cloud-kc-realms-tamarind-\"", required = true)
+ String rolePrefix;
+
+ @CommandLine.Option(names = {"--bucketPrefix"}, description = "Bucket prefix.", required = false, defaultValue = "katta-")
+ String bucketPrefix;
+
+ @CommandLine.Option(names = {"--region"}, description = "Bucket region, e.g. \"eu-west-1\".", required = true)
+ String region;
+
+ @CommandLine.Option(names = {"--regions"}, description = "Bucket regions, e.g. [\"eu-west-1\",\"eu-west-2\",\"eu-west-3\"].", required = true)
+ List regions;
+
+ @Override
+ protected void call(final UUID uuid, final String name, final ApiClient apiClient) throws ApiException {
+ final StorageProfileResourceApi storageProfileResourceApi = new StorageProfileResourceApi(apiClient);
+
+ call(uuid, name, storageProfileResourceApi);
+ }
+
+ protected void call(final UUID uuid, final String name, final StorageProfileResourceApi storageProfileResourceApi) throws ApiException {
+ storageProfileResourceApi.apiStorageprofileS3stsPost(new StorageProfileS3STSDto()
+ .id(uuid)
+ .name(name)
+ .protocol(Protocol.S3_STS)
+ .archived(false)
+
+ // -- (1) bucket creation, template upload and client profile
+ .scheme("https")
+ .port(443)
+ .storageClass(S3STORAGECLASSES.STANDARD)
+ .withPathStyleAccessEnabled(false)
+
+ // -- (2) bucket creation only (only relevant for Desktop client)
+ .bucketPrefix(bucketPrefix)
+ .region(region)
+ .regions(regions)
+ // TODO extract option with default
+ .bucketEncryption(S3SERVERSIDEENCRYPTION.NONE)
+ .bucketVersioning(true)
+
+ // arn:aws:iam::XXXXXXX:role/testing.katta.cloud-kc-realms-tamarind-createbucket
+ .stsRoleCreateBucketClient(String.format("%s-createbucket", rolePrefix))
+ .stsRoleCreateBucketHub(String.format("%s-createbucket", rolePrefix))
+ .bucketEncryption(S3SERVERSIDEENCRYPTION.NONE)
+ // TODO https://github.com/shift7-ch/katta-clientlib/issues/190 naming
+ // arn:aws:iam::XXXXXXX:role/testing.katta.cloud-kc-realms-tamarind-sts-chain-01
+ .stsRoleAccessBucketAssumeRoleWithWebIdentity(String.format("%s-sts-chain-01", rolePrefix))
+ // arn:aws:iam::XXXXXXX:role/testing.katta.cloud-kc-realms-tamarind-sts-chain-02
+ .stsRoleAccessBucketAssumeRoleTaggedSession(String.format("%s-sts-chain-02", rolePrefix))
+ );
+ System.out.println(storageProfileResourceApi.apiStorageprofileProfileIdGet(uuid));
+ }
+}
diff --git a/admin-cli/src/main/java/cloud/katta/cli/commands/login/AuthorizationCode.java b/admin-cli/src/main/java/cloud/katta/cli/commands/login/AuthorizationCode.java
new file mode 100644
index 00000000..44d48863
--- /dev/null
+++ b/admin-cli/src/main/java/cloud/katta/cli/commands/login/AuthorizationCode.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2025 shift7 GmbH. All rights reserved.
+ */
+
+package cloud.katta.cli.commands.login;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpResponse;
+import java.util.concurrent.Callable;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.github.coffeelibs.tinyoauth2client.TinyOAuth2;
+import picocli.CommandLine;
+
+
+/**
+ * Based on hub-cli.
+ */
+@CommandLine.Command(name = "authorizationCode", description = "Get token using authorization code flow.", mixinStandardHelpOptions = true)
+public class AuthorizationCode implements Callable {
+
+ @CommandLine.Option(names = {"--tokenUrl"}, description = "Keycloak realm URL with scheme. Example: \"https://testing.katta.cloud/kc/realms/tamarind/protocol/openid-connect/token\"", required = true)
+ String tokenUrl;
+
+ @CommandLine.Option(names = {"--authUrl"}, description = "Keycloak realm URL with scheme. Example: \"https://testing.katta.cloud/kc/realms/tamarind/protocol/openid-connect/auth\"", required = true)
+ String authUrl;
+
+ @CommandLine.Option(names = {"--clientId"}, description = "Keycloak realm URL with scheme. Example: \"cryptomator\"", required = true)
+ String clientId;
+
+
+ @Override
+ public Void call() throws Exception {
+ var authResponse = TinyOAuth2.client(clientId)
+ .withTokenEndpoint(URI.create(tokenUrl))
+ .authorizationCodeGrant(URI.create(authUrl))
+ .authorize(HttpClient.newHttpClient(), uri -> {
+ System.out.println("Please login on " + uri);
+ });
+ System.out.println(authResponse);
+ printAccessToken(authResponse);
+ return null;
+ }
+
+ private static int printAccessToken(HttpResponse response) throws JsonProcessingException {
+ var statusCode = response.statusCode();
+ if(statusCode != 200) {
+ System.err.println("""
+ Request was responded with code %d and body:\n%s\n""".formatted(statusCode, response.body()));
+ return statusCode;
+ }
+ var token = new ObjectMapper().reader().readTree(response.body()).get("access_token").asText();
+ System.out.println(token);
+ return 0;
+ }
+}
diff --git a/admin-cli/src/main/java/cloud/katta/cli/commands/storage/AwsStsSetup.java b/admin-cli/src/main/java/cloud/katta/cli/commands/storage/AwsStsSetup.java
new file mode 100644
index 00000000..fa8a22bf
--- /dev/null
+++ b/admin-cli/src/main/java/cloud/katta/cli/commands/storage/AwsStsSetup.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (c) 2025 shift7 GmbH. All rights reserved.
+ */
+
+package cloud.katta.cli.commands.storage;
+
+import cloud.katta.cli.KattaSetupCli;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.IOUtils;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import picocli.CommandLine;
+import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.iam.IamClient;
+import software.amazon.awssdk.services.iam.model.*;
+
+import javax.net.ssl.HttpsURLConnection;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.Callable;
+
+/**
+ * Sets up AWS for Katta in STS mode:
+ *
+ * - creates/updates OIDC provider for cryptomator, cryptomatorhub and cryptomatorvaults clients.
+ * - creates roles and role policy for
+ *
+ *
+ *
+ *
+ * See also: Katta Docs.
+ */
+@CommandLine.Command(name = "awsSetup", description = "Setup/update OIDC provider and roles for STS in AWS.", mixinStandardHelpOptions = true)
+public class AwsStsSetup implements Callable {
+
+ @CommandLine.Option(names = {"--realmUrl"}, description = "Keycloak realm URL with scheme. Example: \"https://testing.katta.cloud/kc/realms/tamarind\"", required = true)
+ String realmUrl;
+
+ @CommandLine.Option(names = {"--profileName"}, description = "AWS profile to load AWS credentials from. See ~/.aws/credentials.", required = true)
+ String profileName;
+
+ @CommandLine.Option(names = {"--bucketPrefix"}, description = "Bucket Prefix for STS vaults.", required = false, defaultValue = "katta-")
+ String bucketPrefix;
+
+ @CommandLine.Option(names = {"--maxSessionDuration"}, description = "Bucket Prefix for STS vaults.", required = false)
+ Integer maxSessionDuration;
+
+ @CommandLine.Option(names = {"--clientId"}, description = "ClientIds for the OIDC provider.", required = true)
+ List clientId;
+
+ @Override
+ public Void call() throws Exception {
+ // remove trailing slash
+ realmUrl = realmUrl.replaceAll("/$", "");
+
+ final String arnPostfix = realmUrl.replace("https://", "");
+ final String arnPostfixSanitized = arnPostfix.replace("/", "-");
+
+ final URL url = new URI(realmUrl).toURL();
+
+ final String sha = getThumbprint(url);
+ System.out.println(sha);
+
+ try (final IamClient iam = IamClient.builder()
+ .region(Region.AWS_GLOBAL)
+ .credentialsProvider(ProfileCredentialsProvider.create(profileName))
+ .build()) {
+ final ListOpenIdConnectProvidersResponse existingOpenIdConnectProviders = iam.listOpenIDConnectProviders();
+ System.out.println(existingOpenIdConnectProviders);
+
+
+ final Optional existingOIDCProvider = existingOpenIdConnectProviders.openIDConnectProviderList().stream().filter(idp -> idp.arn().endsWith(arnPostfix)).findFirst();
+ //
+ // aws iam create-open-id-connect-provider --url https://testing.hub.cryptomator.org/kc/realms/cipherduck --client-id-list cryptomator cryptomatorhub --thumbprint-list BE21B29075BF9F3265353F8B85208A8981DAEC2A
+ //
+ final String oidcProviderArn;
+ if(existingOIDCProvider.isEmpty()) {
+ final CreateOpenIdConnectProviderResponse openIDConnectProvider = iam.createOpenIDConnectProvider(CreateOpenIdConnectProviderRequest.builder()
+ .url(realmUrl)
+ .clientIDList(clientId)
+ .thumbprintList(sha)
+ .build());
+ oidcProviderArn = openIDConnectProvider.openIDConnectProviderArn();
+ System.out.println(oidcProviderArn);
+ }
+ else {
+ oidcProviderArn = existingOIDCProvider.get().arn();
+ iam.updateOpenIDConnectProviderThumbprint(UpdateOpenIdConnectProviderThumbprintRequest.builder()
+ .openIDConnectProviderArn(oidcProviderArn)
+ .thumbprintList(sha).build());
+
+ }
+ System.out.println(oidcProviderArn);
+ final String arnPrefix = oidcProviderArn.replace(":oidc-provider" + "/" + arnPostfix, "");
+
+ //
+ // aws iam create-role --role-name cipherduck-createbucket --assume-role-policy-document file://src/main/resources/cipherduck/setup/aws_stscreatebuckettrustpolicy.json
+ // aws iam put-role-policy --role-name cipherduck-createbucket --policy-name cipherduck-createbucket --policy-document file://src/main/resources/cipherduck/setup/aws_stscreatebucketpermissionpolicy.json
+ //
+ final String awsSTSCreateBucketRoleName = String.format("%s-createbucket", arnPostfixSanitized);
+ final JSONObject awsSTSCreateBuckeTrustPolicyTemplate = new JSONObject(IOUtils.toString(KattaSetupCli.class.getResourceAsStream("/setup/aws_sts/createbuckettrustpolicy.json"), Charset.defaultCharset()));
+ final JSONObject awsSTSCreateBuckePermissionPolicyTemplate = new JSONObject(IOUtils.toString(KattaSetupCli.class.getResourceAsStream("/setup/aws_sts/createbucketpermissionpolicy.json"), Charset.defaultCharset()));
+ injectFederated(awsSTSCreateBuckeTrustPolicyTemplate, oidcProviderArn);
+ injectBucketPrefixIntoResources(awsSTSCreateBuckePermissionPolicyTemplate, bucketPrefix);
+ uploadAssumeRolePolicyAndPermissionPolicy(iam, awsSTSCreateBucketRoleName, awsSTSCreateBuckeTrustPolicyTemplate, awsSTSCreateBuckePermissionPolicyTemplate, maxSessionDuration);
+
+ //
+ // aws iam create-role --role-name cipherduck_chain_01 --assume-role-policy-document file://src/main/resources/cipherduck/setup/aws_stscipherduck_chain_01_trustpolicy.json
+ // aws iam put-role-policy --role-name cipherduck_chain_01 --policy-name cipherduck_chain_01 --policy-document file://src/main/resources/cipherduck/setup/aws_stscipherduck_chain_01_permissionpolicy.json
+ //
+ final String awsSTSChain01RoleName = String.format("%s-sts-chain-01", arnPostfixSanitized);
+ final String awsSTSChain02RoleName = String.format("%s-sts-chain-02", arnPostfixSanitized);
+ final JSONObject awsSTSChain01RoleNameTrustPolicyTemplate = new JSONObject(IOUtils.toString(KattaSetupCli.class.getResourceAsStream("/setup/aws_sts/cipherduck_chain_01_trustpolicy.json"), Charset.defaultCharset()));
+ final JSONObject awsSTSChain01RoleNamePermissionPolicyTemplate = new JSONObject(IOUtils.toString(KattaSetupCli.class.getResourceAsStream("/setup/aws_sts/cipherduck_chain_01_permissionpolicy.json"), Charset.defaultCharset()));
+ injectFederated(awsSTSChain01RoleNameTrustPolicyTemplate, oidcProviderArn);
+ awsSTSChain01RoleNamePermissionPolicyTemplate.getJSONArray("Statement").getJSONObject(0).put("Resource", arnPrefix + ":role/" + awsSTSChain02RoleName);
+ uploadAssumeRolePolicyAndPermissionPolicy(iam, awsSTSChain01RoleName, awsSTSChain01RoleNameTrustPolicyTemplate, awsSTSChain01RoleNamePermissionPolicyTemplate, maxSessionDuration);
+
+ //
+ // sleep 10;
+ //
+ Thread.sleep(10000);
+
+ //
+ // aws iam create-role --role-name cipherduck_chain_02 --assume-role-policy-document file://src/main/resources/cipherduck/setup/aws_stscipherduck_chain_02_trustpolicy.json
+ // aws iam put-role-policy --role-name cipherduck_chain_02 --policy-name cipherduck_chain_02 --policy-document file://src/main/resources/cipherduck/setup/aws_stscipherduck_chain_02_permissionpolicy.json
+ //
+ final JSONObject awsSTSChain02RoleNameTrustPolicyTemplate = new JSONObject(IOUtils.toString(KattaSetupCli.class.getResourceAsStream("/setup/aws_sts/cipherduck_chain_02_trustpolicy.json"), Charset.defaultCharset()));
+ final JSONObject awsSTSChain02RoleNamePermissionPolicyTemplate = new JSONObject(IOUtils.toString(KattaSetupCli.class.getResourceAsStream("/setup/aws_sts/cipherduck_chain_02_permissionpolicy.json"), Charset.defaultCharset()));
+ final GetRoleResponse role = iam.getRole(GetRoleRequest.builder().roleName(awsSTSChain01RoleName).build());
+ awsSTSChain02RoleNameTrustPolicyTemplate.getJSONArray("Statement").getJSONObject(0).getJSONObject("Principal").put("AWS", Collections.singletonList(role.role().arn()));
+ injectBucketPrefixIntoResources(awsSTSChain02RoleNamePermissionPolicyTemplate, bucketPrefix);
+ uploadAssumeRolePolicyAndPermissionPolicy(iam, awsSTSChain02RoleName, awsSTSChain02RoleNameTrustPolicyTemplate, awsSTSChain02RoleNamePermissionPolicyTemplate, maxSessionDuration);
+ }
+ return null;
+ }
+
+ private static void injectBucketPrefixIntoResources(final JSONObject policy, final String bucketPrefix) {
+ final JSONArray statements = policy.getJSONArray("Statement");
+ for(int i = 0; i < statements.length(); i++) {
+ final JSONArray resources = statements.getJSONObject(i).getJSONArray("Resource");
+ for(int j = 0; j < resources.length(); j++) {
+ resources.put(j, resources.getString(j).replace("cipherduck", bucketPrefix));
+ }
+ }
+ }
+
+ private static void injectFederated(final JSONObject policy, final String oidcProviderArn) {
+ policy.getJSONArray("Statement").getJSONObject(0).getJSONObject("Principal").put("Federated", Collections.singletonList(oidcProviderArn));
+ }
+
+ private static void uploadAssumeRolePolicyAndPermissionPolicy(final IamClient iam, final String roleName, final JSONObject trustPolicyDocument, final JSONObject permissionPolicyDocument, final Integer maxSessionDuration) {
+ System.out.println(trustPolicyDocument);
+ System.out.println(permissionPolicyDocument);
+ try {
+ final GetRoleResponse role = iam.getRole(GetRoleRequest.builder().roleName(roleName).build());
+ System.out.println(role);
+ iam.updateAssumeRolePolicy(UpdateAssumeRolePolicyRequest.builder()
+ .roleName(roleName)
+ .policyDocument(trustPolicyDocument.toString())
+ .build());
+
+ }
+ catch(NoSuchEntityException e) {
+ iam.createRole(CreateRoleRequest.builder()
+ .roleName(roleName)
+ .assumeRolePolicyDocument(trustPolicyDocument.toString())
+ .maxSessionDuration(maxSessionDuration)
+ .build()
+ );
+ }
+ iam.putRolePolicy(PutRolePolicyRequest.builder()
+ .roleName(roleName)
+ .policyName(roleName)
+ .policyDocument(permissionPolicyDocument.toString())
+ .build());
+ }
+
+ private static String getThumbprint(final URL url) throws IOException, CertificateEncodingException {
+ // openssl s_client -servername testing.hub.cryptomator.org -showcerts -connect testing.hub.cryptomator.org:443 > testing.hub.cryptomator.org.crt
+ //
+ // vi testing.hub.cryptomator.org.crt ...
+ // (remove the irrelevant parts from the chain)
+ //
+ // cat testing.hub.cryptomator.org.crt
+ // -----BEGIN CERTIFICATE-----
+ // MIIGBDCCBOygAwIBAgISA1CGKN3OkGJihg/qGhz2fl3fMA0GCSqGSIb3DQEBCwUA
+ // MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD
+ // EwJSMzAeFw0yMzExMTIxMzAyMTdaFw0yNDAyMTAxMzAyMTZaMCYxJDAiBgNVBAMT
+ // G3Rlc3RpbmcuaHViLmNyeXB0b21hdG9yLm9yZzCCAiIwDQYJKoZIhvcNAQEBBQAD
+ // ggIPADCCAgoCggIBALWWmJr7lckOPCysl8p8FywJ2BwfCfdqMqTeb7KdOa3Zd9kb
+ // rb0dYUAs6cs4XKIxSBzKTDJAZiE5d2/iXUgHIBS8hDjG8U40EFaKDTc/JugOSovs
+ // HB6FQTi4YCMNfm3oMBiREMXYQTEKErBFfECbtGw8mTua2suT6Uc7lwj91qbPO6BN
+ // TROk0Az1NcifYOz8lMZhelg0WXEa10YfalaKGtjh4srMBv0rT85PpXaJXaNp58Ls
+ // 4Psf/YlPjGJOhevnyAuqZouUD9sz7gZX8WvQ87y9uTXpDoarySh/0nppYLPZTDty
+ // sI3LeVwwrf4ir5jObVgjkH1CdS8kj/ueKLLW0BBqSX/9oji9o1zFJlBeRcWbeW08
+ // SD3+7292cy+zpNo3Y7xEFxGs0SVlJjTRk4cf6edkVq5QzTPqIF9FSn6tgXC6OTJi
+ // ISHnLGvkuSOzCieADPwjlYJiix3duK+0rpeN3xH3/NnyvPnncbWr/KLwwGE/tsHx
+ // orv1XLXkV0nmD9MDvE1gqRd7m7n3PwXEojz2Ih37i4bowFx2jYy6acAyY0KJSWwE
+ // 3Rl2BRvOqXY1AOZC2MKOp7mb3hbryr8pzUPb0j4p3iOmOG9MgUQydKLyE97W1Ucd
+ // PRQMHdoG+EKnDeaauKdZ/3Lj0jMJ1CKlmYOB5qShHv1XCR5uimouioQkoJTFAgMB
+ // AAGjggIeMIICGjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEG
+ // CCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFHkBSFhuApvRJvGqRHZg
+ // 5t183UMCMB8GA1UdIwQYMBaAFBQusxe3WFbLrlAJQOYfr52LFMLGMFUGCCsGAQUF
+ // BwEBBEkwRzAhBggrBgEFBQcwAYYVaHR0cDovL3IzLm8ubGVuY3Iub3JnMCIGCCsG
+ // AQUFBzAChhZodHRwOi8vcjMuaS5sZW5jci5vcmcvMCYGA1UdEQQfMB2CG3Rlc3Rp
+ // bmcuaHViLmNyeXB0b21hdG9yLm9yZzATBgNVHSAEDDAKMAgGBmeBDAECATCCAQUG
+ // CisGAQQB1nkCBAIEgfYEgfMA8QB2ADtTd3U+LbmAToswWwb+QDtn2E/D9Me9AA0t
+ // cm/h+tQXAAABi8PXIB0AAAQDAEcwRQIhAPOlsQr63JOSMbTFWOM746oA7i4HQ+hl
+ // p7M3pRpG4HYQAiBKqLSDsx1FdI18Fax3k7zkCgsY8x96ZAQvVUfdch0xoAB3AO7N
+ // 0GTV2xrOxVy3nbTNE6Iyh0Z8vOzew1FIWUZxH7WbAAABi8PXIBwAAAQDAEgwRgIh
+ // AOZskIE18A5sTthKz6w3wMvIocbaoj3UCTCIAXWVJJNzAiEAmMWS709vLq/WOPG0
+ // 5hb6lBPn6NRnjizJaNEnj/ts71EwDQYJKoZIhvcNAQELBQADggEBADiSgsGpOKqZ
+ // 0kzeIS9x7vJlc3I0lnScB9JjxJyLoZFs//T4SNWE18zFxnzVspWRnwu4NTmuGURv
+ // 6RWJ8RAznYwjZCnVDdQREUSX7wahzGdz+3GalRaIYngkvwHOhT+aGLbrKRjz+Pfh
+ // 13qMStwjlfA6iSofHqVeQFCf48itgeVjNbpdZKEOLwdiV+JMwpT4n/i0nfVwWkaG
+ // RcEWn8S4gfSq1iZ/LAhWdyB0QJ4EcCO6mx02wABxbQibPc5FM8Q64j37TizHniVu
+ // hs+X7qFNDF/jvbob3sL09e0BLjiZWxVasAHiAAaZONTRV0N5YYV56F5br/vnegic
+ // u3AvSS5HW70=
+ // -----END CERTIFICATE-----
+ //
+ //
+ // openssl x509 -in testing.hub.cryptomator.org.crt -fingerprint -sha1 -noout | sed -e 's/://g' | sed -e 's/[Ss][Hh][Aa]1 [Ff]ingerprint=//'
+ // BE21B29075BF9F3265353F8B85208A8981DAEC2A
+ //
+ // aws iam create-open-id-connect-provider --url https://testing.hub.cryptomator.org/kc/realms/cipherduck --client-id-list cryptomator cryptomatorhub --thumbprint-list BE21B29075BF9F3265353F8B85208A8981DAEC2A
+ // {
+ // "OpenIDConnectProviderArn": "arn:aws:iam::930717317329:oidc-provider/testing.hub.cryptomator.org/kc/realms/cipherduck1"
+ // }
+ //
+ // aws iam list-open-id-connect-providers
+ //
+ // aws iam get-open-id-connect-provider --open-id-connect-provider-arn arn:aws:iam::930717317329:oidc-provider/testing.hub.cryptomator.org/kc/realms/cipherduck
+ // {
+ // "Url": "testing.hub.cryptomator.org/kc/realms/cipherduck",
+ // "ClientIDList": [
+ // "cryptomatorhub",
+ // "cryptomator"
+ // ],
+ // "ThumbprintList": [
+ // "a053375bfe84e8b748782c7cee15827a6af5a405"
+ // ],
+ // "CreateDate": "2023-11-13T13:51:32.729000+00:00",
+ // "Tags": []
+ // }
+ HttpURLConnection.setFollowRedirects(false);
+ final Certificate[] chain = getCertificates(url);
+
+ // TODO is it always first cert?
+ return DigestUtils.sha1Hex(chain[0].getEncoded());
+ }
+
+ private static Certificate[] getCertificates(final URL url) throws IOException {
+ final HttpsURLConnection c = (HttpsURLConnection) url.openConnection();
+ c.setRequestMethod("HEAD"); // GET
+ c.setDoOutput(false);
+ c.setAllowUserInteraction(false);
+ c.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:45.0) Gecko/20100101 Firefox/45.0 URLInspect/1.0");
+ c.setRequestProperty("Connection", "close");
+
+ c.connect(); // throws SSL handshake exception
+
+ // retrieve TLS info before reading response (which closes connection?)
+ return c.getServerCertificates();
+ }
+}
diff --git a/admin-cli/src/main/java/cloud/katta/cli/commands/storage/MinioStsSetup.java b/admin-cli/src/main/java/cloud/katta/cli/commands/storage/MinioStsSetup.java
new file mode 100644
index 00000000..53f550be
--- /dev/null
+++ b/admin-cli/src/main/java/cloud/katta/cli/commands/storage/MinioStsSetup.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2025 shift7 GmbH. All rights reserved.
+ */
+
+package cloud.katta.cli.commands.storage;
+
+import org.apache.commons.io.IOUtils;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Callable;
+
+import cloud.katta.cli.KattaSetupCli;
+import io.minio.admin.MinioAdminClient;
+import picocli.CommandLine;
+import software.amazon.awssdk.policybuilder.iam.IamEffect;
+import software.amazon.awssdk.policybuilder.iam.IamPolicy;
+import software.amazon.awssdk.policybuilder.iam.IamPolicyWriter;
+
+/**
+ * Sets up MinIO for Katta in STS mode:
+ *
+ * - creates/updates OIDC provider for cryptomator, cryptomatorhub and cryptomatorvaults clients.
+ * - creates roles and role policy for
+ *
+ * - creating vaults: access restricted to creating buckets with given prefix
+ * - accessing vaults: access restricted to reading/writing to single bucket.
+ *
+ *
+ *
+ *
+ * See also: Katta Docs.
+ */
+@CommandLine.Command(name = "minioStsSetup", description = "Setup/update OIDC provider and roles for STS in MinIO.", mixinStandardHelpOptions = true)
+public class MinioStsSetup implements Callable {
+ @CommandLine.Option(names = {"--endpointUrl"}, description = "MinIO URL. Example: \"http://localhost:9000\"", required = true)
+ String endpointUrl;
+
+ @CommandLine.Option(names = {"--hubUrl"}, description = "Hub URL. Example: \"https://testing.katta.cloud/tamarind\"", required = true)
+ String hubUrl;
+
+ @CommandLine.Option(names = {"--profileName"}, description = "AWS profile to load AWS credentials from. See ~/.aws/credentials.", required = false)
+ String profileName;
+
+ @CommandLine.Option(names = {"--accessKey"}, description = "Access Key for administering MinIO if no profile is used.", required = false)
+ String accessKey;
+
+ @CommandLine.Option(names = {"--secretKey"}, description = "Secret Key for administering MinIO if no profile is used.", required = false)
+ String secretKey;
+
+ @CommandLine.Option(names = {"--bucketPrefix"}, description = "Bucket Prefix for STS vaults.", required = false, defaultValue = "katta-")
+ String bucketPrefix;
+
+ @CommandLine.Option(names = {"--maxSessionDuration"}, description = "Bucket Prefix for STS vaults.", required = false)
+ Integer maxSessionDuration;
+
+ @CommandLine.Option(names = {"--createbucketPolicyName"}, description = "Policy name for accessing Katta STS buckets. Defaults to {bucketPrefix}createbucketPolicy.")
+ String createbucketPolicyName;
+
+ @CommandLine.Option(names = {"--accessbucketPolicyName"}, description = "Policy name for accessing Katta STS buckets. Defaults to {bucketPrefix}accessbucketpolicy.")
+ String accessbucketPolicyName;
+
+ @Override
+ public Void call() throws Exception {
+ if(createbucketPolicyName == null) {
+ createbucketPolicyName = String.format("%screatebucketpolicy", bucketPrefix);
+ }
+ if(accessbucketPolicyName == null) {
+ accessbucketPolicyName = String.format("%saccessbucketpolicy", bucketPrefix);
+ }
+
+ final MinioAdminClient minioAdminClient = new MinioAdminClient.Builder()
+ .credentials(accessKey, secretKey)
+ .endpoint(endpointUrl).build();
+
+ // /mc admin policy create myminio cipherduckcreatebucket /setup/minio_sts/createbucketpolicy.json
+ {
+ final IamPolicy miniocreatebucketpolicy = IamPolicy.builder()
+ .addStatement(b -> b
+ .effect(IamEffect.ALLOW)
+ .addAction("s3:CreateBucket")
+ .addAction("s3:GetBucketPolicy")
+ .addAction("s3:PutBucketVersioning")
+ .addAction("s3:GetBucketVersioning")
+ .addResource(String.format("arn:aws:s3:::%s*", bucketPrefix)))
+ .addStatement(b -> b
+ .effect(IamEffect.ALLOW)
+ .addAction("s3:PutObject")
+ .addResource(String.format("arn:aws:s3:::%s*/*/", bucketPrefix))
+ .addResource(String.format("arn:aws:s3:::%s*/*.uvf", bucketPrefix)))
+ .build();
+ minioAdminClient.addCannedPolicy(createbucketPolicyName, miniocreatebucketpolicy.toJson(IamPolicyWriter.builder()
+ .prettyPrint(true)
+ .build()));
+ System.out.println(minioAdminClient.listCannedPolicies().get(createbucketPolicyName));
+ }
+ // /mc admin policy create myminio cipherduckaccessbucket /setup/minio_sts/accessbucketpolicy.json
+ {
+ final JSONObject minioaccessbucketpolicy = new JSONObject(IOUtils.toString(KattaSetupCli.class.getResourceAsStream("/setup/local/minio_sts/accessbucketpolicy.json"), Charset.defaultCharset()));
+ final JSONArray statements = minioaccessbucketpolicy.getJSONArray("Statement");
+ for(int i = 0; i < statements.length(); i++) {
+ final List list = statements.getJSONObject(i).getJSONArray("Resource").toList().stream().map(Objects::toString).map(s -> s.replace("katta", bucketPrefix)).toList();
+ statements.getJSONObject(i).put("Resource", list);
+ }
+ minioAdminClient.addCannedPolicy(accessbucketPolicyName, minioaccessbucketpolicy.toString());
+ System.out.println(minioAdminClient.listCannedPolicies().get(accessbucketPolicyName));
+ }
+
+ final String json = IOUtils.toString(URI.create(hubUrl + "/api/config"), Charset.forName("UTF-8"));
+ final JSONObject apiConfig = new JSONObject(json);
+ final String wellKnown = String.format("%s/realms/%s/.well-known/openid-configuration", apiConfig.getString("keycloakUrl"), apiConfig.getString("keycloakRealm"));
+
+ String keycloakClientIdCryptomator = apiConfig.getString("keycloakClientIdCryptomator");
+ String keycloakClientIdHub = apiConfig.getString("keycloakClientIdHub");
+ String keycloakClientIdCryptomatorVaults = apiConfig.getString("keycloakClientIdCryptomatorVaults");
+ System.out.println(String.format("""
+ # The MinIO Client API is incomplete (https://github.com/minio/minio/issues/16151).
+ # Please execute the following commands on the command line.
+ # Further info: https://github.com/shift7-ch/katta-docs/blob/main/SETUP_KATTA_SERVER.md#minio
+
+ mc alias set myminio %s %s %s
+
+ mc idp openid add myminio %s \\
+ config_url="%s" \\
+ client_id="%s" \\
+ client_secret="ignore-me" \\
+ role_policy="%s"
+ mc idp openid add myminio %s \\
+ config_url="%s" \\
+ client_id="%s" \\
+ client_secret="ignore-me" \\
+ role_policy="%s" \s
+ mc idp openid add myminio %s \\
+ config_url="%s" \\
+ client_id="%s" \\
+ client_secret="ignore-me" \\
+ role_policy="%s" \s
+ mc admin service restart myminio
+ """, endpointUrl, accessKey, secretKey,
+ keycloakClientIdCryptomator, wellKnown, keycloakClientIdCryptomator, createbucketPolicyName,
+ keycloakClientIdHub, wellKnown, keycloakClientIdHub, accessbucketPolicyName,
+ keycloakClientIdCryptomatorVaults, wellKnown, keycloakClientIdCryptomatorVaults, accessbucketPolicyName));
+ return null;
+ }
+}
diff --git a/admin-cli/src/test/java/cloud/katta/cli/commands/hub/StorageProfileArchiveIT.java b/admin-cli/src/test/java/cloud/katta/cli/commands/hub/StorageProfileArchiveIT.java
new file mode 100644
index 00000000..71bbdca7
--- /dev/null
+++ b/admin-cli/src/test/java/cloud/katta/cli/commands/hub/StorageProfileArchiveIT.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2025 shift7 GmbH. All rights reserved.
+ */
+
+package cloud.katta.cli.commands.hub;
+
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.UUID;
+
+import cloud.katta.client.api.StorageProfileResourceApi;
+import cloud.katta.client.model.Protocol;
+import cloud.katta.client.model.S3SERVERSIDEENCRYPTION;
+import cloud.katta.client.model.S3STORAGECLASSES;
+import cloud.katta.client.model.StorageProfileDto;
+import cloud.katta.client.model.StorageProfileS3StaticDto;
+import cloud.katta.model.StorageProfileDtoWrapper;
+import cloud.katta.testcontainers.AbtractAdminCliIT;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class StorageProfileArchiveIT extends AbtractAdminCliIT {
+ @Test
+ public void testStorageProfileArchive() throws Exception {
+ final String storageProfileId = "732D43FA-3716-46C4-B931-66EA5405EF1C".toLowerCase();
+ final StorageProfileResourceApi storageProfileResourceApi = new StorageProfileResourceApi(apiClient);
+ {
+ storageProfileResourceApi.apiStorageprofileS3staticPost(new StorageProfileS3StaticDto()
+ .id(UUID.fromString(storageProfileId))
+ .name("S3 static")
+ .protocol(Protocol.S3_STATIC)
+ .archived(false)
+ .storageClass(S3STORAGECLASSES.STANDARD)
+ .region("eu-west-1")
+ .regions(Arrays.asList("eu-west-1"))
+ .bucketPrefix("katta-test")
+ .stsRoleCreateBucketClient("")
+ .stsRoleCreateBucketHub("")
+ .bucketVersioning(true)
+ .bucketEncryption(S3SERVERSIDEENCRYPTION.NONE)
+ );
+
+ final Optional profile = storageProfileResourceApi.apiStorageprofileGet(null).stream().filter(p -> StorageProfileDtoWrapper.coerce(p).getId().toString().toLowerCase().equals(storageProfileId))
+ .map(StorageProfileDto::getActualInstance).map(StorageProfileS3StaticDto.class::cast)
+ .findFirst();
+ assertTrue(profile.isPresent());
+ assertFalse(profile.get().getArchived());
+ }
+ new StorageProfileArchive().call("http://localhost:8280", accessToken, storageProfileId, "S3 static");
+ {
+ final Optional profile = storageProfileResourceApi.apiStorageprofileGet(null).stream().filter(p -> StorageProfileDtoWrapper.coerce(p).getId().toString().toLowerCase().equals(storageProfileId)).map(StorageProfileDto::getActualInstance).map(StorageProfileS3StaticDto.class::cast).findFirst();
+ assertTrue(profile.isPresent());
+ assertTrue(profile.get().getArchived());
+ }
+ }
+}
diff --git a/admin-cli/src/test/java/cloud/katta/cli/commands/hub/StorageProfileArchiveTest.java b/admin-cli/src/test/java/cloud/katta/cli/commands/hub/StorageProfileArchiveTest.java
new file mode 100644
index 00000000..2dc7086d
--- /dev/null
+++ b/admin-cli/src/test/java/cloud/katta/cli/commands/hub/StorageProfileArchiveTest.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2025 shift7 GmbH. All rights reserved.
+ */
+
+package cloud.katta.cli.commands.hub;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.UUID;
+
+import cloud.katta.client.ApiException;
+import cloud.katta.client.api.StorageProfileResourceApi;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+
+class StorageProfileArchiveTest {
+
+ @Test
+ public void testCall() throws ApiException {
+ final StorageProfileResourceApi proxyMock = Mockito.mock(StorageProfileResourceApi.class);
+ final UUID vaultId = UUID.randomUUID();
+ new StorageProfileArchive().call(vaultId, proxyMock);
+ Mockito.verify(proxyMock, times(1)).apiStorageprofileProfileIdPut(vaultId, true);
+ Mockito.verify(proxyMock, times(1)).apiStorageprofileProfileIdPut(any(), any());
+ }
+
+}
diff --git a/admin-cli/src/test/java/cloud/katta/cli/commands/hub/StorageProfileAwsStaticSetupIT.java b/admin-cli/src/test/java/cloud/katta/cli/commands/hub/StorageProfileAwsStaticSetupIT.java
new file mode 100644
index 00000000..8cbf7c66
--- /dev/null
+++ b/admin-cli/src/test/java/cloud/katta/cli/commands/hub/StorageProfileAwsStaticSetupIT.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2025 shift7 GmbH. All rights reserved.
+ */
+
+package cloud.katta.cli.commands.hub;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.UUID;
+
+import cloud.katta.cli.KattaSetupCli;
+import cloud.katta.client.api.StorageProfileResourceApi;
+import cloud.katta.client.model.Protocol;
+import cloud.katta.client.model.S3SERVERSIDEENCRYPTION;
+import cloud.katta.client.model.S3STORAGECLASSES;
+import cloud.katta.client.model.StorageProfileDto;
+import cloud.katta.client.model.StorageProfileS3StaticDto;
+import cloud.katta.testcontainers.AbtractAdminCliIT;
+import picocli.CommandLine;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class StorageProfileAwsStaticSetupIT extends AbtractAdminCliIT {
+
+ @Test
+ public void testStorageProfileAwsStaticSetup() throws Exception {
+ final UUID storageProfileId = UUID.randomUUID();
+ int rc = new CommandLine(new KattaSetupCli()).execute(
+ "storageProfileAWSStatic",
+ "--hubUrl", "http://localhost:8280",
+ "--accessToken", accessToken,
+ "--uuid", storageProfileId.toString(),
+ "--name", "AWS S3 Static",
+ "--region", "eu-west-1",
+ "--regions", "eu-west-1",
+ "--regions", "eu-west-2",
+ "--regions", "eu-west-3"
+ );
+ assertEquals(0, rc);
+ final StorageProfileResourceApi storageProfileResourceApi = new StorageProfileResourceApi(apiClient);
+ Optional profile = storageProfileResourceApi.apiStorageprofileGet(null).stream()
+ .filter(p -> p.getActualInstance() instanceof StorageProfileS3StaticDto)
+ .filter(p -> p.getStorageProfileS3StaticDto().getId().equals(storageProfileId)).findFirst();
+ assertTrue(profile.isPresent());
+ final StorageProfileS3StaticDto dto = profile.get().getStorageProfileS3StaticDto();
+ assertEquals("AWS S3 Static", dto.getName());
+ assertEquals(Protocol.S3_STATIC, dto.getProtocol());
+ assertFalse(dto.getArchived());
+ assertEquals("https", dto.getScheme());
+ assertNull(dto.getHostname());
+ assertEquals(443, dto.getPort());
+ assertFalse(dto.getWithPathStyleAccessEnabled());
+ assertEquals(S3STORAGECLASSES.STANDARD, dto.getStorageClass());
+ assertEquals("eu-west-1", dto.getRegion());
+ assertEquals(Arrays.asList("eu-west-1", "eu-west-2", "eu-west-3"), dto.getRegions());
+ assertEquals("katta-", dto.getBucketPrefix());
+ assertEquals("", dto.getStsRoleCreateBucketClient());
+ assertEquals("", dto.getStsRoleCreateBucketHub());
+ assertNull(dto.getStsEndpoint());
+ assertTrue(dto.getBucketVersioning());
+ assertNull(dto.getBucketAcceleration());
+ assertEquals(S3SERVERSIDEENCRYPTION.NONE, dto.getBucketEncryption());
+ }
+}
diff --git a/admin-cli/src/test/java/cloud/katta/cli/commands/hub/StorageProfileAwsStaticSetupTest.java b/admin-cli/src/test/java/cloud/katta/cli/commands/hub/StorageProfileAwsStaticSetupTest.java
new file mode 100644
index 00000000..3086b659
--- /dev/null
+++ b/admin-cli/src/test/java/cloud/katta/cli/commands/hub/StorageProfileAwsStaticSetupTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2025 shift7 GmbH. All rights reserved.
+ */
+
+package cloud.katta.cli.commands.hub;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.List;
+import java.util.UUID;
+
+import cloud.katta.client.ApiException;
+import cloud.katta.client.api.StorageProfileResourceApi;
+import cloud.katta.client.model.Protocol;
+import cloud.katta.client.model.S3SERVERSIDEENCRYPTION;
+import cloud.katta.client.model.S3STORAGECLASSES;
+import cloud.katta.client.model.StorageProfileS3StaticDto;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+
+class StorageProfileAwsStaticSetupTest {
+ @Test
+ public void testCall() throws ApiException {
+ final StorageProfileResourceApi api = Mockito.mock(StorageProfileResourceApi.class);
+ final UUID vaultId = UUID.randomUUID();
+ final StorageProfileAwsStaticSetup cli = new StorageProfileAwsStaticSetup();
+
+ cli.region = "us-east-1";
+ cli.regions = List.of();
+ cli.call(vaultId, "AWS S3 static", api);
+
+
+ final StorageProfileS3StaticDto dto = new StorageProfileS3StaticDto();
+ dto.setId(vaultId);
+ dto.setName("AWS S3 static");
+ dto.setRegion("us-east-1");
+ dto.setProtocol(Protocol.S3_STATIC);
+ dto.setArchived(false);
+ dto.setScheme("https");
+ dto.setPort(443);
+ dto.setWithPathStyleAccessEnabled(false);
+ dto.setBucketPrefix("katta-");
+ dto.setStorageClass(S3STORAGECLASSES.STANDARD);
+ dto.setBucketEncryption(S3SERVERSIDEENCRYPTION.NONE);
+ dto.stsRoleCreateBucketClient("");
+ dto.stsRoleCreateBucketHub("");
+ Mockito.verify(api, times(1)).apiStorageprofileS3staticPost(dto);
+ Mockito.verify(api, times(1)).apiStorageprofileS3staticPost(any());
+ }
+}
diff --git a/admin-cli/src/test/java/cloud/katta/cli/commands/hub/StorageProfileAwsStsSetupIT.java b/admin-cli/src/test/java/cloud/katta/cli/commands/hub/StorageProfileAwsStsSetupIT.java
new file mode 100644
index 00000000..3400e918
--- /dev/null
+++ b/admin-cli/src/test/java/cloud/katta/cli/commands/hub/StorageProfileAwsStsSetupIT.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2025 shift7 GmbH. All rights reserved.
+ */
+
+package cloud.katta.cli.commands.hub;
+
+import cloud.katta.cli.KattaSetupCli;
+import cloud.katta.client.api.StorageProfileResourceApi;
+import cloud.katta.client.model.*;
+import cloud.katta.testcontainers.AbtractAdminCliIT;
+import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.Test;
+import picocli.CommandLine;
+
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.UUID;
+
+class StorageProfileAwsStsSetupIT extends AbtractAdminCliIT {
+
+ @Test
+ public void testStorageProfileAwsStsSetup() throws Exception {
+ final UUID storageProfileId = UUID.randomUUID();
+ int rc = new CommandLine(new KattaSetupCli()).execute(
+ "storageProfileAWSSTS",
+ "--hubUrl", "http://localhost:8280",
+ "--accessToken", accessToken,
+ "--uuid", storageProfileId.toString(),
+ "--name", "AWS S3 STS",
+ "--rolePrefix", "arn:aws:iam::linguine:role/farfalle",
+ "--region", "eu-west-1",
+ "--regions", "eu-west-1",
+ "--regions", "eu-west-2",
+ "--regions", "eu-west-3"
+ );
+ assertEquals(0, rc);
+ final StorageProfileResourceApi storageProfileResourceApi = new StorageProfileResourceApi(apiClient);
+ Optional profile = storageProfileResourceApi.apiStorageprofileGet(null).stream()
+ .filter(p -> p.getActualInstance() instanceof StorageProfileS3STSDto)
+ .filter(p -> p.getStorageProfileS3STSDto().getId().equals(storageProfileId)).findFirst();
+ assertTrue(profile.isPresent());
+ final StorageProfileS3STSDto dto = profile.get().getStorageProfileS3STSDto();
+ assertEquals("AWS S3 STS", dto.getName());
+ assertEquals(Protocol.S3_STS, dto.getProtocol());
+ assertFalse(dto.getArchived());
+ assertEquals("https", dto.getScheme());
+ assertNull(dto.getHostname());
+ assertEquals(443, dto.getPort());
+ assertFalse(dto.getWithPathStyleAccessEnabled());
+ assertEquals(S3STORAGECLASSES.STANDARD, dto.getStorageClass());
+ assertEquals("eu-west-1", dto.getRegion());
+ assertEquals(Arrays.asList("eu-west-1", "eu-west-2", "eu-west-3"), dto.getRegions());
+ assertEquals("katta-", dto.getBucketPrefix());
+ assertEquals("arn:aws:iam::linguine:role/farfalle-createbucket", dto.getStsRoleCreateBucketClient());
+ assertEquals("arn:aws:iam::linguine:role/farfalle-createbucket", dto.getStsRoleCreateBucketHub());
+ assertNull(dto.getStsEndpoint());
+ assertTrue(dto.getBucketVersioning());
+ assertNull(dto.getBucketAcceleration());
+ assertEquals(S3SERVERSIDEENCRYPTION.NONE, dto.getBucketEncryption());
+ assertEquals("arn:aws:iam::linguine:role/farfalle-sts-chain-01", dto.getStsRoleAccessBucketAssumeRoleWithWebIdentity());
+ assertEquals("arn:aws:iam::linguine:role/farfalle-sts-chain-02", dto.getStsRoleAccessBucketAssumeRoleTaggedSession());
+ assertNull(dto.getStsDurationSeconds());
+ }
+}
diff --git a/admin-cli/src/test/java/cloud/katta/cli/commands/hub/StorageProfileAwsStsSetupTest.java b/admin-cli/src/test/java/cloud/katta/cli/commands/hub/StorageProfileAwsStsSetupTest.java
new file mode 100644
index 00000000..6a884429
--- /dev/null
+++ b/admin-cli/src/test/java/cloud/katta/cli/commands/hub/StorageProfileAwsStsSetupTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2025 shift7 GmbH. All rights reserved.
+ */
+
+package cloud.katta.cli.commands.hub;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.UUID;
+
+import cloud.katta.client.ApiException;
+import cloud.katta.client.api.StorageProfileResourceApi;
+import cloud.katta.client.model.Protocol;
+import cloud.katta.client.model.S3SERVERSIDEENCRYPTION;
+import cloud.katta.client.model.S3STORAGECLASSES;
+import cloud.katta.client.model.StorageProfileS3STSDto;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+
+class StorageProfileAwsStsSetupTest {
+ @Test
+ public void testCall() throws ApiException {
+ final StorageProfileResourceApi api = Mockito.mock(StorageProfileResourceApi.class);
+ final UUID vaultId = UUID.randomUUID();
+ final StorageProfileAwsStsSetup cli = new StorageProfileAwsStsSetup();
+ cli.bucketPrefix = "fancy-";
+ cli.rolePrefix = "arn:aws:iam::1234:role/testing.katta.cloud-kc-realms-pepper";
+ cli.region = "eu-west-1";
+ cli.regions = Arrays.asList("eu-west-1", "eu-west-2", "eu-west-3");
+ cli.call(vaultId, "AWS S3 STS", api);
+
+ final StorageProfileS3STSDto dto = new StorageProfileS3STSDto();
+ dto.setId(vaultId);
+ dto.setName("AWS S3 STS");
+ dto.setProtocol(Protocol.S3_STS);
+ dto.setArchived(false);
+ dto.setScheme("https");
+ dto.setPort(443);
+ dto.setWithPathStyleAccessEnabled(false);
+ dto.setStorageClass(S3STORAGECLASSES.STANDARD);
+ dto.setRegion("eu-west-1");
+ dto.setRegions(Arrays.asList("eu-west-1", "eu-west-2", "eu-west-3"));
+ dto.bucketPrefix("fancy-");
+ dto.stsRoleCreateBucketClient("arn:aws:iam::1234:role/testing.katta.cloud-kc-realms-pepper-createbucket");
+ dto.stsRoleCreateBucketHub("arn:aws:iam::1234:role/testing.katta.cloud-kc-realms-pepper-createbucket");
+ dto.setBucketEncryption(S3SERVERSIDEENCRYPTION.NONE);
+ dto.stsRoleAccessBucketAssumeRoleWithWebIdentity("arn:aws:iam::1234:role/testing.katta.cloud-kc-realms-pepper-sts-chain-01");
+ dto.stsRoleAccessBucketAssumeRoleTaggedSession("arn:aws:iam::1234:role/testing.katta.cloud-kc-realms-pepper-sts-chain-02");
+ Mockito.verify(api, times(1)).apiStorageprofileS3stsPost(dto);
+ Mockito.verify(api, times(1)).apiStorageprofileS3stsPost(any());
+ }
+}
diff --git a/admin-cli/src/test/java/cloud/katta/cli/commands/hub/Tamarind.java b/admin-cli/src/test/java/cloud/katta/cli/commands/hub/Tamarind.java
new file mode 100644
index 00000000..f2b6f531
--- /dev/null
+++ b/admin-cli/src/test/java/cloud/katta/cli/commands/hub/Tamarind.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2025 shift7 GmbH. All rights reserved.
+ */
+
+package cloud.katta.cli.commands.hub;
+
+import cloud.katta.cli.KattaSetupCli;
+import picocli.CommandLine;
+
+import java.util.UUID;
+
+/**
+ * Tamarind example.
+ */
+public class Tamarind {
+
+ public static void main(String[] args) {
+ if(false) {
+ new CommandLine(new KattaSetupCli()).execute(
+ "storageProfileArchive",
+ "--tokenUrl", "https://testing.katta.cloud/kc/realms/tamarind/protocol/openid-connect/token",
+ "--authUrl", "https://testing.katta.cloud/kc/realms/tamarind/protocol/openid-connect/auth",
+ "--clientId", "cryptomator",
+ "--hubUrl", "https://testing.katta.cloud/tamarind/",
+ "--uuid", "d7f8aa61-7b07-423c-89e5-fff8b8c2a56e",
+ // TODO --name should not be required
+ "--name", "ignore-me"
+ );
+ }
+
+ if(true) {
+ final UUID storageProfileId = UUID.randomUUID();
+ new CommandLine(new KattaSetupCli()).execute(
+ "storageProfileAWSSTS",
+ "--tokenUrl", "https://testing.katta.cloud/kc/realms/tamarind/protocol/openid-connect/token",
+ "--authUrl", "https://testing.katta.cloud/kc/realms/tamarind/protocol/openid-connect/auth",
+ "--clientId", "cryptomator",
+ "--hubUrl", "https://testing.katta.cloud/tamarind/",
+ "--uuid", storageProfileId.toString(),
+ "--name", "AWS S3 STS",
+ "--bucketPrefix", "katta-test-",
+ "--rolePrefix", "arn:aws:iam::430118840017:role/testing.katta.cloud-kc-realms-tamarind"
+ );
+ }
+ }
+}
+
diff --git a/admin-cli/src/test/java/cloud/katta/cli/commands/storage/MinioStsSetupIT.java b/admin-cli/src/test/java/cloud/katta/cli/commands/storage/MinioStsSetupIT.java
new file mode 100644
index 00000000..8e5fc933
--- /dev/null
+++ b/admin-cli/src/test/java/cloud/katta/cli/commands/storage/MinioStsSetupIT.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2025 shift7 GmbH. All rights reserved.
+ */
+
+package cloud.katta.cli.commands.storage;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+import java.util.Objects;
+
+import cloud.katta.cli.KattaSetupCli;
+import cloud.katta.testcontainers.AbtractAdminCliIT;
+import io.minio.admin.MinioAdminClient;
+import picocli.CommandLine;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class MinioStsSetupIT extends AbtractAdminCliIT {
+
+ @Test
+ public void testMinioSetup() throws Exception {
+ int rc = new CommandLine(new KattaSetupCli()).execute(
+ "minioStsSetup",
+ "--endpointUrl", "http://localhost:9100",
+ "--hubUrl", "http://localhost:8280",
+ "--accessKey", "minioadmin",
+ "--secretKey", "minioadmin",
+ "--bucketPrefix", "fusilli"
+ );
+ assertEquals(0, rc);
+
+ final MinioAdminClient minioAdminClient = new MinioAdminClient.Builder()
+ .credentials("minioadmin", "minioadmin")
+ .endpoint("http://localhost:9100").build();
+
+ final Map cannedPolicies = minioAdminClient.listCannedPolicies();
+ {
+ final JSONObject miniocreatebucketpolicy = new JSONObject(cannedPolicies.get("fusillicreatebucketpolicy"));
+ final JSONArray statements = miniocreatebucketpolicy.getJSONArray("Statement");
+ long count = 0;
+ for(int i = 0; i < statements.length(); i++) {
+ count += statements.getJSONObject(i).getJSONArray("Resource").toList().stream().map(Objects::toString).filter(s -> s.contains("fusilli")).count();
+ }
+ assertEquals(3, count);
+ }
+ {
+ final JSONObject minioaccessbucket = new JSONObject(cannedPolicies.get("fusilliaccessbucketpolicy"));
+ final JSONArray statements = minioaccessbucket.getJSONArray("Statement");
+ long count = 0;
+ for(int i = 0; i < statements.length(); i++) {
+ count += statements.getJSONObject(i).getJSONArray("Resource").toList().stream().map(Objects::toString).filter(s -> s.contains("fusilli")).count();
+ }
+ assertEquals(2, count);
+ }
+ }
+}
diff --git a/admin-cli/src/test/java/cloud/katta/testcontainers/AbtractAdminCliIT.java b/admin-cli/src/test/java/cloud/katta/testcontainers/AbtractAdminCliIT.java
new file mode 100644
index 00000000..bc911606
--- /dev/null
+++ b/admin-cli/src/test/java/cloud/katta/testcontainers/AbtractAdminCliIT.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2025 shift7 GmbH. All rights reserved.
+ */
+
+package cloud.katta.testcontainers;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.testcontainers.containers.ComposeContainer;
+import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.stream.Collectors;
+
+import cloud.katta.client.ApiClient;
+import cloud.katta.client.ApiException;
+import cloud.katta.client.Pair;
+import cloud.katta.client.auth.HttpBearerAuth;
+
+import static io.restassured.RestAssured.given;
+
+public class AbtractAdminCliIT {
+ private static final Logger log = LogManager.getLogger(AbtractAdminCliIT.class.getName());
+ public static ComposeContainer compose;
+
+ protected String accessToken;
+ protected ApiClient apiClient;
+
+ @BeforeAll
+ public static void setupDocker() throws URISyntaxException, IOException {
+ final String composeFile = "/docker-compose-minio-localhost-hub.yml";
+ final String envFile = "/.local.env";
+ final String profile = "local";
+ final String hubAdminUser = "admin";
+ final String hubAdminPassword = "admin";
+ final String hubKeycloakSystemClientSecret = "top-secret";
+ final Properties props = new Properties();
+ props.load(AbtractAdminCliIT.class.getResourceAsStream(envFile));
+ final HashMap env = props.entrySet().stream().collect(
+ Collectors.toMap(
+ e -> String.valueOf(e.getKey()),
+ e -> String.valueOf(e.getValue()),
+ (prev, next) -> next, HashMap::new
+ ));
+ env.put("HUB_ADMIN_USER", hubAdminUser);
+ env.put("HUB_ADMIN_PASSWORD", hubAdminPassword);
+ env.put("HUB_KEYCLOAK_SYSTEM_CLIENT_SECRET", hubKeycloakSystemClientSecret);
+ compose = new ComposeContainer(
+ new File(AbtractAdminCliIT.class.getResource(composeFile).toURI()))
+ .withLocalCompose(true)
+ .withPull(true)
+ .withEnv(env)
+ .withOptions(profile == null ? "" : String.format("--profile=%s", profile))
+ .waitingFor("minio_setup-1", new LogMessageWaitStrategy().withRegEx(".*Completed MinIO Setup.*").withStartupTimeout(Duration.ofMinutes(2)));
+ compose.start();
+ }
+
+ @BeforeEach
+ protected void setup() throws Exception {
+ accessToken = given()
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .formParam("client_id", "cryptomator")
+ .formParam("grant_type", "password")
+ .formParam("username", "admin")
+ .formParam("password", "admin")
+ .when()
+ .post("http://localhost:8380/realms/cryptomator/protocol/openid-connect/token")
+ .then()
+ .statusCode(200)
+ .extract().path("access_token");
+ final HttpBearerAuth auth = new HttpBearerAuth("Bearer");
+ auth.setBearerToken(accessToken);
+ apiClient = new ApiClient() {
+ protected void updateParamsForAuth(String[] authNames, List queryParams, Map headerParams,
+ Map cookieParams, String payload, String method, URI uri) throws ApiException {
+ auth.applyToParams(queryParams, headerParams, cookieParams, payload, method, uri);
+ }
+ };
+ apiClient.setBasePath("http://localhost:8280");
+ }
+
+ @AfterAll
+ public static void teardownDocker() {
+ try {
+ compose.stop();
+ }
+ catch(Exception e) {
+ log.warn(e);
+ }
+ }
+}
diff --git a/hub/pom.xml b/hub/pom.xml
index 6be86547..1d74ef36 100644
--- a/hub/pom.xml
+++ b/hub/pom.xml
@@ -27,6 +27,18 @@
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.4.2
+
+
+
+ test-jar
+
+
+
+
org.openapitools
diff --git a/pom.xml b/pom.xml
index 0eb59da5..141c178c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,6 +15,7 @@
hub
osx
+ admin-cli