From 4be72b633d8524bd072ca24d8d838996a784b149 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 5 Jun 2025 10:01:41 +0200 Subject: [PATCH 001/133] Require controller constructor argument. --- .../cloud/katta/core/DeviceSetupCallbackFactory.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/hub/src/main/java/cloud/katta/core/DeviceSetupCallbackFactory.java b/hub/src/main/java/cloud/katta/core/DeviceSetupCallbackFactory.java index b1bc99e6..a4fd9fe4 100644 --- a/hub/src/main/java/cloud/katta/core/DeviceSetupCallbackFactory.java +++ b/hub/src/main/java/cloud/katta/core/DeviceSetupCallbackFactory.java @@ -4,6 +4,7 @@ package cloud.katta.core; +import ch.cyberduck.core.Controller; import ch.cyberduck.core.Factory; import org.apache.commons.lang3.reflect.ConstructorUtils; @@ -20,16 +21,16 @@ private DeviceSetupCallbackFactory() { super("factory.devicesetupcallback.class"); } - public DeviceSetupCallback create() { + public DeviceSetupCallback create(final Controller controller) { try { final Constructor constructor - = ConstructorUtils.getMatchingAccessibleConstructor(clazz); + = ConstructorUtils.getMatchingAccessibleConstructor(clazz, controller.getClass()); if(null == constructor) { - log.warn("No default controller in {}", constructor.getClass()); + log.warn("No matching constructor for parameter {}", controller.getClass()); // Call default constructor for disabled implementations return clazz.getDeclaredConstructor().newInstance(); } - return constructor.newInstance(); + return constructor.newInstance(controller); } catch(InstantiationException | InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { log.error("Failure loading callback class {}. {}", clazz, e.getMessage()); From da18bdcd1c3618d4ef6b46fe28a4270bcac6ff8a Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 11 Jun 2025 15:41:37 +0200 Subject: [PATCH 002/133] Add implementation using alerts. --- .../katta/model/AccountKeyAndDeviceName.java | 10 + .../controller/DeviceSetupController.java | 115 +++++++++ .../DeviceSetupWithAccountKeyController.java | 161 ------------- .../controller/FirstLoginController.java | 218 +++++++----------- .../controller/PromptDeviceSetupCallback.java | 57 +++++ 5 files changed, 259 insertions(+), 302 deletions(-) create mode 100644 osx/src/main/java/cloud/katta/controller/DeviceSetupController.java delete mode 100644 osx/src/main/java/cloud/katta/controller/DeviceSetupWithAccountKeyController.java create mode 100644 osx/src/main/java/cloud/katta/controller/PromptDeviceSetupCallback.java diff --git a/hub/src/main/java/cloud/katta/model/AccountKeyAndDeviceName.java b/hub/src/main/java/cloud/katta/model/AccountKeyAndDeviceName.java index ceb5ed3c..47534599 100644 --- a/hub/src/main/java/cloud/katta/model/AccountKeyAndDeviceName.java +++ b/hub/src/main/java/cloud/katta/model/AccountKeyAndDeviceName.java @@ -7,6 +7,7 @@ public class AccountKeyAndDeviceName { private String accountKey; private String deviceName; + private boolean addToKeychain; public String accountKey() { return accountKey; @@ -16,6 +17,10 @@ public String deviceName() { return deviceName; } + public boolean addToKeychain() { + return addToKeychain; + } + public AccountKeyAndDeviceName withAccountKey(final String accountKey) { this.accountKey = accountKey; return this; @@ -25,4 +30,9 @@ public AccountKeyAndDeviceName withDeviceName(final String deviceName) { this.deviceName = deviceName; return this; } + + public AccountKeyAndDeviceName withAddToKeychain(final boolean addToKeychain) { + this.addToKeychain = addToKeychain; + return this; + } } diff --git a/osx/src/main/java/cloud/katta/controller/DeviceSetupController.java b/osx/src/main/java/cloud/katta/controller/DeviceSetupController.java new file mode 100644 index 00000000..ae099418 --- /dev/null +++ b/osx/src/main/java/cloud/katta/controller/DeviceSetupController.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2025 shift7 GmbH. All rights reserved. + */ + +package cloud.katta.controller; + +import ch.cyberduck.binding.Action; +import ch.cyberduck.binding.AlertController; +import ch.cyberduck.binding.Outlet; +import ch.cyberduck.binding.application.NSAlert; +import ch.cyberduck.binding.application.NSCell; +import ch.cyberduck.binding.application.NSControl; +import ch.cyberduck.binding.application.NSImage; +import ch.cyberduck.binding.application.NSSecureTextField; +import ch.cyberduck.binding.application.NSTextField; +import ch.cyberduck.binding.application.NSView; +import ch.cyberduck.binding.application.SheetCallback; +import ch.cyberduck.binding.foundation.NSNotification; +import ch.cyberduck.binding.foundation.NSNotificationCenter; +import ch.cyberduck.core.LocaleFactory; +import ch.cyberduck.core.StringAppender; +import ch.cyberduck.core.preferences.PreferencesFactory; +import ch.cyberduck.core.resources.IconCacheFactory; + +import org.apache.commons.lang3.StringUtils; +import org.rococoa.Foundation; + +import cloud.katta.model.AccountKeyAndDeviceName; + +public class DeviceSetupController extends AlertController { + + private final AccountKeyAndDeviceName accountKeyAndDeviceName; + + @Outlet + private final NSTextField accountKeyField = NSSecureTextField.textFieldWithString(StringUtils.EMPTY); + + @Outlet + private final NSTextField deviceNameField = NSTextField.textFieldWithString(StringUtils.EMPTY); + + public DeviceSetupController(final AccountKeyAndDeviceName accountKeyAndDeviceName) { + this.accountKeyAndDeviceName = accountKeyAndDeviceName; + } + + @Override + public NSAlert loadAlert() { + final NSAlert alert = NSAlert.alert(); + alert.setIcon(IconCacheFactory.get().iconNamed("cryptomator.tiff", 64)); + alert.setAlertStyle(NSAlert.NSInformationalAlertStyle); + alert.setMessageText(LocaleFactory.localizedString("Authorization Required", "Hub")); + alert.setInformativeText(new StringAppender() + .append(LocaleFactory.localizedString("This is your first login on this device.", "Hub")) + .append(LocaleFactory.localizedString("Your Account Key is required to link this browser to your account.", "Hub")).toString()); + alert.setShowsSuppressionButton(true); + alert.suppressionButton().setTitle(LocaleFactory.localizedString("Add to Keychain", "Login")); + alert.suppressionButton().setState(PreferencesFactory.get().getBoolean("cryptomator.vault.keychain") ? NSCell.NSOnState : NSCell.NSOffState); + return alert; + } + + @Override + public NSView getAccessoryView(final NSAlert alert) { + final NSView accessoryView = NSView.create(); + { + accountKeyField.cell().setPlaceholderString(LocaleFactory.localizedString("Account Key", "Hub")); + accountKeyField.setToolTip(LocaleFactory.localizedString("Your Account Key is required to authorize this device.", "Hub")); + NSNotificationCenter.defaultCenter().addObserver(this.id(), + Foundation.selector("accountKeyFieldTextDidChange:"), + NSControl.NSControlTextDidChangeNotification, + accountKeyField.id()); + this.addAccessorySubview(accessoryView, accountKeyField); + } + { + updateField(deviceNameField, accountKeyAndDeviceName.deviceName(), TRUNCATE_MIDDLE_ATTRIBUTES); + deviceNameField.cell().setPlaceholderString(LocaleFactory.localizedString("Device Name", "Hub")); + deviceNameField.setToolTip(LocaleFactory.localizedString("Name this device for easy identification in your authorized devices list.", "Hub")); + NSNotificationCenter.defaultCenter().addObserver(this.id(), + Foundation.selector("deviceNameFieldTextDidChange:"), + NSControl.NSControlTextDidChangeNotification, + deviceNameField.id()); + this.addAccessorySubview(accessoryView, deviceNameField); + } + return accessoryView; + } + + @Override + protected void focus(final NSAlert alert) { + super.focus(alert); + window.makeFirstResponder(accountKeyField); + } + + @Override + public boolean validate(final int option) { + if(SheetCallback.DEFAULT_OPTION == option) { + return StringUtils.isNotBlank(accountKeyField.stringValue()); + } + return true; + } + + @Override + public void callback(final int returncode) { + if(SheetCallback.DEFAULT_OPTION == returncode) { + accountKeyAndDeviceName.withAddToKeychain(this.isSuppressed()); + } + } + + @Action + public void accountKeyFieldTextDidChange(final NSNotification sender) { + accountKeyAndDeviceName.withAccountKey(StringUtils.trim(accountKeyField.stringValue())); + } + + @Action + public void deviceNameFieldTextDidChange(final NSNotification sender) { + accountKeyAndDeviceName.withDeviceName(StringUtils.trim(deviceNameField.stringValue())); + } +} + diff --git a/osx/src/main/java/cloud/katta/controller/DeviceSetupWithAccountKeyController.java b/osx/src/main/java/cloud/katta/controller/DeviceSetupWithAccountKeyController.java deleted file mode 100644 index 71a90ae4..00000000 --- a/osx/src/main/java/cloud/katta/controller/DeviceSetupWithAccountKeyController.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (c) 2025 shift7 GmbH. All rights reserved. - */ - -package cloud.katta.controller; - -import ch.cyberduck.binding.Action; -import ch.cyberduck.binding.BundleController; -import ch.cyberduck.binding.Outlet; -import ch.cyberduck.binding.SheetController; -import ch.cyberduck.binding.application.NSControl; -import ch.cyberduck.binding.application.NSImage; -import ch.cyberduck.binding.application.NSImageView; -import ch.cyberduck.binding.application.NSSecureTextField; -import ch.cyberduck.binding.application.NSTextField; -import ch.cyberduck.binding.foundation.NSAttributedString; -import ch.cyberduck.binding.foundation.NSNotification; -import ch.cyberduck.binding.foundation.NSNotificationCenter; -import ch.cyberduck.core.LocaleFactory; -import ch.cyberduck.core.StringAppender; -import ch.cyberduck.core.resources.IconCacheFactory; - -import org.apache.commons.lang3.StringUtils; -import org.rococoa.Foundation; - -import cloud.katta.model.AccountKeyAndDeviceName; - - -public class DeviceSetupWithAccountKeyController extends SheetController { - protected final NSNotificationCenter notificationCenter - = NSNotificationCenter.defaultCenter(); - private final String title; - private final String reason; - private final String icon; - private final AccountKeyAndDeviceName accountKeyAndDeviceName; - - private final String setupCodeLabel_; - private final String setupCodeHint_; - private final String deviceNameLabel_; - private final String deviceNameHint_; - - @Outlet - private NSImageView iconView; - @Outlet - private NSTextField titleField; - @Outlet - private NSTextField messageField; - - @Outlet - protected NSTextField setupCodeField; - @Outlet - protected NSTextField setupCodeLabel; - @Outlet - protected NSTextField setupCodeHint; - - @Outlet - protected NSTextField deviceNameField; - @Outlet - protected NSTextField deviceNameLabel; - @Outlet - protected NSTextField deviceNameHint; - - public DeviceSetupWithAccountKeyController(final AccountKeyAndDeviceName accountKeyAndDeviceName) { - this.accountKeyAndDeviceName = accountKeyAndDeviceName; - this.title = LocaleFactory.localizedString("Authorization Required", "Cipherduck"); - this.reason = LocaleFactory.localizedString("This is your first login on this device. ", "Cipherduck"); - this.setupCodeLabel_ = LocaleFactory.localizedString("Account Key", "Cipherduck"); - this.setupCodeHint_ = LocaleFactory.localizedString("Your Account Key is required to authorize this device.", "Cipherduck"); - this.deviceNameLabel_ = LocaleFactory.localizedString("Device Name", "Cipherduck"); - this.deviceNameHint_ = LocaleFactory.localizedString("Name this device for easy identification in your authorized devices list.", "Cipherduck"); - this.icon = "cryptomator.tiff"; - } - - @Override - public void awakeFromNib() { - super.awakeFromNib(); - window.makeFirstResponder(titleField); - } - - @Override - protected String getBundleName() { - return "DeviceSetupWithAccountKey"; - } - - public void setIconView(NSImageView iconView) { - this.iconView = iconView; - this.iconView.setImage(IconCacheFactory.get().iconNamed(this.icon, 64)); - } - - public void setTitleField(NSTextField titleField) { - this.titleField = titleField; - this.updateField(this.titleField, LocaleFactory.localizedString(title, "Credentials")); - } - - public void setMessageField(NSTextField messageField) { - this.messageField = messageField; - this.messageField.setSelectable(true); - this.updateField(this.messageField, new StringAppender().append(reason).toString()); - } - - public void setDeviceNameField(final NSTextField field) { - this.deviceNameField = field; - this.deviceNameField.setStringValue(StringUtils.isNotBlank(this.accountKeyAndDeviceName.deviceName()) ? this.accountKeyAndDeviceName.deviceName() : StringUtils.EMPTY); - this.notificationCenter.addObserver(this.id(), - Foundation.selector("deviceNameInputDidChange:"), - NSControl.NSControlTextDidChangeNotification, - field.id()); - } - - @Action - public void deviceNameInputDidChange(final NSNotification sender) { - accountKeyAndDeviceName.withDeviceName(StringUtils.trim(deviceNameField.stringValue())); - } - - public void setDeviceNameLabel(final NSTextField deviceNameLabel) { - this.deviceNameLabel = deviceNameLabel; - deviceNameLabel.setAttributedStringValue(NSAttributedString.attributedStringWithAttributes( - String.format("%s:", this.deviceNameLabel_), - BundleController.TRUNCATE_TAIL_ATTRIBUTES - )); - } - - public void setDeviceNameHint(NSTextField messageField) { - this.deviceNameHint = messageField; - this.updateField(this.deviceNameHint, LocaleFactory.localizedString(this.deviceNameHint_, "Cipherduck")); - } - - public void setSetupCodeLabel(final NSTextField setSetupCodeLabel) { - this.setupCodeLabel = setSetupCodeLabel; - setSetupCodeLabel.setAttributedStringValue(NSAttributedString.attributedStringWithAttributes( - String.format("%s:", this.setupCodeLabel_), - BundleController.TRUNCATE_TAIL_ATTRIBUTES - )); - } - - public void setSetupCodeField(NSSecureTextField field) { - this.setupCodeField = field; - this.notificationCenter.addObserver(this.id(), - Foundation.selector("setupCodeInputDidChange:"), - NSControl.NSControlTextDidChangeNotification, - field.id()); - } - - @Action - public void setupCodeInputDidChange(final NSNotification sender) { - this.accountKeyAndDeviceName.withAccountKey(StringUtils.trim(setupCodeField.stringValue())); - } - - public void setSetupCodeHint(NSTextField messageField) { - this.setupCodeHint = messageField; - this.updateField(this.setupCodeHint, LocaleFactory.localizedString(this.setupCodeHint_, "Cipherduck")); - } - - - @Override - public void callback(final int returncode) { - // - } - -} - diff --git a/osx/src/main/java/cloud/katta/controller/FirstLoginController.java b/osx/src/main/java/cloud/katta/controller/FirstLoginController.java index 0d00b83c..d9ae5a65 100644 --- a/osx/src/main/java/cloud/katta/controller/FirstLoginController.java +++ b/osx/src/main/java/cloud/katta/controller/FirstLoginController.java @@ -5,184 +5,120 @@ package cloud.katta.controller; import ch.cyberduck.binding.Action; +import ch.cyberduck.binding.AlertController; import ch.cyberduck.binding.Outlet; -import ch.cyberduck.binding.SheetController; -import ch.cyberduck.binding.application.*; +import ch.cyberduck.binding.application.NSAlert; +import ch.cyberduck.binding.application.NSCell; +import ch.cyberduck.binding.application.NSControl; +import ch.cyberduck.binding.application.NSImage; +import ch.cyberduck.binding.application.NSSecureTextField; +import ch.cyberduck.binding.application.NSTextField; +import ch.cyberduck.binding.application.NSView; +import ch.cyberduck.binding.application.SheetCallback; import ch.cyberduck.binding.foundation.NSNotification; import ch.cyberduck.binding.foundation.NSNotificationCenter; import ch.cyberduck.core.LocaleFactory; import ch.cyberduck.core.StringAppender; +import ch.cyberduck.core.preferences.PreferencesFactory; import ch.cyberduck.core.resources.IconCacheFactory; -import ch.cyberduck.ui.pasteboard.PasteboardService; -import ch.cyberduck.ui.pasteboard.PasteboardServiceFactory; -import cloud.katta.model.AccountKeyAndDeviceName; + import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.rococoa.Foundation; -import org.rococoa.ID; - -public class FirstLoginController extends SheetController { - private static final Logger log = LogManager.getLogger(FirstLoginController.class.getName()); - - protected final NSNotificationCenter notificationCenter - = NSNotificationCenter.defaultCenter(); - private final String title; - private final String reason; - private final String icon; - - private final String setupCodeHint_; - private final String setupCodeHintIcon_; - private final String setupCodeHint2_; - private final String setupCodeHint2Icon_; - private final String deviceNameHint_; - private final String deviceNameHintIcon_; - private final AccountKeyAndDeviceName accountKeyAndDeviceName; +import cloud.katta.model.AccountKeyAndDeviceName; - @Outlet - private NSImageView iconView; - @Outlet - private NSTextField titleField; - @Outlet - private NSTextField messageField; +public class FirstLoginController extends AlertController { - @Outlet - protected NSTextField setupCodeField; - @Outlet - protected NSTextField setupCodeHint; - @Outlet - private NSImageView setupCodeHintIcon; + private final AccountKeyAndDeviceName accountKeyAndDeviceName; @Outlet - protected NSTextField setupCodeHint2; - @Outlet - private NSImageView setupCodeHint2Icon; + private final NSTextField accountKeyField = NSTextField.textFieldWithString(StringUtils.EMPTY); @Outlet - protected NSTextField deviceNameHint; - @Outlet - private NSImageView deviceNameHintIcon; + private final NSTextField accountKeyConfirmField = NSSecureTextField.textFieldWithString(StringUtils.EMPTY); @Outlet - protected NSTextField deviceNameField; - private NSButton accountKeyStoredSecurelyCheckbox; - private NSButton finishSetupButton; + private final NSTextField deviceNameField = NSTextField.textFieldWithString(StringUtils.EMPTY); public FirstLoginController(final AccountKeyAndDeviceName accountKeyAndDeviceName) { - this.icon = "cryptomator.tiff"; - this.title = LocaleFactory.localizedString("Welcome to Cipherduck", "Cipherduck"); - this.reason = LocaleFactory.localizedString("On first login, every user gets a unique Account Key.", "Cipherduck"); - this.setupCodeHint_ = LocaleFactory.localizedString("Your Account Key is required to login from other apps or browsers.", "Cipherduck"); - this.setupCodeHint2_ = LocaleFactory.localizedString("You can see a list of authorized apps on your profile page in Cipherduck Hub.", "Cipherduck"); - this.deviceNameHint_ = LocaleFactory.localizedString("This device will be added to this list as:", "Cipherduck"); this.accountKeyAndDeviceName = accountKeyAndDeviceName; - this.setupCodeHintIcon_ = "KeyIcon.tiff"; - this.setupCodeHint2Icon_ = "ListBulletIcon.tiff"; - this.deviceNameHintIcon_ = "ComputerDesktopIcon.tiff"; } @Override - public void awakeFromNib() { - super.awakeFromNib(); - window.makeFirstResponder(titleField); + public NSAlert loadAlert() { + final NSAlert alert = NSAlert.alert(); + alert.setAlertStyle(NSAlert.NSInformationalAlertStyle); + alert.setIcon(IconCacheFactory.get().iconNamed("cryptomator.tiff", 64)); + alert.setMessageText(LocaleFactory.localizedString("Account Key", "Hub")); + alert.setInformativeText(new StringAppender() + .append(LocaleFactory.localizedString("On first login, every user gets a unique Account Key", "Hub")) + .append(LocaleFactory.localizedString("Your Account Key is required to login from other apps or browsers", "Hub")) + .append(LocaleFactory.localizedString("You can see a list of authorized apps on your profile page", "Hub")).toString()); + alert.addButtonWithTitle(LocaleFactory.localizedString("Finish Setup", "Hub")); + alert.addButtonWithTitle(LocaleFactory.localizedString("Cancel", "Alert")); + alert.setShowsSuppressionButton(true); + alert.suppressionButton().setTitle(LocaleFactory.localizedString("Add to Keychain", "Login")); + alert.suppressionButton().setState(PreferencesFactory.get().getBoolean("cryptomator.vault.keychain") ? NSCell.NSOnState : NSCell.NSOffState); + return alert; } @Override - protected String getBundleName() { - return "FirstLogin"; - } - - public void setIconView(NSImageView iconView) { - this.iconView = iconView; - this.iconView.setImage(IconCacheFactory.get().iconNamed(this.icon, 64)); - } - - public void setTitleField(NSTextField titleField) { - this.titleField = titleField; - this.updateField(this.titleField, LocaleFactory.localizedString(title, "Cipherduck")); - } - - public void setMessageField(NSTextField messageField) { - this.messageField = messageField; - this.messageField.setSelectable(true); - this.updateField(this.messageField, new StringAppender().append(reason).toString()); - } - - public void setSetupCodeField(final NSTextField field) { - this.setupCodeField = field; - this.setupCodeField.setStringValue(this.accountKeyAndDeviceName.accountKey()); - } - - public void setSetupCodeHint(NSTextField messageField) { - this.setupCodeHint = messageField; - this.updateField(this.setupCodeHint, LocaleFactory.localizedString(this.setupCodeHint_, "Cipherduck")); + public NSView getAccessoryView(final NSAlert alert) { + final NSView accessoryView = NSView.create(); + { + accountKeyField.setEditable(false); + accountKeyField.setSelectable(true); + accountKeyField.cell().setWraps(false); + this.updateField(accountKeyField, accountKeyAndDeviceName.accountKey(), TRUNCATE_MIDDLE_ATTRIBUTES); + this.addAccessorySubview(accessoryView, accountKeyField); + } + + { + accountKeyConfirmField.cell().setPlaceholderString(LocaleFactory.localizedString("Confirm Account Key", "Hub")); + accountKeyConfirmField.setToolTip(LocaleFactory.localizedString("I stored my Account Key securely.", "Hub")); + this.addAccessorySubview(accessoryView, accountKeyConfirmField); + } + + { + this.updateField(deviceNameField, accountKeyAndDeviceName.deviceName()); + deviceNameField.cell().setPlaceholderString(LocaleFactory.localizedString("Device Name", "Hub")); + deviceNameField.setToolTip(LocaleFactory.localizedString("Name this device for easy identification in your authorized devices list.", "Hub")); + NSNotificationCenter.defaultCenter().addObserver(this.id(), + Foundation.selector("deviceNameFieldTextDidChange:"), + NSControl.NSControlTextDidChangeNotification, + deviceNameField.id()); + this.addAccessorySubview(accessoryView, deviceNameField); + } + + return accessoryView; } - public void setSetupCodeHintIcon(NSImageView iconView) { - this.setupCodeHintIcon = iconView; - this.setupCodeHintIcon.setImage(IconCacheFactory.get().iconNamed(this.setupCodeHintIcon_, 64)); - } - - public void setSetupCodeHint2(NSTextField messageField) { - this.setupCodeHint2 = messageField; - this.updateField(this.setupCodeHint2, LocaleFactory.localizedString(this.setupCodeHint2_, "Cipherduck")); - } - - public void setSetupCodeHint2Icon(NSImageView iconView) { - this.setupCodeHint2Icon = iconView; - this.setupCodeHint2Icon.setImage(IconCacheFactory.get().iconNamed(this.setupCodeHint2Icon_, 64)); - } - - public void setDeviceNameHint(NSTextField messageField) { - this.deviceNameHint = messageField; - this.updateField(this.deviceNameHint, LocaleFactory.localizedString(this.deviceNameHint_, "Cipherduck")); - } - - public void setDeviceNameHintIcon(NSImageView iconView) { - this.deviceNameHintIcon = iconView; - this.deviceNameHintIcon.setImage(IconCacheFactory.get().iconNamed(this.deviceNameHintIcon_, 64)); - } - - public void setDeviceNameField(final NSTextField field) { - this.deviceNameField = field; - this.deviceNameField.setStringValue(StringUtils.isNotBlank(this.accountKeyAndDeviceName.deviceName()) ? this.accountKeyAndDeviceName.deviceName() : StringUtils.EMPTY); - this.notificationCenter.addObserver(this.id(), - Foundation.selector("deviceNameInputDidChange:"), - NSControl.NSControlTextDidChangeNotification, - field.id()); + @Override + protected void focus(final NSAlert alert) { + super.focus(alert); + window.makeFirstResponder(deviceNameField); } - @Action - public void deviceNameInputDidChange(final NSNotification sender) { - accountKeyAndDeviceName.withDeviceName(StringUtils.trim(deviceNameField.stringValue())); + @Override + public boolean validate(final int option) { + if(SheetCallback.DEFAULT_OPTION == option) { + if(StringUtils.equals(accountKeyField.stringValue(), accountKeyConfirmField.stringValue())) { + return StringUtils.isNotBlank(deviceNameField.stringValue()); + } + } + return true; } @Override public void callback(final int returncode) { - // + if(SheetCallback.DEFAULT_OPTION == returncode) { + accountKeyAndDeviceName.withAddToKeychain(this.isSuppressed()); + } } @Action - public void copyToClipboard(final ID sender) { - PasteboardServiceFactory.get().add(PasteboardService.Type.string, accountKeyAndDeviceName.accountKey()); - } - - public void setFinishSetupButton(final NSButton button) { - this.finishSetupButton = button; - this.finishSetupButton.setEnabled(false); - } - - public void setAccountKeyStoredSecurelyCheckbox(final NSButton button) { - this.accountKeyStoredSecurelyCheckbox = button; - this.accountKeyStoredSecurelyCheckbox.setTarget(this.id()); - this.accountKeyStoredSecurelyCheckbox.setAction(Foundation.selector("accountKeyStoredSecurelyCheckboxClicked:")); - } - - @Action - public void accountKeyStoredSecurelyCheckboxClicked(final NSButton sender) { - this.finishSetupButton.setEnabled(sender.state() == NSCell.NSOnState); + public void deviceNameFieldTextDidChange(final NSNotification sender) { + accountKeyAndDeviceName.withDeviceName(StringUtils.trim(deviceNameField.stringValue())); } - } diff --git a/osx/src/main/java/cloud/katta/controller/PromptDeviceSetupCallback.java b/osx/src/main/java/cloud/katta/controller/PromptDeviceSetupCallback.java new file mode 100644 index 00000000..c3246c4e --- /dev/null +++ b/osx/src/main/java/cloud/katta/controller/PromptDeviceSetupCallback.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025 shift7 GmbH. All rights reserved. + */ + +package cloud.katta.controller; + +import ch.cyberduck.binding.ProxyController; +import ch.cyberduck.binding.SheetController; +import ch.cyberduck.binding.application.SheetCallback; +import ch.cyberduck.core.Host; +import ch.cyberduck.core.exception.ConnectionCanceledException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import cloud.katta.core.DeviceSetupCallback; +import cloud.katta.model.AccountKeyAndDeviceName; +import cloud.katta.workflows.exceptions.AccessException; + +public class PromptDeviceSetupCallback implements DeviceSetupCallback { + private static final Logger log = LogManager.getLogger(PromptDeviceSetupCallback.class.getName()); + + private final ProxyController controller; + + public PromptDeviceSetupCallback(final ProxyController controller) { + this.controller = controller; + } + + @Override + public String displayAccountKeyAndAskDeviceName(final Host bookmark, final AccountKeyAndDeviceName accountKeyAndDeviceName) throws AccessException { + if(log.isDebugEnabled()) { + log.debug(String.format("Display Account Key for %s", bookmark)); + } + final SheetController sheet = new FirstLoginController(accountKeyAndDeviceName); + switch(controller.alert(sheet)) { + case SheetCallback.CANCEL_OPTION: + case SheetCallback.ALTERNATE_OPTION: + throw new AccessException(new ConnectionCanceledException()); + } + return accountKeyAndDeviceName.deviceName(); + } + + @Override + public AccountKeyAndDeviceName askForAccountKeyAndDeviceName(final Host bookmark, final String initialDeviceName) throws AccessException { + if(log.isDebugEnabled()) { + log.debug(String.format("Ask for Account Key for %s", bookmark)); + } + final AccountKeyAndDeviceName accountKeyAndDeviceName = new AccountKeyAndDeviceName().withDeviceName(initialDeviceName); + final DeviceSetupController sheet = new DeviceSetupController(accountKeyAndDeviceName); + switch(controller.alert(sheet)) { + case SheetCallback.CANCEL_OPTION: + case SheetCallback.ALTERNATE_OPTION: + throw new AccessException(new ConnectionCanceledException()); + } + return accountKeyAndDeviceName; + } +} From 2755a7cef410180aac08cd5778560d37a63d1d51 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 12 Jun 2025 23:05:41 +0200 Subject: [PATCH 003/133] Save account key in keychain. --- .../cloud/katta/core/DeviceSetupCallback.java | 6 ++--- .../katta/workflows/UserKeysServiceImpl.java | 25 ++++++++++++++++--- .../util/MockableDeviceSetupCallback.java | 2 +- .../katta/testsetup/AbstractHubTest.java | 14 ++++++----- .../controller/PromptDeviceSetupCallback.java | 4 +-- 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/hub/src/main/java/cloud/katta/core/DeviceSetupCallback.java b/hub/src/main/java/cloud/katta/core/DeviceSetupCallback.java index 229e691b..2d5327ca 100644 --- a/hub/src/main/java/cloud/katta/core/DeviceSetupCallback.java +++ b/hub/src/main/java/cloud/katta/core/DeviceSetupCallback.java @@ -17,10 +17,10 @@ public interface DeviceSetupCallback { /** * Prompt user for device name * - * @return Device name + * @return Account key and device name * @throws AccessException Canceled prompt by user */ - String displayAccountKeyAndAskDeviceName(Host bookmark, AccountKeyAndDeviceName accountKeyAndDeviceName) throws AccessException; + AccountKeyAndDeviceName displayAccountKeyAndAskDeviceName(Host bookmark, AccountKeyAndDeviceName accountKeyAndDeviceName) throws AccessException; /** * Prompt user for existing account key @@ -50,7 +50,7 @@ default UserKeys generateUserKeys() { DeviceSetupCallback disabled = new DeviceSetupCallback() { @Override - public String displayAccountKeyAndAskDeviceName(final Host bookmark, final AccountKeyAndDeviceName accountKeyAndDeviceName) throws AccessException { + public AccountKeyAndDeviceName displayAccountKeyAndAskDeviceName(final Host bookmark, final AccountKeyAndDeviceName accountKeyAndDeviceName) throws AccessException { throw new AccessException("Disabled"); } diff --git a/hub/src/main/java/cloud/katta/workflows/UserKeysServiceImpl.java b/hub/src/main/java/cloud/katta/workflows/UserKeysServiceImpl.java index da349b63..29fe758e 100644 --- a/hub/src/main/java/cloud/katta/workflows/UserKeysServiceImpl.java +++ b/hub/src/main/java/cloud/katta/workflows/UserKeysServiceImpl.java @@ -5,6 +5,8 @@ package cloud.katta.workflows; import ch.cyberduck.core.Host; +import ch.cyberduck.core.PasswordStoreFactory; +import ch.cyberduck.core.exception.LocalAccessDeniedException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -77,7 +79,11 @@ public UserKeys getOrCreateUserKeys(final Host hub, final UserDto me, final Devi case 404: log.warn("Device keys from keychain not present in hub. Setting up existing device w/ Account Key for existing user keys."); // Setup existing device w/ Account Key (e.g. same device for multiple hubs) - return this.recover(me, deviceKeyPair, prompt.askForAccountKeyAndDeviceName(hub, COMPUTER_NAME)); + final AccountKeyAndDeviceName input = prompt.askForAccountKeyAndDeviceName(hub, COMPUTER_NAME); + if(input.addToKeychain()) { + this.save(hub, me, input.accountKey()); + } + return this.recover(me, deviceKeyPair, input); default: throw e; } @@ -94,14 +100,25 @@ else if(validate(me)) { // TODO https://github.com/shift7-ch/katta-server/issues/27 // private key generated with P384KeyPair causes "Unexpected Error: Data provided to an operation does not meet requirements" in `UserKeys.recover`: `const privateKey = await crypto.subtle.importKey('pkcs8', decodedPrivateKey, UserKeys.KEY_DESIGNATION, false, UserKeys.KEY_USAGES);` final String accountKey = prompt.generateAccountKey(); - final String deviceName = prompt.displayAccountKeyAndAskDeviceName(hub, + final AccountKeyAndDeviceName input = prompt.displayAccountKeyAndAskDeviceName(hub, new AccountKeyAndDeviceName().withAccountKey(accountKey).withDeviceName(COMPUTER_NAME)); - - return this.uploadDeviceKeys(deviceName, + if(input.addToKeychain()) { + this.save(hub, me, accountKey); + } + return this.uploadDeviceKeys(input.deviceName(), this.uploadUserKeys(me, prompt.generateUserKeys(), accountKey), deviceKeyPair); } } + private void save(final Host hub, final UserDto me, final String accountKey) { + try { + PasswordStoreFactory.get().addPassword(hub.getNickname(), me.getEmail(), accountKey); + } + catch(LocalAccessDeniedException ex) { + log.warn("Failure saving account key", ex); + } + } + private UserKeys recover(final UserDto me, final DeviceKeys deviceKeyPair, final AccountKeyAndDeviceName accountKeyAndDeviceName) throws ApiException, SecurityFailure { try { diff --git a/hub/src/test/java/cloud/katta/core/util/MockableDeviceSetupCallback.java b/hub/src/test/java/cloud/katta/core/util/MockableDeviceSetupCallback.java index 6aa506d8..76a7ea3a 100644 --- a/hub/src/test/java/cloud/katta/core/util/MockableDeviceSetupCallback.java +++ b/hub/src/test/java/cloud/katta/core/util/MockableDeviceSetupCallback.java @@ -18,7 +18,7 @@ public static void setProxy(final DeviceSetupCallback proxy) { private static DeviceSetupCallback proxy = null; @Override - public String displayAccountKeyAndAskDeviceName(final Host bookmark, final AccountKeyAndDeviceName accountKeyAndDeviceName) throws AccessException { + public AccountKeyAndDeviceName displayAccountKeyAndAskDeviceName(final Host bookmark, final AccountKeyAndDeviceName accountKeyAndDeviceName) throws AccessException { return proxy.displayAccountKeyAndAskDeviceName(bookmark, accountKeyAndDeviceName); } diff --git a/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java b/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java index d9bc2e34..dc3c4d5e 100644 --- a/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java +++ b/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java @@ -199,16 +199,19 @@ protected static HubSession setupConnection(final HubTestConfig.Setup setup) thr } protected static @NotNull DeviceSetupCallback deviceSetupCallback(HubTestConfig.Setup setup) { - final DeviceSetupCallback proxy = new DeviceSetupCallback() { + return new DeviceSetupCallback() { @Override - public String displayAccountKeyAndAskDeviceName(final Host bookmark, final AccountKeyAndDeviceName accountKeyAndDeviceName) { - return "firstLoginMockSetup"; + public AccountKeyAndDeviceName displayAccountKeyAndAskDeviceName(final Host bookmark, final AccountKeyAndDeviceName accountKeyAndDeviceName) { + return new AccountKeyAndDeviceName().withAccountKey(setup.userConfig.setupCode).withDeviceName( + String.format("%s %s", accountKeyAndDeviceName.deviceName(), DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL) + .format(ZonedDateTime.now(ZoneId.of("Europe/Zurich"))))); } @Override public AccountKeyAndDeviceName askForAccountKeyAndDeviceName(final Host bookmark, final String initialDeviceName) { - return new AccountKeyAndDeviceName().withAccountKey(setup.userConfig.setupCode).withDeviceName(String.format("firstLoginMockSetup %s", DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL) - .format(ZonedDateTime.now(ZoneId.of("Europe/Zurich"))))); + return new AccountKeyAndDeviceName().withAccountKey(setup.userConfig.setupCode).withDeviceName( + String.format("%s %s", initialDeviceName, DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL) + .format(ZonedDateTime.now(ZoneId.of("Europe/Zurich"))))); } @Override @@ -216,7 +219,6 @@ public String generateAccountKey() { return staticSetupCode(); } }; - return proxy; } } diff --git a/osx/src/main/java/cloud/katta/controller/PromptDeviceSetupCallback.java b/osx/src/main/java/cloud/katta/controller/PromptDeviceSetupCallback.java index c3246c4e..1799676c 100644 --- a/osx/src/main/java/cloud/katta/controller/PromptDeviceSetupCallback.java +++ b/osx/src/main/java/cloud/katta/controller/PromptDeviceSetupCallback.java @@ -27,7 +27,7 @@ public PromptDeviceSetupCallback(final ProxyController controller) { } @Override - public String displayAccountKeyAndAskDeviceName(final Host bookmark, final AccountKeyAndDeviceName accountKeyAndDeviceName) throws AccessException { + public AccountKeyAndDeviceName displayAccountKeyAndAskDeviceName(final Host bookmark, final AccountKeyAndDeviceName accountKeyAndDeviceName) throws AccessException { if(log.isDebugEnabled()) { log.debug(String.format("Display Account Key for %s", bookmark)); } @@ -37,7 +37,7 @@ public String displayAccountKeyAndAskDeviceName(final Host bookmark, final Accou case SheetCallback.ALTERNATE_OPTION: throw new AccessException(new ConnectionCanceledException()); } - return accountKeyAndDeviceName.deviceName(); + return accountKeyAndDeviceName; } @Override From 42dac15a8a259c1f8fa6406e5e1cc2eed82e7cbf Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 13 Jun 2025 10:01:19 +0200 Subject: [PATCH 004/133] Add implementation using login callback. --- .../core/DefaultDeviceSetupCallback.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 hub/src/main/java/cloud/katta/core/DefaultDeviceSetupCallback.java diff --git a/hub/src/main/java/cloud/katta/core/DefaultDeviceSetupCallback.java b/hub/src/main/java/cloud/katta/core/DefaultDeviceSetupCallback.java new file mode 100644 index 00000000..39be9dda --- /dev/null +++ b/hub/src/main/java/cloud/katta/core/DefaultDeviceSetupCallback.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2025 shift7 GmbH. All rights reserved. + */ + +package cloud.katta.core; + +import ch.cyberduck.core.Credentials; +import ch.cyberduck.core.Host; +import ch.cyberduck.core.LocaleFactory; +import ch.cyberduck.core.LoginCallback; +import ch.cyberduck.core.LoginOptions; +import ch.cyberduck.core.StringAppender; +import ch.cyberduck.core.exception.LoginCanceledException; + +import cloud.katta.model.AccountKeyAndDeviceName; +import cloud.katta.workflows.exceptions.AccessException; + +public class DefaultDeviceSetupCallback implements DeviceSetupCallback { + + private final LoginCallback prompt; + + public DefaultDeviceSetupCallback(final LoginCallback prompt) { + this.prompt = prompt; + } + + @Override + public AccountKeyAndDeviceName displayAccountKeyAndAskDeviceName(final Host bookmark, final AccountKeyAndDeviceName accountKeyAndDeviceName) throws AccessException { + try { + final Credentials input = prompt.prompt(bookmark, accountKeyAndDeviceName.accountKey(), + LocaleFactory.localizedString("Account Key", "Hub"), + new StringAppender() + .append(LocaleFactory.localizedString("On first login, every user gets a unique Account Key", "Hub")) + .append(LocaleFactory.localizedString("Your Account Key is required to login from other apps or browsers", "Hub")) + .append(LocaleFactory.localizedString("You can see a list of authorized apps on your profile page", "Hub")).toString(), + new LoginOptions() + .usernamePlaceholder(LocaleFactory.localizedString("Account Key", "Hub")) + // Account key not editable + .user(false) + .passwordPlaceholder(accountKeyAndDeviceName.deviceName()) + // Input device name + .password(true) + .keychain(true) + ); + return new AccountKeyAndDeviceName() + .withAddToKeychain(input.isSaved()) + .withDeviceName(input.getUsername()) + .withAccountKey(input.getPassword()); + } + catch(LoginCanceledException e) { + throw new AccessException(e); + } + } + + @Override + public AccountKeyAndDeviceName askForAccountKeyAndDeviceName(final Host bookmark, final String initialDeviceName) throws AccessException { + try { + final Credentials input = prompt.prompt(bookmark, initialDeviceName, + LocaleFactory.localizedString("Authorization Required", "Hub"), + new StringAppender() + .append(LocaleFactory.localizedString("This is your first login on this device.", "Hub")) + .append(LocaleFactory.localizedString("Your Account Key is required to link this browser to your account.", "Hub")).toString(), + new LoginOptions() + .usernamePlaceholder(LocaleFactory.localizedString("Device Name", "Hub")) + // Customize device name + .user(true) + .passwordPlaceholder(LocaleFactory.localizedString("Account Key", "Hub")) + // Input account key + .password(true) + .keychain(true) + ); + return new AccountKeyAndDeviceName() + .withAddToKeychain(input.isSaved()) + .withDeviceName(input.getUsername()) + .withAccountKey(input.getPassword()); + } + catch(LoginCanceledException e) { + throw new AccessException(e); + } + } +} From cbd7b916406a57e6a2a898da55ba4a90a3db1b77 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 13 Jun 2025 11:23:37 +0200 Subject: [PATCH 005/133] Attach sources. --- pom.xml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pom.xml b/pom.xml index 013676ab..0eb59da5 100644 --- a/pom.xml +++ b/pom.xml @@ -144,6 +144,11 @@ + + org.apache.maven.plugins + maven-source-plugin + 3.3.1 + @@ -208,6 +213,19 @@ + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + verify + + jar-no-fork + + + + From adf2a9bcb334320ccfe6eacbdf2ea6be97a3ede0 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 13 Jun 2025 11:51:56 +0200 Subject: [PATCH 006/133] Require controller constructor argument. --- .../java/cloud/katta/core/DeviceSetupCallbackFactory.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hub/src/main/java/cloud/katta/core/DeviceSetupCallbackFactory.java b/hub/src/main/java/cloud/katta/core/DeviceSetupCallbackFactory.java index a4fd9fe4..9eccf3e9 100644 --- a/hub/src/main/java/cloud/katta/core/DeviceSetupCallbackFactory.java +++ b/hub/src/main/java/cloud/katta/core/DeviceSetupCallbackFactory.java @@ -43,10 +43,10 @@ public DeviceSetupCallback create(final Controller controller) { /** * @return Firs tLogin Device Setup Callback instance for the current platform. */ - public static synchronized DeviceSetupCallback get() { + public static synchronized DeviceSetupCallback get(final Controller controller) { if(null == singleton) { singleton = new DeviceSetupCallbackFactory(); } - return singleton.create(); + return singleton.create(controller); } } From 65e7efe63364a9d20d8c14412631646040437aae Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 13 Jun 2025 14:11:45 +0200 Subject: [PATCH 007/133] Lookup device setup prompt from login feature. --- .../core/DeviceSetupCallbackFactory.java | 52 ------------------- .../cloud/katta/protocols/hub/HubSession.java | 7 ++- .../util/MockableDeviceSetupCallback.java | 34 ------------ .../katta/testsetup/AbstractHubTest.java | 23 +++++--- osx/pom.xml | 3 +- .../controller/PromptDeviceSetupCallback.java | 13 ++++- 6 files changed, 32 insertions(+), 100 deletions(-) delete mode 100644 hub/src/main/java/cloud/katta/core/DeviceSetupCallbackFactory.java delete mode 100644 hub/src/test/java/cloud/katta/core/util/MockableDeviceSetupCallback.java diff --git a/hub/src/main/java/cloud/katta/core/DeviceSetupCallbackFactory.java b/hub/src/main/java/cloud/katta/core/DeviceSetupCallbackFactory.java deleted file mode 100644 index 9eccf3e9..00000000 --- a/hub/src/main/java/cloud/katta/core/DeviceSetupCallbackFactory.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2025 shift7 GmbH. All rights reserved. - */ - -package cloud.katta.core; - -import ch.cyberduck.core.Controller; -import ch.cyberduck.core.Factory; - -import org.apache.commons.lang3.reflect.ConstructorUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; - -public final class DeviceSetupCallbackFactory extends Factory { - private static final Logger log = LogManager.getLogger(DeviceSetupCallbackFactory.class); - - private DeviceSetupCallbackFactory() { - super("factory.devicesetupcallback.class"); - } - - public DeviceSetupCallback create(final Controller controller) { - try { - final Constructor constructor - = ConstructorUtils.getMatchingAccessibleConstructor(clazz, controller.getClass()); - if(null == constructor) { - log.warn("No matching constructor for parameter {}", controller.getClass()); - // Call default constructor for disabled implementations - return clazz.getDeclaredConstructor().newInstance(); - } - return constructor.newInstance(controller); - } - catch(InstantiationException | InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { - log.error("Failure loading callback class {}. {}", clazz, e.getMessage()); - return DeviceSetupCallback.disabled; - } - } - - private static DeviceSetupCallbackFactory singleton; - - /** - * @return Firs tLogin Device Setup Callback instance for the current platform. - */ - public static synchronized DeviceSetupCallback get(final Controller controller) { - if(null == singleton) { - singleton = new DeviceSetupCallbackFactory(); - } - return singleton.create(controller); - } -} diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 70403e18..8cc286b1 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -45,7 +45,6 @@ import cloud.katta.client.model.ConfigDto; import cloud.katta.client.model.UserDto; import cloud.katta.core.DeviceSetupCallback; -import cloud.katta.core.DeviceSetupCallbackFactory; import cloud.katta.crypto.UserKeys; import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService; import cloud.katta.protocols.hub.serializer.HubConfigDtoDeserializer; @@ -143,9 +142,9 @@ protected HubApiClient connect(final ProxyFinder proxy, final HostKeyCallback ke @Override public void login(final LoginCallback prompt, final CancelCallback cancel) throws BackgroundException { - final DeviceSetupCallback setup = DeviceSetupCallbackFactory.get(); - final Credentials credentials = host.getCredentials(); - credentials.setOauth(authorizationService.validate(credentials.getOauth())); + final DeviceSetupCallback setup = prompt.getFeature(DeviceSetupCallback.class); + log.debug("Configured with setup prompt {}", setup); + final Credentials credentials = authorizationService.validate(); try { // Set username from OAuth ID Token for saving in keychain credentials.setUsername(JWT.decode(credentials.getOauth().getIdToken()).getSubject()); diff --git a/hub/src/test/java/cloud/katta/core/util/MockableDeviceSetupCallback.java b/hub/src/test/java/cloud/katta/core/util/MockableDeviceSetupCallback.java deleted file mode 100644 index 76a7ea3a..00000000 --- a/hub/src/test/java/cloud/katta/core/util/MockableDeviceSetupCallback.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2025 shift7 GmbH. All rights reserved. - */ - -package cloud.katta.core.util; - -import ch.cyberduck.core.Host; - -import cloud.katta.core.DeviceSetupCallback; -import cloud.katta.model.AccountKeyAndDeviceName; -import cloud.katta.workflows.exceptions.AccessException; - -public class MockableDeviceSetupCallback implements DeviceSetupCallback { - public static void setProxy(final DeviceSetupCallback proxy) { - MockableDeviceSetupCallback.proxy = proxy; - } - - private static DeviceSetupCallback proxy = null; - - @Override - public AccountKeyAndDeviceName displayAccountKeyAndAskDeviceName(final Host bookmark, final AccountKeyAndDeviceName accountKeyAndDeviceName) throws AccessException { - return proxy.displayAccountKeyAndAskDeviceName(bookmark, accountKeyAndDeviceName); - } - - @Override - public AccountKeyAndDeviceName askForAccountKeyAndDeviceName(final Host bookmark, final String initialDeviceName) throws AccessException { - return proxy.askForAccountKeyAndDeviceName(bookmark, initialDeviceName); - } - - @Override - public String generateAccountKey() { - return proxy.generateAccountKey(); - } -} diff --git a/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java b/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java index dc3c4d5e..d0632f42 100644 --- a/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java +++ b/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java @@ -14,7 +14,6 @@ import ch.cyberduck.core.vault.VaultRegistryFactory; import ch.cyberduck.test.VaultTest; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Named; import org.junit.jupiter.params.provider.Arguments; @@ -28,7 +27,6 @@ import java.util.function.Function; import cloud.katta.core.DeviceSetupCallback; -import cloud.katta.core.util.MockableDeviceSetupCallback; import cloud.katta.model.AccountKeyAndDeviceName; import cloud.katta.protocols.hub.HubProtocol; import cloud.katta.protocols.hub.HubSession; @@ -149,7 +147,6 @@ protected void configureLogging(final String level) { preferences.setProperty("factory.vault.class", HubUVFVault.class.getName()); preferences.setProperty("factory.supportdirectoryfinder.class", ch.cyberduck.core.preferences.TemporarySupportDirectoryFinder.class.getName()); preferences.setProperty("factory.passwordstore.class", UnsecureHostPasswordStore.class.getName()); - preferences.setProperty("factory.devicesetupcallback.class", MockableDeviceSetupCallback.class.getName()); preferences.setProperty("factory.vaultregistry.class", HubVaultRegistry.class.getName()); preferences.setProperty("oauth.handler.scheme", "katta"); @@ -186,19 +183,29 @@ protected static HubSession setupConnection(final HubTestConfig.Setup setup) thr assertTrue(factory.forName("s3").isEnabled()); assertTrue(factory.forType(Protocol.Type.s3).isEnabled()); - final DeviceSetupCallback proxy = deviceSetupCallback(setup); - MockableDeviceSetupCallback.setProxy(proxy); - final Host hub = new HostParser(factory).get(setup.hubURL).withCredentials(new Credentials(setup.userConfig.username, setup.userConfig.password)); final HubSession session = (HubSession) SessionFactory.create(hub, new DefaultX509TrustManager(), new DefaultX509KeyManager()) .withRegistry(VaultRegistryFactory.get(new DisabledPasswordCallback())); - final LoginConnectionService login = new LoginConnectionService(new DisabledLoginCallback(), new DisabledHostKeyCallback(), + final LoginConnectionService login = new LoginConnectionService(loginCallback(setup), new DisabledHostKeyCallback(), PasswordStoreFactory.get(), new DisabledProgressListener()); login.check(session, new DisabledCancelCallback()); return session; } - protected static @NotNull DeviceSetupCallback deviceSetupCallback(HubTestConfig.Setup setup) { + protected static LoginCallback loginCallback(HubTestConfig.Setup setup) { + return new DisabledLoginCallback() { + @SuppressWarnings("unchecked") + @Override + public T getFeature(final Class type) { + if(DeviceSetupCallback.class == type) { + return (T) deviceSetupCallback(setup); + } + return null; + } + }; + } + + protected static DeviceSetupCallback deviceSetupCallback(HubTestConfig.Setup setup) { return new DeviceSetupCallback() { @Override public AccountKeyAndDeviceName displayAccountKeyAndAskDeviceName(final Host bookmark, final AccountKeyAndDeviceName accountKeyAndDeviceName) { diff --git a/osx/pom.xml b/osx/pom.xml index 36963181..4c078110 100644 --- a/osx/pom.xml +++ b/osx/pom.xml @@ -22,7 +22,8 @@ ch.cyberduck - binding + osx + ${cyberduck.version} diff --git a/osx/src/main/java/cloud/katta/controller/PromptDeviceSetupCallback.java b/osx/src/main/java/cloud/katta/controller/PromptDeviceSetupCallback.java index 1799676c..e53ba4d2 100644 --- a/osx/src/main/java/cloud/katta/controller/PromptDeviceSetupCallback.java +++ b/osx/src/main/java/cloud/katta/controller/PromptDeviceSetupCallback.java @@ -9,6 +9,7 @@ import ch.cyberduck.binding.application.SheetCallback; import ch.cyberduck.core.Host; import ch.cyberduck.core.exception.ConnectionCanceledException; +import ch.cyberduck.ui.cocoa.callback.PromptLoginCallback; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -17,12 +18,13 @@ import cloud.katta.model.AccountKeyAndDeviceName; import cloud.katta.workflows.exceptions.AccessException; -public class PromptDeviceSetupCallback implements DeviceSetupCallback { +public class PromptDeviceSetupCallback extends PromptLoginCallback implements DeviceSetupCallback { private static final Logger log = LogManager.getLogger(PromptDeviceSetupCallback.class.getName()); private final ProxyController controller; public PromptDeviceSetupCallback(final ProxyController controller) { + super(controller); this.controller = controller; } @@ -54,4 +56,13 @@ public AccountKeyAndDeviceName askForAccountKeyAndDeviceName(final Host bookmark } return accountKeyAndDeviceName; } + + @SuppressWarnings("unchecked") + @Override + public T getFeature(final Class type) { + if(type == DeviceSetupCallback.class) { + return (T) this; + } + return null; + } } From 505d20fb357dbadc7ad74430422e28eb94229721 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 16 Jun 2025 17:55:17 +0200 Subject: [PATCH 008/133] Add custom buttons. --- .../main/java/cloud/katta/controller/DeviceSetupController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osx/src/main/java/cloud/katta/controller/DeviceSetupController.java b/osx/src/main/java/cloud/katta/controller/DeviceSetupController.java index ae099418..fe3a2a29 100644 --- a/osx/src/main/java/cloud/katta/controller/DeviceSetupController.java +++ b/osx/src/main/java/cloud/katta/controller/DeviceSetupController.java @@ -50,6 +50,8 @@ public NSAlert loadAlert() { alert.setInformativeText(new StringAppender() .append(LocaleFactory.localizedString("This is your first login on this device.", "Hub")) .append(LocaleFactory.localizedString("Your Account Key is required to link this browser to your account.", "Hub")).toString()); + alert.addButtonWithTitle(LocaleFactory.localizedString("Finish Setup", "Hub")); + alert.addButtonWithTitle(LocaleFactory.localizedString("Cancel", "Alert")); alert.setShowsSuppressionButton(true); alert.suppressionButton().setTitle(LocaleFactory.localizedString("Add to Keychain", "Login")); alert.suppressionButton().setState(PreferencesFactory.get().getBoolean("cryptomator.vault.keychain") ? NSCell.NSOnState : NSCell.NSOffState); From 2954905de319ada322d795ca05ac05ad9b761bed Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 16 Jun 2025 22:08:40 +0200 Subject: [PATCH 009/133] Remove confirmation input. --- .../katta/controller/FirstLoginController.java | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/osx/src/main/java/cloud/katta/controller/FirstLoginController.java b/osx/src/main/java/cloud/katta/controller/FirstLoginController.java index d9ae5a65..533cab82 100644 --- a/osx/src/main/java/cloud/katta/controller/FirstLoginController.java +++ b/osx/src/main/java/cloud/katta/controller/FirstLoginController.java @@ -34,9 +34,6 @@ public class FirstLoginController extends AlertController { @Outlet private final NSTextField accountKeyField = NSTextField.textFieldWithString(StringUtils.EMPTY); - @Outlet - private final NSTextField accountKeyConfirmField = NSSecureTextField.textFieldWithString(StringUtils.EMPTY); - @Outlet private final NSTextField deviceNameField = NSTextField.textFieldWithString(StringUtils.EMPTY); @@ -73,12 +70,6 @@ public NSView getAccessoryView(final NSAlert alert) { this.addAccessorySubview(accessoryView, accountKeyField); } - { - accountKeyConfirmField.cell().setPlaceholderString(LocaleFactory.localizedString("Confirm Account Key", "Hub")); - accountKeyConfirmField.setToolTip(LocaleFactory.localizedString("I stored my Account Key securely.", "Hub")); - this.addAccessorySubview(accessoryView, accountKeyConfirmField); - } - { this.updateField(deviceNameField, accountKeyAndDeviceName.deviceName()); deviceNameField.cell().setPlaceholderString(LocaleFactory.localizedString("Device Name", "Hub")); @@ -102,9 +93,7 @@ protected void focus(final NSAlert alert) { @Override public boolean validate(final int option) { if(SheetCallback.DEFAULT_OPTION == option) { - if(StringUtils.equals(accountKeyField.stringValue(), accountKeyConfirmField.stringValue())) { - return StringUtils.isNotBlank(deviceNameField.stringValue()); - } + return StringUtils.isNotBlank(deviceNameField.stringValue()); } return true; } From 511de655acd3971062762e06a07f42dd75c2d65f Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 17 Jun 2025 12:51:33 +0200 Subject: [PATCH 010/133] Implement home interface. --- .../cloud/katta/protocols/hub/HubSession.java | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 8cc286b1..8b4eb5a2 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -4,23 +4,12 @@ package cloud.katta.protocols.hub; -import ch.cyberduck.core.Credentials; -import ch.cyberduck.core.DisabledListProgressListener; -import ch.cyberduck.core.Host; -import ch.cyberduck.core.HostKeyCallback; -import ch.cyberduck.core.HostPasswordStore; -import ch.cyberduck.core.ListService; -import ch.cyberduck.core.LocaleFactory; -import ch.cyberduck.core.LoginCallback; -import ch.cyberduck.core.OAuthTokens; -import ch.cyberduck.core.PasswordStoreFactory; -import ch.cyberduck.core.Profile; -import ch.cyberduck.core.ProtocolFactory; -import ch.cyberduck.core.Session; +import ch.cyberduck.core.*; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.exception.ConnectionCanceledException; import ch.cyberduck.core.exception.InteroperabilityException; import ch.cyberduck.core.exception.LoginCanceledException; +import ch.cyberduck.core.features.AttributesFinder; import ch.cyberduck.core.features.Home; import ch.cyberduck.core.features.Scheduler; import ch.cyberduck.core.http.HttpSession; @@ -195,6 +184,12 @@ public T _getFeature(final Class type) { if(type == Scheduler.class) { return (T) access; } + if(type == Home.class) { + return (T) (Home) Home::root; + } + if(type == AttributesFinder.class) { + return (T) (AttributesFinder) (f, l) -> f.attributes(); + } return host.getProtocol().getFeature(type); } } From 67e7a133a3eeffed27322183d1e074348f6ad7c0 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 17 Jun 2025 12:52:39 +0200 Subject: [PATCH 011/133] Fix initialization. --- .../hub/HubGrantAccessSchedulerService.java | 29 +++--- .../HubGrantAccessSchedulerServiceTest.java | 91 ------------------- 2 files changed, 11 insertions(+), 109 deletions(-) delete mode 100644 hub/src/test/java/cloud/katta/protocols/hub/HubGrantAccessSchedulerServiceTest.java diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubGrantAccessSchedulerService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubGrantAccessSchedulerService.java index 3749e5e9..7cf060a2 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubGrantAccessSchedulerService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubGrantAccessSchedulerService.java @@ -11,6 +11,8 @@ import ch.cyberduck.core.preferences.HostPreferences; import ch.cyberduck.core.shared.ThreadPoolSchedulerFeature; +import cloud.katta.client.api.StorageProfileResourceApi; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -25,7 +27,6 @@ import cloud.katta.crypto.UserKeys; import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService; import cloud.katta.workflows.DeviceKeysServiceImpl; -import cloud.katta.workflows.GrantAccessService; import cloud.katta.workflows.GrantAccessServiceImpl; import cloud.katta.workflows.UserKeysServiceImpl; import cloud.katta.workflows.exceptions.AccessException; @@ -36,40 +37,32 @@ public class HubGrantAccessSchedulerService extends ThreadPoolSchedulerFeature accessibleVaults = vaults.apiVaultsAccessibleGet(Role.OWNER); - + final List accessibleVaults = new VaultResourceApi(session.getClient()).apiVaultsAccessibleGet(Role.OWNER); for(final VaultDto accessibleVault : accessibleVaults) { if(Boolean.TRUE.equals(accessibleVault.getArchived())) { log.debug("Skip archived vault {}", accessibleVault); continue; } - service.grantAccessToUsersRequiringAccessGrant(accessibleVault.getId(), userKeys); + new GrantAccessServiceImpl( + new VaultResourceApi(session.getClient()), + new StorageProfileResourceApi(session.getClient()), + new UsersResourceApi(session.getClient()) + ).grantAccessToUsersRequiringAccessGrant(accessibleVault.getId(), userKeys); } userKeys.destroy(); } diff --git a/hub/src/test/java/cloud/katta/protocols/hub/HubGrantAccessSchedulerServiceTest.java b/hub/src/test/java/cloud/katta/protocols/hub/HubGrantAccessSchedulerServiceTest.java deleted file mode 100644 index 71bd7590..00000000 --- a/hub/src/test/java/cloud/katta/protocols/hub/HubGrantAccessSchedulerServiceTest.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) 2025 shift7 GmbH. All rights reserved. - */ - -package cloud.katta.protocols.hub; - -import ch.cyberduck.core.Credentials; -import ch.cyberduck.core.Host; -import ch.cyberduck.core.HostPasswordStore; -import ch.cyberduck.core.exception.BackgroundException; - -import org.joda.time.DateTime; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import java.util.Arrays; -import java.util.Base64; -import java.util.UUID; - -import cloud.katta.client.ApiException; -import cloud.katta.client.HubApiClient; -import cloud.katta.client.api.DeviceResourceApi; -import cloud.katta.client.api.UsersResourceApi; -import cloud.katta.client.api.VaultResourceApi; -import cloud.katta.client.model.DeviceDto; -import cloud.katta.client.model.Role; -import cloud.katta.client.model.Type1; -import cloud.katta.client.model.UserDto; -import cloud.katta.client.model.VaultDto; -import cloud.katta.crypto.DeviceKeys; -import cloud.katta.crypto.UserKeys; -import cloud.katta.protocols.hub.HubGrantAccessSchedulerService; -import cloud.katta.protocols.hub.HubSession; -import cloud.katta.workflows.GrantAccessService; -import cloud.katta.workflows.exceptions.AccessException; -import cloud.katta.workflows.exceptions.SecurityFailure; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.nimbusds.jose.JOSEException; - -import static cloud.katta.crypto.KeyHelper.encodePrivateKey; -import static cloud.katta.crypto.KeyHelper.encodePublicKey; -import static cloud.katta.workflows.DeviceKeysServiceImpl.KEYCHAIN_PRIVATE_DEVICE_KEY_ACCOUNT_NAME; -import static cloud.katta.workflows.DeviceKeysServiceImpl.KEYCHAIN_PUBLIC_DEVICE_KEY_ACCOUNT_NAME; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.times; - -class HubGrantAccessSchedulerServiceTest { - - @Test - public void testOperate() throws BackgroundException, ApiException, JOSEException, JsonProcessingException, AccessException, SecurityFailure { - final HostPasswordStore keychain = Mockito.mock(HostPasswordStore.class); - final HubSession hubSession = Mockito.mock(HubSession.class); - final Host hub = Mockito.mock(Host.class); - final VaultResourceApi vaults = Mockito.mock(VaultResourceApi.class); - final UsersResourceApi users = Mockito.mock(UsersResourceApi.class); - final DeviceResourceApi devices = Mockito.mock(DeviceResourceApi.class); - final GrantAccessService grants = Mockito.mock(GrantAccessService.class); - - final UserKeys userKeys = UserKeys.create(); - final DeviceKeys deviceKeys = DeviceKeys.create(); - - final HubApiClient apiClient = Mockito.mock(HubApiClient.class); - - Mockito.when(hubSession.getHost()).thenReturn(hub); - - Mockito.when(hubSession.getClient()).thenReturn(apiClient); - Mockito.when(keychain.getPassword(eq(KEYCHAIN_PUBLIC_DEVICE_KEY_ACCOUNT_NAME), eq("Fritzl@Unterwittelsbach"))).thenReturn(encodePublicKey(deviceKeys.getEcKeyPair().getPublic())); - Mockito.when(keychain.getPassword(eq(KEYCHAIN_PRIVATE_DEVICE_KEY_ACCOUNT_NAME), eq("Fritzl@Unterwittelsbach"))).thenReturn(encodePrivateKey(deviceKeys.getEcKeyPair().getPrivate())); - Mockito.when(hubSession.getMe()).thenReturn(new UserDto() - .ecdhPublicKey(encodePublicKey(userKeys.ecdhKeyPair().getPublic())) - .ecdsaPublicKey(encodePublicKey(userKeys.ecdsaKeyPair().getPublic())) - ); - Mockito.when(hub.getCredentials()).thenReturn(new Credentials().setUsername("Fritzl")); - Mockito.when(hub.getHostname()).thenReturn("Unterwittelsbach"); - Mockito.when(devices.apiDevicesDeviceIdGet(any())).thenReturn(new DeviceDto() - .name("Franzl") - .publicKey(Base64.getEncoder().encodeToString(deviceKeys.getEcKeyPair().getPublic().getEncoded())) - .userPrivateKey(userKeys.encryptForDevice(deviceKeys.getEcKeyPair().getPublic())) - .type(Type1.DESKTOP) - .creationTime(new DateTime())); - final UUID vaultId = UUID.randomUUID(); - Mockito.when(vaults.apiVaultsAccessibleGet(Role.OWNER)).thenReturn(Arrays.asList( - new VaultDto().archived(true), new VaultDto().archived(false).id(vaultId), new VaultDto().archived(null))); - - final HubGrantAccessSchedulerService service = new HubGrantAccessSchedulerService(hubSession, keychain, vaults, users, devices, grants); - service.operate(null); - - Mockito.verify(grants, times(1)).grantAccessToUsersRequiringAccessGrant(eq(vaultId), any()); - } -} From ac514cfa4e1cbe48d953f3c47336c294c1fcc66b Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 17 Jun 2025 13:01:45 +0200 Subject: [PATCH 012/133] Fix default return value for fallback to parent profile. --- .../cloud/katta/protocols/hub/serializer/ProxyDeserializer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/serializer/ProxyDeserializer.java b/hub/src/main/java/cloud/katta/protocols/hub/serializer/ProxyDeserializer.java index 77a0aac2..980f85fb 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/serializer/ProxyDeserializer.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/serializer/ProxyDeserializer.java @@ -81,7 +81,7 @@ public Map mapForKey(final String key) { @Override public Boolean booleanForKey(final String key) { log.warn("Unknown key {}", key); - return false; + return null; } @Override From a9134461809e2246479b241fcf673b435d1a8da9 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 17 Jun 2025 13:02:03 +0200 Subject: [PATCH 013/133] Extract variable. --- .../cloud/katta/protocols/hub/HubSession.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 8b4eb5a2..6285b97a 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -4,7 +4,20 @@ package cloud.katta.protocols.hub; -import ch.cyberduck.core.*; +import ch.cyberduck.core.Credentials; +import ch.cyberduck.core.DisabledListProgressListener; +import ch.cyberduck.core.Host; +import ch.cyberduck.core.HostKeyCallback; +import ch.cyberduck.core.HostPasswordStore; +import ch.cyberduck.core.ListService; +import ch.cyberduck.core.LocaleFactory; +import ch.cyberduck.core.LoginCallback; +import ch.cyberduck.core.OAuthTokens; +import ch.cyberduck.core.PasswordStoreFactory; +import ch.cyberduck.core.Profile; +import ch.cyberduck.core.Protocol; +import ch.cyberduck.core.ProtocolFactory; +import ch.cyberduck.core.Session; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.exception.ConnectionCanceledException; import ch.cyberduck.core.exception.InteroperabilityException; @@ -85,7 +98,8 @@ public HubVaultRegistry getRegistry() { protected HubApiClient connect(final ProxyFinder proxy, final HostKeyCallback key, final LoginCallback prompt, final CancelCallback cancel) throws BackgroundException { final HttpClientBuilder configuration = builder.build(proxy, this, prompt); - if(host.getProtocol().isBundled()) { + final Protocol bundled = host.getProtocol(); + if(bundled.isBundled()) { // Use REST API for bootstrapping via /api/config final HubApiClient client = new HubApiClient(host, configuration.build()); try { @@ -103,7 +117,7 @@ protected HubApiClient connect(final ProxyFinder proxy, final HostKeyCallback ke final String hubId = configDto.getUuid(); log.debug("Configure bookmark with id {}", hubId); host.setUuid(hubId); - final Profile profile = new Profile(host.getProtocol(), new HubConfigDtoDeserializer(configDto)); + final Profile profile = new Profile(bundled, new HubConfigDtoDeserializer(configDto)); log.debug("Apply profile {} to bookmark {}", profile, host); host.setProtocol(profile); } From 40a390d53b9850e53842f6df0ac10a9a6e0d3b37 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 19 Jun 2025 14:25:38 +0200 Subject: [PATCH 014/133] Explicitly require PKCE. --- hub/src/test/resources/Katta Server.cyberduckprofile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hub/src/test/resources/Katta Server.cyberduckprofile b/hub/src/test/resources/Katta Server.cyberduckprofile index e2739fcb..a3210e9e 100644 --- a/hub/src/test/resources/Katta Server.cyberduckprofile +++ b/hub/src/test/resources/Katta Server.cyberduckprofile @@ -33,6 +33,8 @@ ${oauth.handler.scheme}:oauth OAuth Client Secret + OAuth PKCE + Password Configurable Username Configurable From 30e69808040002b8ea6ced12283029888c47afb7 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 2 Jul 2025 13:04:26 +0200 Subject: [PATCH 015/133] Add password store as constructor parameter. --- .../cloud/katta/protocols/hub/HubSession.java | 2 +- .../katta/workflows/UserKeysServiceImpl.java | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 6285b97a..2123ec3f 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -159,7 +159,7 @@ public void login(final LoginCallback prompt, final CancelCallback cancel) throw try { me = new UsersResourceApi(client).apiUsersMeGet(true, false); log.debug("Retrieved user {}", me); - final UserKeys userKeys = new UserKeysServiceImpl(this).getOrCreateUserKeys(host, me, + final UserKeys userKeys = new UserKeysServiceImpl(this, keychain).getOrCreateUserKeys(host, me, new DeviceKeysServiceImpl(keychain).getOrCreateDeviceKeys(host, setup), setup); log.debug("Retrieved user keys {}", userKeys); // Ensure vaults are registered diff --git a/hub/src/main/java/cloud/katta/workflows/UserKeysServiceImpl.java b/hub/src/main/java/cloud/katta/workflows/UserKeysServiceImpl.java index 29fe758e..5fc09c71 100644 --- a/hub/src/main/java/cloud/katta/workflows/UserKeysServiceImpl.java +++ b/hub/src/main/java/cloud/katta/workflows/UserKeysServiceImpl.java @@ -4,7 +4,9 @@ package cloud.katta.workflows; +import ch.cyberduck.core.BookmarkNameProvider; import ch.cyberduck.core.Host; +import ch.cyberduck.core.PasswordStore; import ch.cyberduck.core.PasswordStoreFactory; import ch.cyberduck.core.exception.LocalAccessDeniedException; @@ -42,13 +44,24 @@ public class UserKeysServiceImpl implements UserKeysService { private final UsersResourceApi usersResourceApi; private final DeviceResourceApi deviceResourceApi; + private final PasswordStore store; + public UserKeysServiceImpl(final HubSession hubSession) { - this(new UsersResourceApi(hubSession.getClient()), new DeviceResourceApi(hubSession.getClient())); + this(hubSession, PasswordStoreFactory.get()); + } + + public UserKeysServiceImpl(final HubSession hubSession, PasswordStore store) { + this(new UsersResourceApi(hubSession.getClient()), new DeviceResourceApi(hubSession.getClient()), store); } public UserKeysServiceImpl(final UsersResourceApi usersResourceApi, final DeviceResourceApi deviceResourceApi) { + this(usersResourceApi, deviceResourceApi, PasswordStoreFactory.get()); + } + + public UserKeysServiceImpl(final UsersResourceApi usersResourceApi, final DeviceResourceApi deviceResourceApi, PasswordStore store) { this.usersResourceApi = usersResourceApi; this.deviceResourceApi = deviceResourceApi; + this.store = store; } @Override @@ -112,7 +125,7 @@ else if(validate(me)) { private void save(final Host hub, final UserDto me, final String accountKey) { try { - PasswordStoreFactory.get().addPassword(hub.getNickname(), me.getEmail(), accountKey); + store.addPassword(BookmarkNameProvider.toString(hub), me.getEmail(), accountKey); } catch(LocalAccessDeniedException ex) { log.warn("Failure saving account key", ex); From 4274e3b1159dd6a4049c2c340d0399c83836b87d Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 11 Jul 2025 11:52:27 +0200 Subject: [PATCH 016/133] Add find feature for vaults. --- .../cloud/katta/protocols/hub/HubSession.java | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 2123ec3f..428f46c9 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -4,25 +4,13 @@ package cloud.katta.protocols.hub; -import ch.cyberduck.core.Credentials; -import ch.cyberduck.core.DisabledListProgressListener; -import ch.cyberduck.core.Host; -import ch.cyberduck.core.HostKeyCallback; -import ch.cyberduck.core.HostPasswordStore; -import ch.cyberduck.core.ListService; -import ch.cyberduck.core.LocaleFactory; -import ch.cyberduck.core.LoginCallback; -import ch.cyberduck.core.OAuthTokens; -import ch.cyberduck.core.PasswordStoreFactory; -import ch.cyberduck.core.Profile; -import ch.cyberduck.core.Protocol; -import ch.cyberduck.core.ProtocolFactory; -import ch.cyberduck.core.Session; +import ch.cyberduck.core.*; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.exception.ConnectionCanceledException; import ch.cyberduck.core.exception.InteroperabilityException; import ch.cyberduck.core.exception.LoginCanceledException; import ch.cyberduck.core.features.AttributesFinder; +import ch.cyberduck.core.features.Find; import ch.cyberduck.core.features.Home; import ch.cyberduck.core.features.Scheduler; import ch.cyberduck.core.http.HttpSession; @@ -204,6 +192,9 @@ public T _getFeature(final Class type) { if(type == AttributesFinder.class) { return (T) (AttributesFinder) (f, l) -> f.attributes(); } + if(type == Find.class) { + return (T) (Find) (file, listener) -> new SimplePathPredicate(registry.find(HubSession.this, file).getHome()).test(file); + } return host.getProtocol().getFeature(type); } } From 7db4b1f52db297cb9175f7de8a6c1efbc9775628 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sat, 12 Jul 2025 18:29:41 +0200 Subject: [PATCH 017/133] Add null check for feature. --- .../main/java/cloud/katta/protocols/hub/HubUVFVault.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 8c9375f7..860cb101 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -48,7 +48,12 @@ public HubUVFVault(final Session storage, final Path home) { public T getFeature(final Session ignore, final Class type, final T delegate) throws UnsupportedException { log.debug("Delegate to {} for feature {}", storage, type); // Ignore feature implementation but delegate to storage backend - return super.getFeature(storage, type, storage._getFeature(type)); + final T feature = storage._getFeature(type); + if(null == feature) { + log.warn("No feature {} available for {}", type, storage); + return null; + } + return super.getFeature(storage, type, feature); } @Override From 50b847dac3ae79f04a7c2a49fab8b1bb40b9430f Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sun, 10 Aug 2025 13:24:09 +0200 Subject: [PATCH 018/133] Add details to exception. --- .../java/cloud/katta/protocols/hub/HubVaultListService.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java index c24490ac..ca0b4b3f 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java @@ -8,6 +8,7 @@ import ch.cyberduck.core.Host; import ch.cyberduck.core.ListProgressListener; import ch.cyberduck.core.ListService; +import ch.cyberduck.core.LocaleFactory; import ch.cyberduck.core.OAuthTokens; import ch.cyberduck.core.Path; import ch.cyberduck.core.PathAttributes; @@ -29,6 +30,7 @@ import org.apache.logging.log4j.Logger; import org.cryptomator.cryptolib.api.UVFMasterkey; +import java.text.MessageFormat; import java.util.EnumSet; import cloud.katta.client.ApiException; @@ -130,6 +132,7 @@ public void preflight(final Path directory) throws BackgroundException { return; } log.warn("Deny directory listing with no vault available for {}", directory); - throw new AccessDeniedException(); + throw new AccessDeniedException(MessageFormat.format(LocaleFactory.localizedString("Listing directory {0} failed", "Error"), + directory.getName())).withFile(directory); } } From acf18562db2f3bb4a1d0a76d7dc40ef8539e6a7b Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 26 Aug 2025 12:25:57 +0200 Subject: [PATCH 019/133] Logging. --- .../java/cloud/katta/protocols/hub/HubVaultRegistry.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultRegistry.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultRegistry.java index 789824a4..6212f1a3 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultRegistry.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultRegistry.java @@ -12,8 +12,11 @@ import ch.cyberduck.core.vault.DefaultVaultRegistry; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; public class HubVaultRegistry extends DefaultVaultRegistry { + private static final Logger log = LogManager.getLogger(HubSession.class); public HubVaultRegistry() { super(new DisabledPasswordCallback()); @@ -25,9 +28,11 @@ public Vault find(final Session session, final Path file, final boolean unlock) if(StringUtils.equals(new DefaultPathContainerService().getContainer(file).getName(), new DefaultPathContainerService().getContainer(vault.getHome()).getName())) { // Return matching vault + log.debug("Found vault {} for file {}", vault, file); return vault; } } + log.warn("No vault found for file {}", file); return Vault.DISABLED; } From 8677628dc07140a0bad7b848587eda623c91379b Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 29 Aug 2025 19:17:32 +0200 Subject: [PATCH 020/133] Fix logger name. --- .../main/java/cloud/katta/protocols/hub/HubVaultRegistry.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultRegistry.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultRegistry.java index 6212f1a3..d9fdea6a 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultRegistry.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultRegistry.java @@ -16,7 +16,7 @@ import org.apache.logging.log4j.Logger; public class HubVaultRegistry extends DefaultVaultRegistry { - private static final Logger log = LogManager.getLogger(HubSession.class); + private static final Logger log = LogManager.getLogger(HubVaultRegistry.class); public HubVaultRegistry() { super(new DisabledPasswordCallback()); From 0015cd7790def6fb209c261e6de9b654167f4994 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 29 Aug 2025 19:29:15 +0200 Subject: [PATCH 021/133] Deny read/write access to root in preflight checks. --- .../cloud/katta/protocols/hub/HubSession.java | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 428f46c9..3c75ecd6 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -5,15 +5,27 @@ package cloud.katta.protocols.hub; import ch.cyberduck.core.*; +import ch.cyberduck.core.exception.AccessDeniedException; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.exception.ConnectionCanceledException; import ch.cyberduck.core.exception.InteroperabilityException; import ch.cyberduck.core.exception.LoginCanceledException; +import ch.cyberduck.core.exception.UnsupportedException; import ch.cyberduck.core.features.AttributesFinder; +import ch.cyberduck.core.features.Copy; +import ch.cyberduck.core.features.Delete; +import ch.cyberduck.core.features.Directory; import ch.cyberduck.core.features.Find; import ch.cyberduck.core.features.Home; +import ch.cyberduck.core.features.Move; +import ch.cyberduck.core.features.Read; import ch.cyberduck.core.features.Scheduler; +import ch.cyberduck.core.features.Timestamp; +import ch.cyberduck.core.features.Touch; +import ch.cyberduck.core.features.Write; import ch.cyberduck.core.http.HttpSession; +import ch.cyberduck.core.io.StatusOutputStream; +import ch.cyberduck.core.io.StreamListener; import ch.cyberduck.core.oauth.OAuth2AuthorizationService; import ch.cyberduck.core.oauth.OAuth2ErrorResponseInterceptor; import ch.cyberduck.core.oauth.OAuth2RequestInterceptor; @@ -22,12 +34,17 @@ import ch.cyberduck.core.ssl.X509KeyManager; import ch.cyberduck.core.ssl.X509TrustManager; import ch.cyberduck.core.threading.CancelCallback; +import ch.cyberduck.core.transfer.TransferStatus; import ch.cyberduck.core.vault.VaultRegistry; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.io.InputStream; +import java.util.Map; +import java.util.Optional; + import cloud.katta.client.ApiException; import cloud.katta.client.HubApiClient; import cloud.katta.client.api.ConfigResourceApi; @@ -195,6 +212,109 @@ public T _getFeature(final Class type) { if(type == Find.class) { return (T) (Find) (file, listener) -> new SimplePathPredicate(registry.find(HubSession.this, file).getHome()).test(file); } + if(type == Read.class) { + return (T) new Read() { + @Override + public InputStream read(final Path file, final TransferStatus status, final ConnectionCallback callback) throws BackgroundException { + log.warn("Deny read access to {}", file); + throw new UnsupportedException().withFile(file); + } + + @Override + public void preflight(final Path file) throws BackgroundException { + throw new UnsupportedException().withFile(file); + } + }; + } + if(type == Write.class) { + return (T) new Write() { + @Override + public StatusOutputStream write(final Path file, final TransferStatus status, final ConnectionCallback callback) throws BackgroundException { + log.warn("Deny write access to {}", file); + throw new UnsupportedException().withFile(file); + } + + @Override + public void preflight(final Path file) throws BackgroundException { + throw new UnsupportedException().withFile(file); + } + }; + } + if(type == Touch.class) { + return (T) new Touch() { + @Override + public Path touch(final Write writer, final Path file, final TransferStatus status) throws BackgroundException { + log.warn("Deny write access to {}", file); + throw new UnsupportedException().withFile(file); + } + + @Override + public void preflight(final Path workdir, final String filename) throws BackgroundException { + throw new UnsupportedException().withFile(workdir); + } + }; + } + if(type == Directory.class) { + return (T) new Directory() { + @Override + public Path mkdir(final Write writer, final Path folder, final TransferStatus status) throws BackgroundException { + log.warn("Deny write access to {}", folder); + throw new UnsupportedException().withFile(folder); + } + + @Override + public void preflight(final Path workdir, final String filename) throws BackgroundException { + throw new UnsupportedException().withFile(workdir); + } + }; + } + if(type == Move.class) { + return (T) new Move() { + @Override + public Path move(final Path source, final Path target, final TransferStatus status, final Delete.Callback delete, final ConnectionCallback prompt) throws BackgroundException { + log.warn("Deny write access to {}", source); + throw new UnsupportedException().withFile(source); + } + + @Override + public void preflight(final Path source, final Optional target) throws BackgroundException { + throw new UnsupportedException().withFile(source); + } + }; + } + if(type == Copy.class) { + return (T) new Copy() { + @Override + public Path copy(final Path source, final Path target, final TransferStatus status, final ConnectionCallback prompt, final StreamListener listener) throws BackgroundException { + log.warn("Deny write access to {}", source); + throw new UnsupportedException().withFile(source); + } + + @Override + public void preflight(final Path source, final Optional target) throws BackgroundException { + throw new UnsupportedException().withFile(source); + } + }; + } + if(type == Delete.class) { + return (T) new Delete() { + @Override + public void delete(final Map files, final PasswordCallback prompt, final Callback callback) throws BackgroundException { + log.warn("Deny write access to {}", files); + throw new UnsupportedException(); + } + + @Override + public void preflight(final Path file) throws BackgroundException { + throw new UnsupportedException().withFile(file); + } + }; + } + if(type == Timestamp.class) { + return (T) (Timestamp) (file, status) -> { + throw new UnsupportedException().withFile(file); + }; + } return host.getProtocol().getFeature(type); } } From 80a9d3c5747feb30876e0bf3fe1a02f3b75839a6 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 29 Aug 2025 19:29:26 +0200 Subject: [PATCH 022/133] Delete redundant override. --- .../katta/protocols/hub/HubVaultRegistry.java | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultRegistry.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultRegistry.java index d9fdea6a..71144729 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultRegistry.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultRegistry.java @@ -4,14 +4,10 @@ package cloud.katta.protocols.hub; -import ch.cyberduck.core.DefaultPathContainerService; import ch.cyberduck.core.DisabledPasswordCallback; -import ch.cyberduck.core.Path; import ch.cyberduck.core.Session; -import ch.cyberduck.core.features.Vault; import ch.cyberduck.core.vault.DefaultVaultRegistry; -import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -22,20 +18,6 @@ public HubVaultRegistry() { super(new DisabledPasswordCallback()); } - @Override - public Vault find(final Session session, final Path file, final boolean unlock) { - for(final Vault vault : this) { - if(StringUtils.equals(new DefaultPathContainerService().getContainer(file).getName(), - new DefaultPathContainerService().getContainer(vault.getHome()).getName())) { - // Return matching vault - log.debug("Found vault {} for file {}", vault, file); - return vault; - } - } - log.warn("No vault found for file {}", file); - return Vault.DISABLED; - } - @Override public T getFeature(Session session, Class type, T proxy) { // Always forward to load feature from vault From 55aebb51eba2e9ffa0250aaed06fb6906f0527a3 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 29 Aug 2025 19:39:12 +0200 Subject: [PATCH 023/133] Set nickname of vault as display name. --- .../main/java/cloud/katta/protocols/hub/HubVaultListService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java index ca0b4b3f..fcb8feab 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java @@ -108,6 +108,7 @@ public AttributedList list(final Path directory, final ListProgressListene try (UVFMasterkey masterKey = UVFMasterkey.fromDecryptedPayload(vaultMetadata.toJSON())) { attr.setDirectoryId(masterKey.rootDirId()); } + attr.setDisplayname(vaultMetadata.storage().getNickname()); vaults.add(new Path(vault.getHome()).withType(EnumSet.of(Path.Type.volume, Path.Type.directory)) .withAttributes(attr)); } From 4716add3c18c950160bd48a6f7edcd3e6b441733 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 29 Aug 2025 19:58:03 +0200 Subject: [PATCH 024/133] Add missing listener notification for chunks retrieved. --- .../main/java/cloud/katta/protocols/hub/HubVaultListService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java index fcb8feab..0adb51b5 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java @@ -111,6 +111,7 @@ public AttributedList list(final Path directory, final ListProgressListene attr.setDisplayname(vaultMetadata.storage().getNickname()); vaults.add(new Path(vault.getHome()).withType(EnumSet.of(Path.Type.volume, Path.Type.directory)) .withAttributes(attr)); + listener.chunk(directory, vaults); } return vaults; } From f958e82801dadbcfa405c9286a6c96df6186b227 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 29 Aug 2025 21:42:10 +0200 Subject: [PATCH 025/133] Remove logger. --- .../java/cloud/katta/protocols/hub/HubVaultRegistry.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultRegistry.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultRegistry.java index 71144729..16a03b64 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultRegistry.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultRegistry.java @@ -8,18 +8,14 @@ import ch.cyberduck.core.Session; import ch.cyberduck.core.vault.DefaultVaultRegistry; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - public class HubVaultRegistry extends DefaultVaultRegistry { - private static final Logger log = LogManager.getLogger(HubVaultRegistry.class); public HubVaultRegistry() { super(new DisabledPasswordCallback()); } @Override - public T getFeature(Session session, Class type, T proxy) { + public T getFeature(final Session session, final Class type, final T proxy) { // Always forward to load feature from vault return this._getFeature(session, type, proxy); } From fce02590824262ed4e9427131ca6fd03cd26a4de Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 29 Aug 2025 21:42:34 +0200 Subject: [PATCH 026/133] Fail with exception when storage does not support feature. --- hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 860cb101..740f3a58 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -51,7 +51,7 @@ public T getFeature(final Session ignore, final Class type, final T de final T feature = storage._getFeature(type); if(null == feature) { log.warn("No feature {} available for {}", type, storage); - return null; + throw new UnsupportedException(); } return super.getFeature(storage, type, feature); } From 0270b44c4206334b2546e6fb4c5e34ed05b5bb05 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 29 Aug 2025 22:06:47 +0200 Subject: [PATCH 027/133] Disable custom versioning option. --- hub/src/main/java/cloud/katta/protocols/hub/HubProtocol.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubProtocol.java b/hub/src/main/java/cloud/katta/protocols/hub/HubProtocol.java index bb1243d2..3710a791 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubProtocol.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubProtocol.java @@ -62,4 +62,9 @@ public boolean isUsernameConfigurable() { public boolean isPasswordConfigurable() { return false; } + + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.storage; + } } From 5ac479025c36fed28a0f2e170f9dfaa35209e189 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sun, 31 Aug 2025 13:45:33 +0200 Subject: [PATCH 028/133] Allow multiple attempts to recover with account key until canceled by user. --- .../cloud/katta/protocols/hub/HubSession.java | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 3c75ecd6..215c9aca 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -5,7 +5,6 @@ package cloud.katta.protocols.hub; import ch.cyberduck.core.*; -import ch.cyberduck.core.exception.AccessDeniedException; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.exception.ConnectionCanceledException; import ch.cyberduck.core.exception.InteroperabilityException; @@ -150,8 +149,6 @@ protected HubApiClient connect(final ProxyFinder proxy, final HostKeyCallback ke @Override public void login(final LoginCallback prompt, final CancelCallback cancel) throws BackgroundException { - final DeviceSetupCallback setup = prompt.getFeature(DeviceSetupCallback.class); - log.debug("Configured with setup prompt {}", setup); final Credentials credentials = authorizationService.validate(); try { // Set username from OAuth ID Token for saving in keychain @@ -164,24 +161,37 @@ public void login(final LoginCallback prompt, final CancelCallback cancel) throw try { me = new UsersResourceApi(client).apiUsersMeGet(true, false); log.debug("Retrieved user {}", me); - final UserKeys userKeys = new UserKeysServiceImpl(this, keychain).getOrCreateUserKeys(host, me, - new DeviceKeysServiceImpl(keychain).getOrCreateDeviceKeys(host, setup), setup); - log.debug("Retrieved user keys {}", userKeys); + // Ensure device key is available + final DeviceSetupCallback setup = prompt.getFeature(DeviceSetupCallback.class); + log.debug("Configured with setup prompt {}", setup); + this.pair(setup); // Ensure vaults are registered final OAuthTokens tokens = new OAuthTokens(credentials.getOauth().getAccessToken(), credentials.getOauth().getRefreshToken(), credentials.getOauth().getExpiryInMilliseconds(), credentials.getOauth().getIdToken()); vaults = new HubVaultListService(protocols, this, trust, key, registry, tokens); vaults.list(Home.root(), new DisabledListProgressListener()); } - catch(SecurityFailure e) { - throw new InteroperabilityException(LocaleFactory.localizedString("Login failed", "Credentials"), e); - } catch(ApiException e) { throw new HubExceptionMappingService().map(e); } + } + + private void pair(final DeviceSetupCallback setup) throws BackgroundException { + try { + final UserKeys userKeys = new UserKeysServiceImpl(this, keychain).getOrCreateUserKeys(host, me, + new DeviceKeysServiceImpl(keychain).getOrCreateDeviceKeys(host, setup), setup); + log.debug("Retrieved user keys {}", userKeys); + } + catch(SecurityFailure e) { + // Repeat until canceled by user + this.pair(setup); + } catch(AccessException e) { throw new ConnectionCanceledException(e); } + catch(ApiException e) { + throw new HubExceptionMappingService().map(e); + } } @Override From 22bb26d60a2e2e604711de616f94855dbe819c37 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sun, 31 Aug 2025 15:39:44 +0200 Subject: [PATCH 029/133] Use default application icon in prompt. --- .../main/java/cloud/katta/controller/DeviceSetupController.java | 1 - .../main/java/cloud/katta/controller/FirstLoginController.java | 1 - 2 files changed, 2 deletions(-) diff --git a/osx/src/main/java/cloud/katta/controller/DeviceSetupController.java b/osx/src/main/java/cloud/katta/controller/DeviceSetupController.java index fe3a2a29..08e996ab 100644 --- a/osx/src/main/java/cloud/katta/controller/DeviceSetupController.java +++ b/osx/src/main/java/cloud/katta/controller/DeviceSetupController.java @@ -44,7 +44,6 @@ public DeviceSetupController(final AccountKeyAndDeviceName accountKeyAndDeviceNa @Override public NSAlert loadAlert() { final NSAlert alert = NSAlert.alert(); - alert.setIcon(IconCacheFactory.get().iconNamed("cryptomator.tiff", 64)); alert.setAlertStyle(NSAlert.NSInformationalAlertStyle); alert.setMessageText(LocaleFactory.localizedString("Authorization Required", "Hub")); alert.setInformativeText(new StringAppender() diff --git a/osx/src/main/java/cloud/katta/controller/FirstLoginController.java b/osx/src/main/java/cloud/katta/controller/FirstLoginController.java index 533cab82..c5d74b99 100644 --- a/osx/src/main/java/cloud/katta/controller/FirstLoginController.java +++ b/osx/src/main/java/cloud/katta/controller/FirstLoginController.java @@ -45,7 +45,6 @@ public FirstLoginController(final AccountKeyAndDeviceName accountKeyAndDeviceNam public NSAlert loadAlert() { final NSAlert alert = NSAlert.alert(); alert.setAlertStyle(NSAlert.NSInformationalAlertStyle); - alert.setIcon(IconCacheFactory.get().iconNamed("cryptomator.tiff", 64)); alert.setMessageText(LocaleFactory.localizedString("Account Key", "Hub")); alert.setInformativeText(new StringAppender() .append(LocaleFactory.localizedString("On first login, every user gets a unique Account Key", "Hub")) From 3ed504208391031536430fad37a5a9badc4e977f Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sun, 31 Aug 2025 15:44:23 +0200 Subject: [PATCH 030/133] Add null check for token expiry. --- .../s3/TokenExchangeRequestInterceptor.java | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 hub/src/main/java/cloud/katta/protocols/s3/TokenExchangeRequestInterceptor.java diff --git a/hub/src/main/java/cloud/katta/protocols/s3/TokenExchangeRequestInterceptor.java b/hub/src/main/java/cloud/katta/protocols/s3/TokenExchangeRequestInterceptor.java new file mode 100644 index 00000000..7678d153 --- /dev/null +++ b/hub/src/main/java/cloud/katta/protocols/s3/TokenExchangeRequestInterceptor.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2025 shift7 GmbH. All rights reserved. + */ + +package cloud.katta.protocols.s3; + +import ch.cyberduck.core.Credentials; +import ch.cyberduck.core.Host; +import ch.cyberduck.core.LoginCallback; +import ch.cyberduck.core.OAuthTokens; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.exception.LoginCanceledException; +import ch.cyberduck.core.exception.LoginFailureException; +import ch.cyberduck.core.oauth.OAuth2RequestInterceptor; +import ch.cyberduck.core.preferences.HostPreferences; +import ch.cyberduck.core.preferences.PreferencesReader; + +import org.apache.http.client.HttpClient; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Arrays; +import java.util.List; + +import cloud.katta.client.ApiException; +import cloud.katta.client.api.StorageResourceApi; +import cloud.katta.client.model.AccessTokenResponse; +import cloud.katta.protocols.hub.HubSession; +import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService; +import com.auth0.jwt.JWT; +import com.auth0.jwt.exceptions.JWTDecodeException; +import com.auth0.jwt.interfaces.DecodedJWT; + +/** + * Exchange OIDC token to scoped token using OAuth 2.0 Token Exchange. Used for S3-STS in Katta. + */ +public class TokenExchangeRequestInterceptor extends OAuth2RequestInterceptor { + private static final Logger log = LogManager.getLogger(TokenExchangeRequestInterceptor.class); + + /** + * The party to which the ID Token was issued + * ... + */ + public static final String OIDC_AUTHORIZED_PARTY = "azp"; + + private final Host bookmark; + + public TokenExchangeRequestInterceptor(final HttpClient client, final Host bookmark, final LoginCallback prompt) throws LoginCanceledException { + super(client, bookmark, prompt); + this.bookmark = bookmark; + } + + @Override + public OAuthTokens authorize() throws BackgroundException { + return this.exchange(super.authorize()); + } + + @Override + public OAuthTokens refresh(final OAuthTokens previous) throws BackgroundException { + return this.exchange(super.refresh(previous)); + } + + /** + * Perform OAuth 2.0 Token Exchange + * + * @param previous Input tokens retrieved to exchange at the token endpoint + * @return New tokens + * @see S3AssumeRoleProtocol#OAUTH_TOKENEXCHANGE_VAULT + * @see S3AssumeRoleProtocol#OAUTH_TOKENEXCHANGE_BASEPATH + */ + public OAuthTokens exchange(final OAuthTokens previous) throws BackgroundException { + log.info("Exchange tokens {} for {}", previous, bookmark); + final PreferencesReader preferences = new HostPreferences(bookmark); + final HubSession hub = bookmark.getProtocol().getFeature(HubSession.class); + log.debug("Exchange token with hub {}", hub); + final StorageResourceApi api = new StorageResourceApi(hub.getClient()); + try { + final AccessTokenResponse tokenExchangeResponse = api.apiStorageS3TokenPost(preferences.getProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_VAULT)); + // N.B. token exchange with Id token does not work! + final OAuthTokens tokens = new OAuthTokens(tokenExchangeResponse.getAccessToken(), + tokenExchangeResponse.getRefreshToken(), + tokenExchangeResponse.getExpiresIn() != null ? System.currentTimeMillis() + tokenExchangeResponse.getExpiresIn() * 1000 : null); + log.debug("Received exchanged token {} for {}", tokens, bookmark); + return tokens; + } + catch(ApiException e) { + throw new HubExceptionMappingService().map(e); + } + } + + @Override + public Credentials validate() throws BackgroundException { + final Credentials credentials = super.validate(); + final OAuthTokens tokens = credentials.getOauth(); + final String accessToken = tokens.getAccessToken(); + try { + final DecodedJWT jwt = JWT.decode(accessToken); + + final List auds = jwt.getAudience(); + final String azp = jwt.getClaim(OIDC_AUTHORIZED_PARTY).asString(); + + final boolean audNotUnique = 1 != auds.size(); // either multiple audiences or none + // do exchange if aud is not unique or azp is not equal to aud + if(audNotUnique || !auds.get(0).equals(azp)) { + log.debug("None or multiple audiences found {} or audience differs from azp {}, triggering token-exchange.", Arrays.toString(auds.toArray()), azp); + return credentials.withOauth(this.exchange(tokens)); + } + } + catch(JWTDecodeException e) { + throw new LoginFailureException("Invalid JWT or JSON format in authentication token", e); + } + return credentials; + } +} From f5764a00c5abfb50be0b317fbab223ed26b7b90d Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 1 Sep 2025 08:51:42 +0200 Subject: [PATCH 031/133] Standard wrapping for error handler to limit retries. --- hub/src/main/java/cloud/katta/protocols/hub/HubSession.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 215c9aca..535fb267 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -22,6 +22,8 @@ import ch.cyberduck.core.features.Timestamp; import ch.cyberduck.core.features.Touch; import ch.cyberduck.core.features.Write; +import ch.cyberduck.core.http.CustomServiceUnavailableRetryStrategy; +import ch.cyberduck.core.http.ExecutionCountServiceUnavailableRetryStrategy; import ch.cyberduck.core.http.HttpSession; import ch.cyberduck.core.io.StatusOutputStream; import ch.cyberduck.core.io.StreamListener; @@ -142,7 +144,8 @@ protected HubApiClient connect(final ProxyFinder proxy, final HostKeyCallback ke host.getProtocol().isOAuthPKCE(), prompt) .withFlowType(OAuth2AuthorizationService.FlowType.valueOf(host.getProtocol().getAuthorization())) .withRedirectUri(host.getProtocol().getOAuthRedirectUrl()); - configuration.setServiceUnavailableRetryStrategy(new OAuth2ErrorResponseInterceptor(host, authorizationService)); + configuration.setServiceUnavailableRetryStrategy(new CustomServiceUnavailableRetryStrategy(host, + new ExecutionCountServiceUnavailableRetryStrategy(new OAuth2ErrorResponseInterceptor(host, authorizationService)))); configuration.addInterceptorLast(authorizationService); return new HubApiClient(host, configuration.build()); } From 23897233b541c6ae90dca1cdc8cfdaf0166f60b2 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 1 Sep 2025 13:59:19 +0200 Subject: [PATCH 032/133] Remove redundant override of registry. --- .../main/java/cloud/katta/protocols/hub/HubSession.java | 9 +-------- .../cloud/katta/core/AbstractHubSynchronizeTest.java | 4 +++- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 535fb267..f7846953 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -77,8 +77,6 @@ public class HubSession extends HttpSession { */ private final Scheduler access = new HubGrantAccessSchedulerService(this, keychain); - private final HubVaultRegistry registry = new HubVaultRegistry(); - private HubVaultListService vaults; /** @@ -91,12 +89,7 @@ public HubSession(final Host host, final X509TrustManager trust, final X509KeyMa super(host, trust, key); } - @Override - public Session withRegistry(final VaultRegistry ignored) { - return super.withRegistry(registry); - } - - public HubVaultRegistry getRegistry() { + public VaultRegistry getRegistry() { return registry; } diff --git a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java index 4d19858a..93e763f2 100644 --- a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java +++ b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java @@ -24,6 +24,7 @@ import ch.cyberduck.core.transfer.Transfer; import ch.cyberduck.core.transfer.TransferItem; import ch.cyberduck.core.transfer.TransferStatus; +import ch.cyberduck.core.vault.VaultRegistry; import ch.cyberduck.core.worker.DeleteWorker; import org.apache.commons.io.IOUtils; @@ -265,7 +266,8 @@ public void test03AddVault(final HubTestConfig config) throws Exception { final Path bucket = new Path(storageProfileWrapper.getProtocol() == Protocol.S3_STS ? storageProfileWrapper.getBucketPrefix() + vaultId : config.vault.bucketName, EnumSet.of(Path.Type.volume, Path.Type.directory)); - final HubVaultRegistry vaultRegistry = hubSession.getRegistry(); + final VaultRegistry vaultRegistry = hubSession.getRegistry(); + assertTrue(vaultRegistry instanceof HubVaultRegistry); { assertNotNull(vaults.find(new SimplePathPredicate(bucket))); From f87fd8aabaf929e0bfb63d02b59d78e318493bb6 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 1 Sep 2025 14:07:48 +0200 Subject: [PATCH 033/133] Use storage specific comparison service. --- .../cloud/katta/protocols/hub/HubSession.java | 36 +++++++++++++++++++ .../katta/protocols/hub/HubUVFVault.java | 4 +++ 2 files changed, 40 insertions(+) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index f7846953..a1b1b349 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -34,9 +34,12 @@ import ch.cyberduck.core.proxy.ProxyFinder; import ch.cyberduck.core.ssl.X509KeyManager; import ch.cyberduck.core.ssl.X509TrustManager; +import ch.cyberduck.core.synchronization.Comparison; +import ch.cyberduck.core.synchronization.ComparisonService; import ch.cyberduck.core.threading.CancelCallback; import ch.cyberduck.core.transfer.TransferStatus; import ch.cyberduck.core.vault.VaultRegistry; +import ch.cyberduck.core.vault.VaultUnlockCancelException; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; @@ -321,6 +324,39 @@ public void preflight(final Path file) throws BackgroundException { throw new UnsupportedException().withFile(file); }; } + if(type == ComparisonService.class) { + return (T) new ComparisonService() { + @Override + public Comparison compare(final Path.Type type, final PathAttributes local, final PathAttributes remote) { + try { + final ComparisonService feature = this.getFeature(remote.getVault()); + return feature.compare(type, local, remote); + } + catch(VaultUnlockCancelException e) { + return Comparison.unknown; + } + } + + @Override + public int hashCode(final Path.Type type, final PathAttributes attr) { + try { + final ComparisonService feature = this.getFeature(attr.getVault()); + return feature.hashCode(type, attr); + } + catch(VaultUnlockCancelException e) { + return 0; + } + } + + private ComparisonService getFeature(final Path vault) throws VaultUnlockCancelException { + if(null == vault) { + return ComparisonService.disabled; + } + final HubUVFVault cryptomator = (HubUVFVault) registry.find(HubSession.this, vault); + return cryptomator.getStorage().getFeature(ComparisonService.class); + } + }; + } return host.getProtocol().getFeature(type); } } diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 740f3a58..6c420f16 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -44,6 +44,10 @@ public HubUVFVault(final Session storage, final Path home) { this.home = home; } + public Session getStorage() { + return storage; + } + @Override public T getFeature(final Session ignore, final Class type, final T delegate) throws UnsupportedException { log.debug("Delegate to {} for feature {}", storage, type); From d131a87234553eefdb047d4261ff756f7a446f30 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 1 Sep 2025 14:11:35 +0200 Subject: [PATCH 034/133] Extract class. --- .../cloud/katta/protocols/hub/HubSession.java | 35 +------------ ...HubVaultStorageAwareComparisonService.java | 50 +++++++++++++++++++ 2 files changed, 52 insertions(+), 33 deletions(-) create mode 100644 hub/src/main/java/cloud/katta/protocols/hub/HubVaultStorageAwareComparisonService.java diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index a1b1b349..8a6b2bb4 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -34,12 +34,10 @@ import ch.cyberduck.core.proxy.ProxyFinder; import ch.cyberduck.core.ssl.X509KeyManager; import ch.cyberduck.core.ssl.X509TrustManager; -import ch.cyberduck.core.synchronization.Comparison; import ch.cyberduck.core.synchronization.ComparisonService; import ch.cyberduck.core.threading.CancelCallback; import ch.cyberduck.core.transfer.TransferStatus; import ch.cyberduck.core.vault.VaultRegistry; -import ch.cyberduck.core.vault.VaultUnlockCancelException; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; @@ -325,38 +323,9 @@ public void preflight(final Path file) throws BackgroundException { }; } if(type == ComparisonService.class) { - return (T) new ComparisonService() { - @Override - public Comparison compare(final Path.Type type, final PathAttributes local, final PathAttributes remote) { - try { - final ComparisonService feature = this.getFeature(remote.getVault()); - return feature.compare(type, local, remote); - } - catch(VaultUnlockCancelException e) { - return Comparison.unknown; - } - } - - @Override - public int hashCode(final Path.Type type, final PathAttributes attr) { - try { - final ComparisonService feature = this.getFeature(attr.getVault()); - return feature.hashCode(type, attr); - } - catch(VaultUnlockCancelException e) { - return 0; - } - } - - private ComparisonService getFeature(final Path vault) throws VaultUnlockCancelException { - if(null == vault) { - return ComparisonService.disabled; - } - final HubUVFVault cryptomator = (HubUVFVault) registry.find(HubSession.this, vault); - return cryptomator.getStorage().getFeature(ComparisonService.class); - } - }; + return (T) new HubVaultStorageAwareComparisonService(this); } return host.getProtocol().getFeature(type); } + } diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultStorageAwareComparisonService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultStorageAwareComparisonService.java new file mode 100644 index 00000000..c26521c8 --- /dev/null +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultStorageAwareComparisonService.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025 shift7 GmbH. All rights reserved. + */ + +package cloud.katta.protocols.hub; + +import ch.cyberduck.core.Path; +import ch.cyberduck.core.PathAttributes; +import ch.cyberduck.core.synchronization.Comparison; +import ch.cyberduck.core.synchronization.ComparisonService; +import ch.cyberduck.core.vault.VaultUnlockCancelException; + +public class HubVaultStorageAwareComparisonService implements ComparisonService { + + private final HubSession session; + + public HubVaultStorageAwareComparisonService(final HubSession session) { + this.session = session; + } + + @Override + public Comparison compare(final Path.Type type, final PathAttributes local, final PathAttributes remote) { + try { + final ComparisonService feature = this.getFeature(remote.getVault()); + return feature.compare(type, local, remote); + } + catch(VaultUnlockCancelException e) { + return Comparison.unknown; + } + } + + @Override + public int hashCode(final Path.Type type, final PathAttributes attr) { + try { + final ComparisonService feature = this.getFeature(attr.getVault()); + return feature.hashCode(type, attr); + } + catch(VaultUnlockCancelException e) { + return 0; + } + } + + private ComparisonService getFeature(final Path vault) throws VaultUnlockCancelException { + if(null == vault) { + return ComparisonService.disabled; + } + final HubUVFVault cryptomator = (HubUVFVault) session.getRegistry().find(session, vault); + return cryptomator.getStorage().getFeature(ComparisonService.class); + } +} From 51c2956978b47d7025bd4a3304e86e28847704c4 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 1 Sep 2025 14:18:08 +0200 Subject: [PATCH 035/133] Prune imports. --- .../java/cloud/katta/controller/DeviceSetupController.java | 2 -- .../main/java/cloud/katta/controller/FirstLoginController.java | 3 --- 2 files changed, 5 deletions(-) diff --git a/osx/src/main/java/cloud/katta/controller/DeviceSetupController.java b/osx/src/main/java/cloud/katta/controller/DeviceSetupController.java index 08e996ab..1be3bdb8 100644 --- a/osx/src/main/java/cloud/katta/controller/DeviceSetupController.java +++ b/osx/src/main/java/cloud/katta/controller/DeviceSetupController.java @@ -10,7 +10,6 @@ import ch.cyberduck.binding.application.NSAlert; import ch.cyberduck.binding.application.NSCell; import ch.cyberduck.binding.application.NSControl; -import ch.cyberduck.binding.application.NSImage; import ch.cyberduck.binding.application.NSSecureTextField; import ch.cyberduck.binding.application.NSTextField; import ch.cyberduck.binding.application.NSView; @@ -20,7 +19,6 @@ import ch.cyberduck.core.LocaleFactory; import ch.cyberduck.core.StringAppender; import ch.cyberduck.core.preferences.PreferencesFactory; -import ch.cyberduck.core.resources.IconCacheFactory; import org.apache.commons.lang3.StringUtils; import org.rococoa.Foundation; diff --git a/osx/src/main/java/cloud/katta/controller/FirstLoginController.java b/osx/src/main/java/cloud/katta/controller/FirstLoginController.java index c5d74b99..0c3dad6d 100644 --- a/osx/src/main/java/cloud/katta/controller/FirstLoginController.java +++ b/osx/src/main/java/cloud/katta/controller/FirstLoginController.java @@ -10,8 +10,6 @@ import ch.cyberduck.binding.application.NSAlert; import ch.cyberduck.binding.application.NSCell; import ch.cyberduck.binding.application.NSControl; -import ch.cyberduck.binding.application.NSImage; -import ch.cyberduck.binding.application.NSSecureTextField; import ch.cyberduck.binding.application.NSTextField; import ch.cyberduck.binding.application.NSView; import ch.cyberduck.binding.application.SheetCallback; @@ -20,7 +18,6 @@ import ch.cyberduck.core.LocaleFactory; import ch.cyberduck.core.StringAppender; import ch.cyberduck.core.preferences.PreferencesFactory; -import ch.cyberduck.core.resources.IconCacheFactory; import org.apache.commons.lang3.StringUtils; import org.rococoa.Foundation; From f1915213e2546818bfa0e8adf087213a3bbc15f9 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 12 May 2025 16:49:36 +0200 Subject: [PATCH 036/133] Return storage profile name as default nickname. --- .../hub/serializer/StorageProfileDtoWrapperDeserializer.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java b/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java index c85602e4..a526a428 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java @@ -81,6 +81,8 @@ public String stringForKey(final String key) { break; case VENDOR_KEY: return dto.getId().toString(); + case DEFAULT_NICKNAME_KEY: + return dto.getName(); case SCHEME_KEY: return dto.getScheme(); case DEFAULT_HOSTNAME_KEY: @@ -119,6 +121,9 @@ public List keys() { PROPERTIES_KEY, OAUTH_CONFIGURABLE_KEY) ); + if(dto.getName() != null) { + keys.add(DEFAULT_NICKNAME_KEY); + } if(dto.getScheme() != null) { keys.add(SCHEME_KEY); } From d6e9109feb09376a02e9b2136ec751fab64abbe9 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 2 Sep 2025 14:01:31 +0200 Subject: [PATCH 037/133] Must not cache storage profile with reference to Hub connection. --- .../katta/workflows/VaultServiceImpl.java | 42 ++++++++----------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java b/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java index 26b85fd7..f234316e 100644 --- a/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java +++ b/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java @@ -85,31 +85,23 @@ public UvfAccessTokenPayload getVaultAccessTokenJWE(final UUID vaultId, final Us @Override public Host getStorageBackend(final ProtocolFactory protocols, final HubSession hub, final ConfigDto configDto, final UUID vaultId, final VaultMetadataJWEBackendDto vaultMetadata, final OAuthTokens tokens) throws ApiException, AccessException { - if(null == protocols.forName(vaultMetadata.getProvider())) { - log.debug("Load missing profile {}", vaultMetadata.getProvider()); - final StorageProfileDtoWrapper storageProfile = StorageProfileDtoWrapper.coerce(storageProfileResourceApi - .apiStorageprofileProfileIdGet(UUID.fromString(vaultMetadata.getProvider()))); - log.debug("Read storage profile {}", storageProfile); - switch(storageProfile.getProtocol()) { - case S3: - case S3_STS: - final Profile profile = new HubAwareProfile(hub, protocols.forType(protocols.find(ProtocolFactory.BUNDLED_PROFILE_PREDICATE), Type.s3), - configDto, storageProfile); - log.debug("Register storage profile {}", profile); - protocols.register(profile); - break; - default: - throw new AccessException(String.format("Unsupported storage configuration %s", storageProfile.getProtocol().name())); - } - } - final String provider = vaultMetadata.getProvider(); - log.debug("Lookup provider {} from vault metadata", provider); - final Protocol protocol = protocols.forName(provider); - if((protocol.getOAuthTokenUrl() != null) && (!protocol.getOAuthTokenUrl().equals(configDto.getKeycloakTokenEndpoint()))) { - // this may happen if the storage profile ID is deployed to two different hubs - throw new AccessException(String.format("Expected keycloak endpoint %s, found %s.", configDto.getKeycloakTokenEndpoint(), protocol.getOAuthTokenUrl())); + log.debug("Load profile {}", vaultMetadata.getProvider()); + final StorageProfileDtoWrapper storageProfile = StorageProfileDtoWrapper.coerce(storageProfileResourceApi + .apiStorageprofileProfileIdGet(UUID.fromString(vaultMetadata.getProvider()))); + log.debug("Read storage profile {}", storageProfile); + final Profile profile; + switch(storageProfile.getProtocol()) { + case S3: + case S3_STS: + profile = new HubAwareProfile(hub, protocols.forType(protocols.find(ProtocolFactory.BUNDLED_PROFILE_PREDICATE), Type.s3), + configDto, storageProfile); + log.debug("Loaded profile {}", profile); + protocols.register(profile); + break; + default: + throw new AccessException(String.format("Unsupported storage configuration %s", storageProfile.getProtocol().name())); } - final Host bookmark = new Host(protocol); + final Host bookmark = new Host(profile); log.debug("Configure bookmark for vault {}", vaultMetadata); bookmark.setNickname(vaultMetadata.getNickname()); bookmark.setDefaultPath(vaultMetadata.getDefaultPath()); @@ -121,7 +113,7 @@ public Host getStorageBackend(final ProtocolFactory protocols, final HubSession if(vaultMetadata.getPassword() != null) { credentials.setPassword(vaultMetadata.getPassword()); } - if(protocol.getProperties().get(S3_ASSUMEROLE_ROLEARN) != null) { + if(profile.getProperties().get(S3_ASSUMEROLE_ROLEARN) != null) { bookmark.setProperty(OAUTH_TOKENEXCHANGE_VAULT, vaultId.toString()); bookmark.setProperty(OAUTH_TOKENEXCHANGE_BASEPATH, this.vaultResource.getApiClient().getBasePath()); } From 8a5ebcea21c990fe47830d53025888d6f8cd209d Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 2 Sep 2025 16:50:42 +0200 Subject: [PATCH 038/133] No need to register profile. --- hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java | 1 - 1 file changed, 1 deletion(-) diff --git a/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java b/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java index f234316e..affee7a1 100644 --- a/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java +++ b/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java @@ -96,7 +96,6 @@ public Host getStorageBackend(final ProtocolFactory protocols, final HubSession profile = new HubAwareProfile(hub, protocols.forType(protocols.find(ProtocolFactory.BUNDLED_PROFILE_PREDICATE), Type.s3), configDto, storageProfile); log.debug("Loaded profile {}", profile); - protocols.register(profile); break; default: throw new AccessException(String.format("Unsupported storage configuration %s", storageProfile.getProtocol().name())); From 5412f4f56f80db9b1fe761534a3f735fe44c96ec Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 2 Sep 2025 16:51:47 +0200 Subject: [PATCH 039/133] Reduce required parameters. --- .../cloud/katta/protocols/hub/HubSession.java | 6 ++---- .../protocols/hub/HubVaultListService.java | 19 ++++++------------- .../katta/workflows/CreateVaultService.java | 2 +- .../cloud/katta/workflows/VaultService.java | 12 +++++------- .../katta/workflows/VaultServiceImpl.java | 3 ++- .../core/AbstractHubSynchronizeTest.java | 1 - 6 files changed, 16 insertions(+), 27 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 8a6b2bb4..16745e79 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -71,7 +71,6 @@ public class HubSession extends HttpSession { private static final Logger log = LogManager.getLogger(HubSession.class); private final HostPasswordStore keychain = PasswordStoreFactory.get(); - private final ProtocolFactory protocols = ProtocolFactory.get(); /** * Periodically grant vault access to users @@ -165,7 +164,7 @@ public void login(final LoginCallback prompt, final CancelCallback cancel) throw // Ensure vaults are registered final OAuthTokens tokens = new OAuthTokens(credentials.getOauth().getAccessToken(), credentials.getOauth().getRefreshToken(), credentials.getOauth().getExpiryInMilliseconds(), credentials.getOauth().getIdToken()); - vaults = new HubVaultListService(protocols, this, trust, key, registry, tokens); + vaults = new HubVaultListService(this, tokens); vaults.list(Home.root(), new DisabledListProgressListener()); } catch(ApiException e) { @@ -325,7 +324,6 @@ public void preflight(final Path file) throws BackgroundException { if(type == ComparisonService.class) { return (T) new HubVaultStorageAwareComparisonService(this); } - return host.getProtocol().getFeature(type); + return super._getFeature(type); } - } diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java index 0adb51b5..bb17ac3c 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java @@ -53,20 +53,11 @@ public class HubVaultListService implements ListService { private static final Logger log = LogManager.getLogger(HubVaultListService.class); - private final ProtocolFactory protocols; private final HubSession session; - private final X509TrustManager trust; - private final X509KeyManager key; - private final VaultRegistry registry; private final OAuthTokens tokens; - public HubVaultListService(final ProtocolFactory protocols, final HubSession session, - final X509TrustManager trust, final X509KeyManager key, final VaultRegistry registry, final OAuthTokens tokens) { - this.protocols = protocols; + public HubVaultListService(final HubSession session, final OAuthTokens tokens) { this.session = session; - this.trust = trust; - this.key = key; - this.registry = registry; this.tokens = tokens; } @@ -98,11 +89,13 @@ public AttributedList list(final Path directory, final ListProgressListene } throw e; } - final Host bookmark = vaultService.getStorageBackend(protocols, session, configDto, vaultDto.getId(), + final Host bookmark = vaultService.getStorageBackend(session, configDto, vaultDto.getId(), vaultMetadata.storage(), tokens); log.debug("Configured {} for vault {}", bookmark, vaultDto); - final Session storage = SessionFactory.create(bookmark, trust, key); + final Session storage = SessionFactory.create(bookmark, + session.getFeature(X509TrustManager.class), session.getFeature(X509KeyManager.class)); final HubUVFVault vault = new HubUVFVault(storage, new DefaultPathHomeFeature(bookmark).find()); + final VaultRegistry registry = session.getRegistry(); registry.add(vault.load(session, new UvfMetadataPayloadPasswordCallback(vaultMetadata))); final PathAttributes attr = storage.getFeature(AttributesFinder.class).find(vault.getHome()); try (UVFMasterkey masterKey = UVFMasterkey.fromDecryptedPayload(vaultMetadata.toJSON())) { @@ -130,7 +123,7 @@ public void preflight(final Path directory) throws BackgroundException { if(directory.isRoot()) { return; } - if(registry.contains(directory)) { + if(session.getRegistry().contains(directory)) { return; } log.warn("Deny directory listing with no vault available for {}", directory); diff --git a/hub/src/main/java/cloud/katta/workflows/CreateVaultService.java b/hub/src/main/java/cloud/katta/workflows/CreateVaultService.java index 7098184f..49791a58 100644 --- a/hub/src/main/java/cloud/katta/workflows/CreateVaultService.java +++ b/hub/src/main/java/cloud/katta/workflows/CreateVaultService.java @@ -141,7 +141,7 @@ public void createVault(final UserKeys userKeys, final StorageProfileDtoWrapper final HostPasswordStore keychain = PasswordStoreFactory.get(); final OAuthTokens tokens = keychain.findOAuthTokens(hubSession.getHost()); - final Host bookmark = new VaultServiceImpl(vaultResource, storageProfileResource).getStorageBackend(ProtocolFactory.get(), + final Host bookmark = new VaultServiceImpl(vaultResource, storageProfileResource).getStorageBackend( hubSession, configResource.apiConfigGet(), vaultDto.getId(), metadataPayload.storage(), tokens); if(storageProfileWrapper.getProtocol() == Protocol.S3) { // permanent: template upload into existing bucket from client (not backend) diff --git a/hub/src/main/java/cloud/katta/workflows/VaultService.java b/hub/src/main/java/cloud/katta/workflows/VaultService.java index 8966d66d..98a206aa 100644 --- a/hub/src/main/java/cloud/katta/workflows/VaultService.java +++ b/hub/src/main/java/cloud/katta/workflows/VaultService.java @@ -6,7 +6,6 @@ import ch.cyberduck.core.Host; import ch.cyberduck.core.OAuthTokens; -import ch.cyberduck.core.ProtocolFactory; import java.util.UUID; @@ -48,14 +47,13 @@ public interface VaultService { /** * Prepares (virtual) bookmark for vault to access its configured storage backend. * - * @param protocols Registered protocol implementations to access backend storage - * @param hub Hub API Connection - * @param configDto Hub configuration - * @param vaultId Vault ID - * @param metadata Storage Backend configuration + * @param hub Hub API Connection + * @param configDto Hub configuration + * @param vaultId Vault ID + * @param vaultMetadata Storage Backend configuration * @return Configuration * @throws AccessException Unsupported backend storage protocol * @throws ApiException Server error accessing storage profile */ - Host getStorageBackend(final ProtocolFactory protocols, final HubSession hub, final ConfigDto configDto, UUID vaultId, VaultMetadataJWEBackendDto metadata, final OAuthTokens tokens) throws AccessException, ApiException; + Host getStorageBackend(final HubSession hub, final ConfigDto configDto, UUID vaultId, VaultMetadataJWEBackendDto vaultMetadata, final OAuthTokens tokens) throws AccessException, ApiException; } diff --git a/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java b/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java index affee7a1..085196b6 100644 --- a/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java +++ b/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java @@ -84,7 +84,7 @@ public UvfAccessTokenPayload getVaultAccessTokenJWE(final UUID vaultId, final Us } @Override - public Host getStorageBackend(final ProtocolFactory protocols, final HubSession hub, final ConfigDto configDto, final UUID vaultId, final VaultMetadataJWEBackendDto vaultMetadata, final OAuthTokens tokens) throws ApiException, AccessException { + public Host getStorageBackend(final HubSession hub, final ConfigDto configDto, final UUID vaultId, final VaultMetadataJWEBackendDto vaultMetadata, final OAuthTokens tokens) throws ApiException, AccessException { log.debug("Load profile {}", vaultMetadata.getProvider()); final StorageProfileDtoWrapper storageProfile = StorageProfileDtoWrapper.coerce(storageProfileResourceApi .apiStorageprofileProfileIdGet(UUID.fromString(vaultMetadata.getProvider()))); @@ -93,6 +93,7 @@ public Host getStorageBackend(final ProtocolFactory protocols, final HubSession switch(storageProfile.getProtocol()) { case S3: case S3_STS: + final ProtocolFactory protocols = ProtocolFactory.get(); profile = new HubAwareProfile(hub, protocols.forType(protocols.find(ProtocolFactory.BUNDLED_PROFILE_PREDICATE), Type.s3), configDto, storageProfile); log.debug("Loaded profile {}", profile); diff --git a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java index 93e763f2..e0e6cc89 100644 --- a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java +++ b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java @@ -238,7 +238,6 @@ public void test03AddVault(final HubTestConfig config) throws Exception { final OAuthTokens tokens = keychain.findOAuthTokens(hubSession.getHost()); final Host bookmark = new VaultServiceImpl(new VaultResourceApi(hubSession.getClient()), new StorageProfileResourceApi(hubSession.getClient())) .getStorageBackend( - ProtocolFactory.get(), hubSession, new ConfigResourceApi(hubSession.getClient()).apiConfigGet(), vaultId, new VaultMetadataJWEBackendDto() .provider(storageProfileWrapper.getId().toString()) From 2a57408c9309859292eede0d6998d5c74e0eab02 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 2 Sep 2025 16:53:30 +0200 Subject: [PATCH 040/133] No custom cleanup. --- .../core/AbstractHubSynchronizeTest.java | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java index e0e6cc89..630cd756 100644 --- a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java +++ b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java @@ -230,30 +230,6 @@ public void test03AddVault(final HubTestConfig config) throws Exception { log.info("Creating vault in {}", hubSession); final UUID vaultId = UUID.randomUUID(); - - if(storageProfileWrapper.getProtocol() == Protocol.S3) { - // empty bucket - final HostPasswordStore keychain = PasswordStoreFactory.get(); - - final OAuthTokens tokens = keychain.findOAuthTokens(hubSession.getHost()); - final Host bookmark = new VaultServiceImpl(new VaultResourceApi(hubSession.getClient()), new StorageProfileResourceApi(hubSession.getClient())) - .getStorageBackend( - hubSession, - new ConfigResourceApi(hubSession.getClient()).apiConfigGet(), vaultId, new VaultMetadataJWEBackendDto() - .provider(storageProfileWrapper.getId().toString()) - .defaultPath(config.vault.bucketName) - .nickname(config.vault.bucketName) - .username(config.vault.username) - .password(config.vault.password), tokens); - final S3Session session = new S3Session(bookmark); - session.open(new DisabledProxyFinder(), new DisabledHostKeyCallback(), new DisabledLoginCallback(), new DisabledCancelCallback()); - session.login(new DisabledLoginCallback(), new DisabledCancelCallback()); - new DeleteWorker(new DisabledLoginCallback(), - session.getFeature(ListService.class).list(new Path("/" + config.vault.bucketName, EnumSet.of(AbstractPath.Type.directory)), new DisabledListProgressListener()).toStream().filter(f -> session.getFeature(Delete.class).isSupported(f)).collect(Collectors.toList()), - new DisabledListProgressListener()).run(session); - session.close(); - } - final UserKeys userKeys = new UserKeysServiceImpl(hubSession).getUserKeys(hubSession.getHost(), hubSession.getMe(), new DeviceKeysServiceImpl().getDeviceKeys(hubSession.getHost())); new CreateVaultService(hubSession).createVault(userKeys, storageProfileWrapper, new CreateVaultService.CreateVaultModel( From 6e55170e1d183b2009af92840613ac361470dfdb Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 2 Sep 2025 16:53:43 +0200 Subject: [PATCH 041/133] Simplify assertion. --- .../test/java/cloud/katta/core/AbstractHubSynchronizeTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java index 630cd756..08ac5070 100644 --- a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java +++ b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java @@ -242,7 +242,7 @@ public void test03AddVault(final HubTestConfig config) throws Exception { final Path bucket = new Path(storageProfileWrapper.getProtocol() == Protocol.S3_STS ? storageProfileWrapper.getBucketPrefix() + vaultId : config.vault.bucketName, EnumSet.of(Path.Type.volume, Path.Type.directory)); final VaultRegistry vaultRegistry = hubSession.getRegistry(); - assertTrue(vaultRegistry instanceof HubVaultRegistry); + assertInstanceOf(HubVaultRegistry.class, vaultRegistry); { assertNotNull(vaults.find(new SimplePathPredicate(bucket))); From 14e452031a1191cc8acb2b2e023cd0651642de0d Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 2 Sep 2025 17:06:03 +0200 Subject: [PATCH 042/133] Extract class. --- .../katta/protocols/hub/HubAwareProfile.java | 32 +++++++++++++++++++ .../katta/workflows/VaultServiceImpl.java | 26 ++------------- 2 files changed, 35 insertions(+), 23 deletions(-) create mode 100644 hub/src/main/java/cloud/katta/protocols/hub/HubAwareProfile.java diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubAwareProfile.java b/hub/src/main/java/cloud/katta/protocols/hub/HubAwareProfile.java new file mode 100644 index 00000000..a44dcef3 --- /dev/null +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubAwareProfile.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2025 shift7 GmbH. All rights reserved. + */ + +package cloud.katta.protocols.hub; + +import ch.cyberduck.core.Profile; +import ch.cyberduck.core.Protocol; + +import cloud.katta.client.model.ConfigDto; +import cloud.katta.model.StorageProfileDtoWrapper; +import cloud.katta.protocols.hub.serializer.HubConfigDtoDeserializer; +import cloud.katta.protocols.hub.serializer.StorageProfileDtoWrapperDeserializer; + +public final class HubAwareProfile extends Profile { + private final HubSession hub; + + public HubAwareProfile(final HubSession hub, final Protocol parent, final ConfigDto configDto, final StorageProfileDtoWrapper storageProfile) { + super(parent, new StorageProfileDtoWrapperDeserializer( + new HubConfigDtoDeserializer(configDto), storageProfile)); + this.hub = hub; + } + + @SuppressWarnings("unchecked") + @Override + public T getFeature(final Class type) { + if(type == HubSession.class) { + return (T) hub; + } + return super.getFeature(type); + } +} diff --git a/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java b/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java index 085196b6..41abef09 100644 --- a/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java +++ b/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java @@ -8,9 +8,10 @@ import ch.cyberduck.core.Host; import ch.cyberduck.core.OAuthTokens; import ch.cyberduck.core.Profile; -import ch.cyberduck.core.Protocol; import ch.cyberduck.core.ProtocolFactory; +import cloud.katta.protocols.hub.HubAwareProfile; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -29,8 +30,6 @@ import cloud.katta.crypto.uvf.VaultMetadataJWEBackendDto; import cloud.katta.model.StorageProfileDtoWrapper; import cloud.katta.protocols.hub.HubSession; -import cloud.katta.protocols.hub.serializer.HubConfigDtoDeserializer; -import cloud.katta.protocols.hub.serializer.StorageProfileDtoWrapperDeserializer; import cloud.katta.workflows.exceptions.AccessException; import cloud.katta.workflows.exceptions.SecurityFailure; import com.fasterxml.jackson.core.JsonProcessingException; @@ -115,29 +114,10 @@ public Host getStorageBackend(final HubSession hub, final ConfigDto configDto, f } if(profile.getProperties().get(S3_ASSUMEROLE_ROLEARN) != null) { bookmark.setProperty(OAUTH_TOKENEXCHANGE_VAULT, vaultId.toString()); - bookmark.setProperty(OAUTH_TOKENEXCHANGE_BASEPATH, this.vaultResource.getApiClient().getBasePath()); + bookmark.setProperty(OAUTH_TOKENEXCHANGE_BASEPATH, vaultResource.getApiClient().getBasePath()); } // region as chosen by user upon vault creation (STS) or as retrieved from bucket (permanent) bookmark.setRegion(vaultMetadata.getRegion()); return bookmark; } - - private static final class HubAwareProfile extends Profile { - private final HubSession hub; - - public HubAwareProfile(final HubSession hub, final Protocol parent, final ConfigDto configDto, final StorageProfileDtoWrapper storageProfile) { - super(parent, new StorageProfileDtoWrapperDeserializer( - new HubConfigDtoDeserializer(configDto), storageProfile)); - this.hub = hub; - } - - @SuppressWarnings("unchecked") - @Override - public T getFeature(final Class type) { - if(type == HubSession.class) { - return (T) hub; - } - return super.getFeature(type); - } - } } From 716d282715d4937474572f0d241f4e8b55d397bb Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 2 Sep 2025 17:12:48 +0200 Subject: [PATCH 043/133] Extract method. --- .../protocols/hub/HubVaultListService.java | 3 +- .../katta/workflows/CreateVaultService.java | 2 +- .../cloud/katta/workflows/VaultService.java | 22 +++++++++---- .../katta/workflows/VaultServiceImpl.java | 33 +++++++++++-------- 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java index bb17ac3c..1dc83718 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java @@ -89,8 +89,7 @@ public AttributedList list(final Path directory, final ListProgressListene } throw e; } - final Host bookmark = vaultService.getStorageBackend(session, configDto, vaultDto.getId(), - vaultMetadata.storage(), tokens); + final Host bookmark = vaultService.getStorageBackend(session, configDto, vaultDto.getId(), vaultMetadata, tokens); log.debug("Configured {} for vault {}", bookmark, vaultDto); final Session storage = SessionFactory.create(bookmark, session.getFeature(X509TrustManager.class), session.getFeature(X509KeyManager.class)); diff --git a/hub/src/main/java/cloud/katta/workflows/CreateVaultService.java b/hub/src/main/java/cloud/katta/workflows/CreateVaultService.java index 49791a58..e1edd0d4 100644 --- a/hub/src/main/java/cloud/katta/workflows/CreateVaultService.java +++ b/hub/src/main/java/cloud/katta/workflows/CreateVaultService.java @@ -142,7 +142,7 @@ public void createVault(final UserKeys userKeys, final StorageProfileDtoWrapper final OAuthTokens tokens = keychain.findOAuthTokens(hubSession.getHost()); final Host bookmark = new VaultServiceImpl(vaultResource, storageProfileResource).getStorageBackend( - hubSession, configResource.apiConfigGet(), vaultDto.getId(), metadataPayload.storage(), tokens); + hubSession, configResource.apiConfigGet(), vaultDto.getId(), metadataPayload, tokens); if(storageProfileWrapper.getProtocol() == Protocol.S3) { // permanent: template upload into existing bucket from client (not backend) templateUploadService.uploadTemplate(bookmark, metadataPayload, storageDto, hashedRootDirId); diff --git a/hub/src/main/java/cloud/katta/workflows/VaultService.java b/hub/src/main/java/cloud/katta/workflows/VaultService.java index 98a206aa..0bc770e0 100644 --- a/hub/src/main/java/cloud/katta/workflows/VaultService.java +++ b/hub/src/main/java/cloud/katta/workflows/VaultService.java @@ -14,7 +14,7 @@ import cloud.katta.crypto.UserKeys; import cloud.katta.crypto.uvf.UvfAccessTokenPayload; import cloud.katta.crypto.uvf.UvfMetadataPayload; -import cloud.katta.crypto.uvf.VaultMetadataJWEBackendDto; +import cloud.katta.model.StorageProfileDtoWrapper; import cloud.katta.protocols.hub.HubSession; import cloud.katta.workflows.exceptions.AccessException; import cloud.katta.workflows.exceptions.SecurityFailure; @@ -31,7 +31,7 @@ public interface VaultService { * @param userKeys EC key pair * @return Vault metadata */ - UvfMetadataPayload getVaultMetadataJWE(UUID vaultId, UserKeys userKeys) throws ApiException, SecurityFailure, AccessException; + UvfMetadataPayload getVaultMetadataJWE(UUID vaultId, UserKeys userKeys) throws ApiException, AccessException, SecurityFailure; /** * Get vault access token containing vault member key and recovery key (if owner) @@ -44,16 +44,24 @@ public interface VaultService { */ UvfAccessTokenPayload getVaultAccessTokenJWE(UUID vaultId, UserKeys userKeys) throws ApiException, AccessException, SecurityFailure; + /** + * Get storage configuration for vault + * + * @param metadataPayload Vault metadata including storage configuration + * @return Storage profile + */ + StorageProfileDtoWrapper getVaultStorageProfile(UvfMetadataPayload metadataPayload) throws ApiException, AccessException, SecurityFailure; + /** * Prepares (virtual) bookmark for vault to access its configured storage backend. * - * @param hub Hub API Connection - * @param configDto Hub configuration - * @param vaultId Vault ID - * @param vaultMetadata Storage Backend configuration + * @param hub Hub API Connection + * @param configDto Hub configuration + * @param vaultId Vault ID + * @param metadataPayload Storage Backend configuration * @return Configuration * @throws AccessException Unsupported backend storage protocol * @throws ApiException Server error accessing storage profile */ - Host getStorageBackend(final HubSession hub, final ConfigDto configDto, UUID vaultId, VaultMetadataJWEBackendDto vaultMetadata, final OAuthTokens tokens) throws AccessException, ApiException; + Host getStorageBackend(final HubSession hub, final ConfigDto configDto, UUID vaultId, final UvfMetadataPayload metadataPayload, final OAuthTokens tokens) throws AccessException, ApiException; } diff --git a/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java b/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java index 41abef09..87e652cf 100644 --- a/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java +++ b/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java @@ -10,8 +10,6 @@ import ch.cyberduck.core.Profile; import ch.cyberduck.core.ProtocolFactory; -import cloud.katta.protocols.hub.HubAwareProfile; - import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -29,6 +27,7 @@ import cloud.katta.crypto.uvf.UvfMetadataPayload; import cloud.katta.crypto.uvf.VaultMetadataJWEBackendDto; import cloud.katta.model.StorageProfileDtoWrapper; +import cloud.katta.protocols.hub.HubAwareProfile; import cloud.katta.protocols.hub.HubSession; import cloud.katta.workflows.exceptions.AccessException; import cloud.katta.workflows.exceptions.SecurityFailure; @@ -83,10 +82,15 @@ public UvfAccessTokenPayload getVaultAccessTokenJWE(final UUID vaultId, final Us } @Override - public Host getStorageBackend(final HubSession hub, final ConfigDto configDto, final UUID vaultId, final VaultMetadataJWEBackendDto vaultMetadata, final OAuthTokens tokens) throws ApiException, AccessException { - log.debug("Load profile {}", vaultMetadata.getProvider()); - final StorageProfileDtoWrapper storageProfile = StorageProfileDtoWrapper.coerce(storageProfileResourceApi - .apiStorageprofileProfileIdGet(UUID.fromString(vaultMetadata.getProvider()))); + public StorageProfileDtoWrapper getVaultStorageProfile(final UvfMetadataPayload metadataPayload) throws ApiException { + log.debug("Load profile {}", metadataPayload.storage().getProvider()); + return StorageProfileDtoWrapper.coerce(storageProfileResourceApi + .apiStorageprofileProfileIdGet(UUID.fromString(metadataPayload.storage().getProvider()))); + } + + @Override + public Host getStorageBackend(final HubSession hub, final ConfigDto configDto, final UUID vaultId, final UvfMetadataPayload vaultMetadataPayload, final OAuthTokens tokens) throws ApiException, AccessException { + final StorageProfileDtoWrapper storageProfile = this.getVaultStorageProfile(vaultMetadataPayload); log.debug("Read storage profile {}", storageProfile); final Profile profile; switch(storageProfile.getProtocol()) { @@ -101,23 +105,24 @@ public Host getStorageBackend(final HubSession hub, final ConfigDto configDto, f throw new AccessException(String.format("Unsupported storage configuration %s", storageProfile.getProtocol().name())); } final Host bookmark = new Host(profile); - log.debug("Configure bookmark for vault {}", vaultMetadata); - bookmark.setNickname(vaultMetadata.getNickname()); - bookmark.setDefaultPath(vaultMetadata.getDefaultPath()); + final VaultMetadataJWEBackendDto vaultStorageMetadata = vaultMetadataPayload.storage(); + log.debug("Configure bookmark for vault {}", vaultStorageMetadata); + bookmark.setNickname(vaultStorageMetadata.getNickname()); + bookmark.setDefaultPath(vaultStorageMetadata.getDefaultPath()); final Credentials credentials = bookmark.getCredentials(); credentials.setOauth(tokens); - if(vaultMetadata.getUsername() != null) { - credentials.setUsername(vaultMetadata.getUsername()); + if(vaultStorageMetadata.getUsername() != null) { + credentials.setUsername(vaultStorageMetadata.getUsername()); } - if(vaultMetadata.getPassword() != null) { - credentials.setPassword(vaultMetadata.getPassword()); + if(vaultStorageMetadata.getPassword() != null) { + credentials.setPassword(vaultStorageMetadata.getPassword()); } if(profile.getProperties().get(S3_ASSUMEROLE_ROLEARN) != null) { bookmark.setProperty(OAUTH_TOKENEXCHANGE_VAULT, vaultId.toString()); bookmark.setProperty(OAUTH_TOKENEXCHANGE_BASEPATH, vaultResource.getApiClient().getBasePath()); } // region as chosen by user upon vault creation (STS) or as retrieved from bucket (permanent) - bookmark.setRegion(vaultMetadata.getRegion()); + bookmark.setRegion(vaultStorageMetadata.getRegion()); return bookmark; } } From 357eb1c97d98a10efda521a867880a7346d4c834 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 2 Sep 2025 23:27:30 +0200 Subject: [PATCH 044/133] Remove redundant override. --- hub/src/main/java/cloud/katta/protocols/hub/HubSession.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 16745e79..86e10d57 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -89,10 +89,6 @@ public HubSession(final Host host, final X509TrustManager trust, final X509KeyMa super(host, trust, key); } - public VaultRegistry getRegistry() { - return registry; - } - @Override protected HubApiClient connect(final ProxyFinder proxy, final HostKeyCallback key, final LoginCallback prompt, final CancelCallback cancel) throws BackgroundException { From 3dddc43a8a0eca61ed45092f757c92d55474a134 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 4 Sep 2025 10:17:36 +0200 Subject: [PATCH 045/133] Delete unused property. --- .../main/java/cloud/katta/protocols/s3/S3AssumeRoleProtocol.java | 1 - .../katta/protocols/s3/TokenExchangeRequestInterceptor.java | 1 - hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java | 1 - 3 files changed, 3 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleProtocol.java b/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleProtocol.java index c274d1b8..efbc6fa8 100644 --- a/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleProtocol.java +++ b/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleProtocol.java @@ -17,7 +17,6 @@ public class S3AssumeRoleProtocol extends S3Protocol { // Token exchange public static final String OAUTH_TOKENEXCHANGE = "oauth.tokenexchange"; public static final String OAUTH_TOKENEXCHANGE_VAULT = "oauth.tokenexchange.vault"; - public static final String OAUTH_TOKENEXCHANGE_BASEPATH = "oauth.tokenexchange.basepath"; // STS assume role with web identity resource name public static final String S3_ASSUMEROLE_ROLEARN = Profile.STS_ROLE_ARN_PROPERTY_KEY; diff --git a/hub/src/main/java/cloud/katta/protocols/s3/TokenExchangeRequestInterceptor.java b/hub/src/main/java/cloud/katta/protocols/s3/TokenExchangeRequestInterceptor.java index 7678d153..0ec5d2da 100644 --- a/hub/src/main/java/cloud/katta/protocols/s3/TokenExchangeRequestInterceptor.java +++ b/hub/src/main/java/cloud/katta/protocols/s3/TokenExchangeRequestInterceptor.java @@ -66,7 +66,6 @@ public OAuthTokens refresh(final OAuthTokens previous) throws BackgroundExceptio * @param previous Input tokens retrieved to exchange at the token endpoint * @return New tokens * @see S3AssumeRoleProtocol#OAUTH_TOKENEXCHANGE_VAULT - * @see S3AssumeRoleProtocol#OAUTH_TOKENEXCHANGE_BASEPATH */ public OAuthTokens exchange(final OAuthTokens previous) throws BackgroundException { log.info("Exchange tokens {} for {}", previous, bookmark); diff --git a/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java b/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java index 87e652cf..4f9eba19 100644 --- a/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java +++ b/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java @@ -119,7 +119,6 @@ public Host getStorageBackend(final HubSession hub, final ConfigDto configDto, f } if(profile.getProperties().get(S3_ASSUMEROLE_ROLEARN) != null) { bookmark.setProperty(OAUTH_TOKENEXCHANGE_VAULT, vaultId.toString()); - bookmark.setProperty(OAUTH_TOKENEXCHANGE_BASEPATH, vaultResource.getApiClient().getBasePath()); } // region as chosen by user upon vault creation (STS) or as retrieved from bucket (permanent) bookmark.setRegion(vaultStorageMetadata.getRegion()); From d589135ab53b43acccf6928f6fc64caa333d9438 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 4 Sep 2025 10:38:06 +0200 Subject: [PATCH 046/133] Use factory. --- .../hub/HubGrantAccessSchedulerService.java | 7 +- .../protocols/s3/S3AssumeRoleSession.java | 67 +++++++++++++++---- .../s3/TokenExchangeRequestInterceptor.java | 4 +- 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubGrantAccessSchedulerService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubGrantAccessSchedulerService.java index 7cf060a2..3bc04dbc 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubGrantAccessSchedulerService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubGrantAccessSchedulerService.java @@ -8,11 +8,9 @@ import ch.cyberduck.core.HostPasswordStore; import ch.cyberduck.core.PasswordCallback; import ch.cyberduck.core.exception.BackgroundException; -import ch.cyberduck.core.preferences.HostPreferences; +import ch.cyberduck.core.preferences.HostPreferencesFactory; import ch.cyberduck.core.shared.ThreadPoolSchedulerFeature; -import cloud.katta.client.api.StorageProfileResourceApi; - import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -20,6 +18,7 @@ import cloud.katta.client.ApiException; import cloud.katta.client.api.DeviceResourceApi; +import cloud.katta.client.api.StorageProfileResourceApi; import cloud.katta.client.api.UsersResourceApi; import cloud.katta.client.api.VaultResourceApi; import cloud.katta.client.model.Role; @@ -39,7 +38,7 @@ public class HubGrantAccessSchedulerService extends ThreadPoolSchedulerFeature Date: Thu, 4 Sep 2025 11:26:54 +0200 Subject: [PATCH 047/133] Set vault ID as vendor key in profile. --- .../katta/protocols/hub/HubAwareProfile.java | 7 +-- .../serializer/HubConfigDtoDeserializer.java | 10 +++- .../StorageProfileDtoWrapperDeserializer.java | 9 ++-- .../hub/serializer/VaultDtoDeserializer.java | 48 +++++++++++++++++++ 4 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 hub/src/main/java/cloud/katta/protocols/hub/serializer/VaultDtoDeserializer.java diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubAwareProfile.java b/hub/src/main/java/cloud/katta/protocols/hub/HubAwareProfile.java index a44dcef3..5bfd8cd9 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubAwareProfile.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubAwareProfile.java @@ -8,16 +8,17 @@ import ch.cyberduck.core.Protocol; import cloud.katta.client.model.ConfigDto; +import cloud.katta.client.model.VaultDto; import cloud.katta.model.StorageProfileDtoWrapper; import cloud.katta.protocols.hub.serializer.HubConfigDtoDeserializer; import cloud.katta.protocols.hub.serializer.StorageProfileDtoWrapperDeserializer; +import cloud.katta.protocols.hub.serializer.VaultDtoDeserializer; public final class HubAwareProfile extends Profile { private final HubSession hub; - public HubAwareProfile(final HubSession hub, final Protocol parent, final ConfigDto configDto, final StorageProfileDtoWrapper storageProfile) { - super(parent, new StorageProfileDtoWrapperDeserializer( - new HubConfigDtoDeserializer(configDto), storageProfile)); + public HubAwareProfile(final HubSession hub, final Protocol parent, final ConfigDto configDto, final StorageProfileDtoWrapper storageProfile, final VaultDto vaultDto) { + super(parent, new HubConfigDtoDeserializer(configDto, new StorageProfileDtoWrapperDeserializer(storageProfile, new VaultDtoDeserializer(vaultDto)))); this.hub = hub; } diff --git a/hub/src/main/java/cloud/katta/protocols/hub/serializer/HubConfigDtoDeserializer.java b/hub/src/main/java/cloud/katta/protocols/hub/serializer/HubConfigDtoDeserializer.java index 0a138240..3b8d2895 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/serializer/HubConfigDtoDeserializer.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/serializer/HubConfigDtoDeserializer.java @@ -4,6 +4,8 @@ package cloud.katta.protocols.hub.serializer; +import ch.cyberduck.core.serializer.Deserializer; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -18,11 +20,15 @@ public class HubConfigDtoDeserializer extends ProxyDeserializer { private final ConfigDto dto; + public HubConfigDtoDeserializer(final ConfigDto dto) { + this(dto, ProxyDeserializer.empty()); + } + /** * @param dto Hub configuration */ - public HubConfigDtoDeserializer(final ConfigDto dto) { - super(ProxyDeserializer.empty()); + public HubConfigDtoDeserializer(final ConfigDto dto, final Deserializer parent) { + super(parent); this.dto = dto; } diff --git a/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java b/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java index a526a428..9a0d2fcc 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java @@ -25,10 +25,14 @@ public class StorageProfileDtoWrapperDeserializer extends ProxyDeserializer parent, final StorageProfileDtoWrapper dto) { + public StorageProfileDtoWrapperDeserializer(final StorageProfileDtoWrapper dto, final Deserializer parent) { super(parent); this.dto = dto; } @@ -79,8 +83,6 @@ public String stringForKey(final String key) { return new S3AssumeRoleProtocol().getIdentifier(); } break; - case VENDOR_KEY: - return dto.getId().toString(); case DEFAULT_NICKNAME_KEY: return dto.getName(); case SCHEME_KEY: @@ -117,7 +119,6 @@ public List keys() { final List keys = new ArrayList<>(super.keys()); keys.addAll(Arrays.asList( PROTOCOL_KEY, - VENDOR_KEY, PROPERTIES_KEY, OAUTH_CONFIGURABLE_KEY) ); diff --git a/hub/src/main/java/cloud/katta/protocols/hub/serializer/VaultDtoDeserializer.java b/hub/src/main/java/cloud/katta/protocols/hub/serializer/VaultDtoDeserializer.java new file mode 100644 index 00000000..43d438a1 --- /dev/null +++ b/hub/src/main/java/cloud/katta/protocols/hub/serializer/VaultDtoDeserializer.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025 shift7 GmbH. All rights reserved. + */ + +package cloud.katta.protocols.hub.serializer; + +import ch.cyberduck.core.serializer.Deserializer; + +import java.util.Collections; +import java.util.List; + +import cloud.katta.client.model.VaultDto; +import com.dd.plist.NSDictionary; + +import static ch.cyberduck.core.Profile.VENDOR_KEY; + +public class VaultDtoDeserializer extends ProxyDeserializer { + + private final VaultDto dto; + + public VaultDtoDeserializer(final VaultDto dto) { + this(dto, ProxyDeserializer.empty()); + } + + /** + * @param dto Storage configuration + */ + public VaultDtoDeserializer(final VaultDto dto, final Deserializer parent) { + super(parent); + this.dto = dto; + } + + @Override + public String stringForKey(final String key) { + // default profile and possible regions for UI: + switch(key) { + case VENDOR_KEY: + // Allow lookup of profile with vault id + return dto.getId().toString(); + } + return super.stringForKey(key); + } + + @Override + public List keys() { + return Collections.singletonList(VENDOR_KEY); + } +} From fa61288859efe3f6e0b262e879f6ebcf4dcf0029 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 4 Sep 2025 11:36:07 +0200 Subject: [PATCH 048/133] Log failures. --- .../protocols/hub/exceptions/HubExceptionMappingService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/exceptions/HubExceptionMappingService.java b/hub/src/main/java/cloud/katta/protocols/hub/exceptions/HubExceptionMappingService.java index 2ede6c73..d7a27ef8 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/exceptions/HubExceptionMappingService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/exceptions/HubExceptionMappingService.java @@ -32,6 +32,7 @@ public BackgroundException map(final String message, final ApiException failure, @Override public BackgroundException map(final ApiException failure) { + log.warn("Map failure {}", failure.toString()); for(final Throwable cause : ExceptionUtils.getThrowableList(failure)) { if(cause instanceof SocketException) { // Map Connection has been shutdown: javax.net.ssl.SSLException: java.net.SocketException: Broken pipe From 7ba91ead6cd34df784e5dbea9758f560ad5830d4 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 4 Sep 2025 11:52:51 +0200 Subject: [PATCH 049/133] Add overloaded constructor. --- .../uvf/UvfMetadataPayloadPasswordCallback.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/hub/src/main/java/cloud/katta/crypto/uvf/UvfMetadataPayloadPasswordCallback.java b/hub/src/main/java/cloud/katta/crypto/uvf/UvfMetadataPayloadPasswordCallback.java index 1929f2a9..8d364e71 100644 --- a/hub/src/main/java/cloud/katta/crypto/uvf/UvfMetadataPayloadPasswordCallback.java +++ b/hub/src/main/java/cloud/katta/crypto/uvf/UvfMetadataPayloadPasswordCallback.java @@ -15,19 +15,18 @@ public class UvfMetadataPayloadPasswordCallback extends DisabledPasswordCallback { - private final UvfMetadataPayload payload; + private final String payloadJson; - public UvfMetadataPayloadPasswordCallback(final UvfMetadataPayload payload) { - this.payload = payload; + public UvfMetadataPayloadPasswordCallback(final UvfMetadataPayload payload) throws JsonProcessingException { + this(payload.toJSON()); + } + + public UvfMetadataPayloadPasswordCallback(final String payloadJson) { + this.payloadJson = payloadJson; } @Override public Credentials prompt(final Host bookmark, final String title, final String reason, final LoginOptions options) throws LoginCanceledException { - try { - return new VaultCredentials(payload.toJSON()); - } - catch(JsonProcessingException e) { - throw new LoginCanceledException(e); - } + return new VaultCredentials(payloadJson); } } From b4fcf21cadd2d5dea7c03b75281dc652fec45f7c Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 4 Sep 2025 11:55:12 +0200 Subject: [PATCH 050/133] Refactor to load vaults only once with short-lived user keys. --- .../cloud/katta/protocols/hub/HubSession.java | 70 +++++--- .../katta/protocols/hub/HubUVFVault.java | 155 ++++++++++++------ .../protocols/hub/HubVaultListService.java | 119 ++++++-------- .../cloud/katta/workflows/VaultService.java | 18 -- .../katta/workflows/VaultServiceImpl.java | 47 ------ 5 files changed, 205 insertions(+), 204 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 86e10d57..fd94eb96 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -30,14 +30,13 @@ import ch.cyberduck.core.oauth.OAuth2AuthorizationService; import ch.cyberduck.core.oauth.OAuth2ErrorResponseInterceptor; import ch.cyberduck.core.oauth.OAuth2RequestInterceptor; -import ch.cyberduck.core.preferences.PreferencesFactory; +import ch.cyberduck.core.preferences.HostPreferencesFactory; import ch.cyberduck.core.proxy.ProxyFinder; import ch.cyberduck.core.ssl.X509KeyManager; import ch.cyberduck.core.ssl.X509TrustManager; import ch.cyberduck.core.synchronization.ComparisonService; import ch.cyberduck.core.threading.CancelCallback; import ch.cyberduck.core.transfer.TransferStatus; -import ch.cyberduck.core.vault.VaultRegistry; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; @@ -54,6 +53,7 @@ import cloud.katta.client.model.ConfigDto; import cloud.katta.client.model.UserDto; import cloud.katta.core.DeviceSetupCallback; +import cloud.katta.crypto.DeviceKeys; import cloud.katta.crypto.UserKeys; import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService; import cloud.katta.protocols.hub.serializer.HubConfigDtoDeserializer; @@ -77,18 +77,24 @@ public class HubSession extends HttpSession { */ private final Scheduler access = new HubGrantAccessSchedulerService(this, keychain); - private HubVaultListService vaults; - /** * Interceptor for OpenID connect flow */ private OAuth2RequestInterceptor authorizationService; + private UserDto me; + private ConfigDto config; + private UserKeys userKeys; + private AttributedList vaults; public HubSession(final Host host, final X509TrustManager trust, final X509KeyManager key) { super(host, trust, key); } + public static HubSession coerce(final Session session) { + return (HubSession) session; + } + @Override protected HubApiClient connect(final ProxyFinder proxy, final HostKeyCallback key, final LoginCallback prompt, final CancelCallback cancel) throws BackgroundException { @@ -99,20 +105,19 @@ protected HubApiClient connect(final ProxyFinder proxy, final HostKeyCallback ke final HubApiClient client = new HubApiClient(host, configuration.build()); try { // Obtain OAuth configuration - final ConfigDto configDto = new ConfigResourceApi(client).apiConfigGet(); - - int minHubApiLevel = PreferencesFactory.get().getInteger("cloud.katta.min_api_level"); - final Integer apiLevel = configDto.getApiLevel(); + config = new ConfigResourceApi(client).apiConfigGet(); + final int minHubApiLevel = HostPreferencesFactory.get(host).getInteger("cloud.katta.min_api_level"); + final Integer apiLevel = config.getApiLevel(); if(apiLevel == null || apiLevel < minHubApiLevel) { final String detail = String.format("Client requires API level at least %s, found %s, for hub %s", minHubApiLevel, apiLevel, host); log.error(detail); throw new InteroperabilityException(LocaleFactory.localizedString("Login failed", "Credentials"), detail); } - final String hubId = configDto.getUuid(); + final String hubId = config.getUuid(); log.debug("Configure bookmark with id {}", hubId); host.setUuid(hubId); - final Profile profile = new Profile(bundled, new HubConfigDtoDeserializer(configDto)); + final Profile profile = new Profile(bundled, new HubConfigDtoDeserializer(config)); log.debug("Apply profile {} to bookmark {}", profile, host); host.setProtocol(profile); } @@ -156,27 +161,32 @@ public void login(final LoginCallback prompt, final CancelCallback cancel) throw // Ensure device key is available final DeviceSetupCallback setup = prompt.getFeature(DeviceSetupCallback.class); log.debug("Configured with setup prompt {}", setup); - this.pair(setup); + userKeys = this.pair(setup); // Ensure vaults are registered - final OAuthTokens tokens = new OAuthTokens(credentials.getOauth().getAccessToken(), credentials.getOauth().getRefreshToken(), credentials.getOauth().getExpiryInMilliseconds(), - credentials.getOauth().getIdToken()); - vaults = new HubVaultListService(this, tokens); - vaults.list(Home.root(), new DisabledListProgressListener()); + try { + vaults = new HubVaultListService(this, prompt).list(Home.root(), new DisabledListProgressListener()); + } + finally { + // Short-lived + userKeys.destroy(); + } } catch(ApiException e) { throw new HubExceptionMappingService().map(e); } } - private void pair(final DeviceSetupCallback setup) throws BackgroundException { + private UserKeys pair(final DeviceSetupCallback setup) throws BackgroundException { try { - final UserKeys userKeys = new UserKeysServiceImpl(this, keychain).getOrCreateUserKeys(host, me, - new DeviceKeysServiceImpl(keychain).getOrCreateDeviceKeys(host, setup), setup); + final DeviceKeys deviceKeys = new DeviceKeysServiceImpl(keychain).getOrCreateDeviceKeys(host, setup); + log.debug("Retrieved device keys {}", deviceKeys); + final UserKeys userKeys = new UserKeysServiceImpl(this, keychain).getOrCreateUserKeys(host, me, deviceKeys, setup); log.debug("Retrieved user keys {}", userKeys); + return userKeys; } catch(SecurityFailure e) { // Repeat until canceled by user - this.pair(setup); + return this.pair(setup); } catch(AccessException e) { throw new ConnectionCanceledException(e); @@ -192,15 +202,35 @@ protected void logout() { client.getHttpClient().close(); } + /** + * + * @return Null prior login + */ public UserDto getMe() { return me; } + /** + * + * @return Null when not connected + */ + public ConfigDto getConfig() { + return config; + } + + /** + * + * @return Destroyed keys after login + */ + public UserKeys getUserKeys() { + return userKeys; + } + @Override @SuppressWarnings("unchecked") public T _getFeature(final Class type) { if(type == ListService.class) { - return (T) vaults; + return (T) (ListService) (directory, listener) -> vaults; } if(type == Scheduler.class) { return (T) access; diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 6c420f16..50799364 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -4,30 +4,46 @@ package cloud.katta.protocols.hub; -import ch.cyberduck.core.AbstractPath; +import ch.cyberduck.core.Credentials; import ch.cyberduck.core.DisabledCancelCallback; import ch.cyberduck.core.DisabledHostKeyCallback; -import ch.cyberduck.core.DisabledListProgressListener; import ch.cyberduck.core.DisabledLoginCallback; -import ch.cyberduck.core.ListService; +import ch.cyberduck.core.Host; import ch.cyberduck.core.PasswordCallback; import ch.cyberduck.core.Path; +import ch.cyberduck.core.PathAttributes; +import ch.cyberduck.core.Protocol; +import ch.cyberduck.core.ProtocolFactory; import ch.cyberduck.core.Session; -import ch.cyberduck.core.cryptomator.ContentWriter; +import ch.cyberduck.core.SessionFactory; import ch.cyberduck.core.cryptomator.UVFVault; import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.exception.InteroperabilityException; import ch.cyberduck.core.exception.UnsupportedException; -import ch.cyberduck.core.features.Directory; -import ch.cyberduck.core.features.Write; -import ch.cyberduck.core.preferences.PreferencesFactory; +import ch.cyberduck.core.features.AttributesFinder; import ch.cyberduck.core.proxy.ProxyFactory; -import ch.cyberduck.core.transfer.TransferStatus; +import ch.cyberduck.core.ssl.X509KeyManager; +import ch.cyberduck.core.ssl.X509TrustManager; +import ch.cyberduck.core.vault.VaultUnlockCancelException; +import org.apache.http.HttpStatus; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.nio.charset.StandardCharsets; import java.util.EnumSet; +import java.util.UUID; + +import cloud.katta.client.ApiException; +import cloud.katta.crypto.uvf.UvfMetadataPayload; +import cloud.katta.crypto.uvf.UvfMetadataPayloadPasswordCallback; +import cloud.katta.crypto.uvf.VaultMetadataJWEBackendDto; +import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService; +import cloud.katta.workflows.VaultServiceImpl; +import cloud.katta.workflows.exceptions.AccessException; +import cloud.katta.workflows.exceptions.SecurityFailure; +import com.fasterxml.jackson.core.JsonProcessingException; + +import static cloud.katta.protocols.s3.S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_VAULT; /** * Unified vault format (UVF) implementation for Katta @@ -35,13 +51,32 @@ public class HubUVFVault extends UVFVault { private static final Logger log = LogManager.getLogger(HubUVFVault.class); - private final Session storage; - private final Path home; + private final UUID vaultId; + + /** + * Storage connection only available after loading vault + */ + private Session storage; - public HubUVFVault(final Session storage, final Path home) { + /** + * Constructor for factory creating new vault + * + * @param home Bucket + */ + public HubUVFVault(final Path home) { super(home); - this.storage = storage; - this.home = home; + this.vaultId = UUID.fromString(home.getName()); + } + + /** + * Open from existing metadata + * + * @param vaultId Vault ID Used to lookup profile + * @param bucket Bucket name + */ + public HubUVFVault(final UUID vaultId, final String bucket) { + super(new Path(bucket, EnumSet.of(Path.Type.directory, Path.Type.volume))); + this.vaultId = vaultId; } public Session getStorage() { @@ -73,46 +108,66 @@ public synchronized void close() { } /** - * Upload vault template into existing bucket (permanent credentials) + * + * @param session Hub Connection + * @param prompt Return user keys + * @return Vault configuration with storage connection */ - // TODO https://github.com/shift7-ch/cipherduck-hub/issues/19 review @dko check method signature? - public synchronized Path create(final Session session, final String region, final String metadata, final String hashedRootDirId, final byte[] rootDirUvf) throws BackgroundException { - log.debug("Uploading vault template {} in {} ", home, session.getHost()); - - // N.B. there seems to be no API to check write permissions without actually writing. - if(!session.getFeature(ListService.class).list(home, new DisabledListProgressListener()).isEmpty()) { - throw new BackgroundException("Bucket not empty", String.format("Cannot upload bucket %s in %s is not empty.", home, session.getHost())); + @Override + public HubUVFVault load(final Session session, final PasswordCallback prompt) throws BackgroundException { + try { + final HubSession hub = HubSession.coerce(session); + // Find storage configuration in vault metadata + final VaultServiceImpl vaultService = new VaultServiceImpl(hub); + final UvfMetadataPayload vaultMetadata = vaultService.getVaultMetadataJWE(vaultId, hub.getUserKeys()); + final Protocol profile = ProtocolFactory.get().forName(vaultId.toString()); + log.debug("Loaded profile {} for vault {}", profile, this); + final Credentials credentials = new Credentials(hub.getHost().getCredentials()); + log.debug("Copy credentials {}", credentials); + final VaultMetadataJWEBackendDto vaultStorageMetadata = vaultMetadata.storage(); + if(vaultStorageMetadata.getUsername() != null) { + credentials.setUsername(vaultStorageMetadata.getUsername()); + } + if(vaultStorageMetadata.getPassword() != null) { + credentials.setPassword(vaultStorageMetadata.getPassword()); + } + final Host bookmark = new Host(profile, credentials); + log.debug("Configure bookmark for vault {}", vaultStorageMetadata); + bookmark.setNickname(vaultStorageMetadata.getNickname()); + bookmark.setDefaultPath(vaultStorageMetadata.getDefaultPath()); + bookmark.setProperty(OAUTH_TOKENEXCHANGE_VAULT, vaultId.toString()); + // region as chosen by user upon vault creation (STS) or as retrieved from bucket (permanent) + bookmark.setRegion(vaultStorageMetadata.getRegion()); + log.debug("Configured {} for vault {}", bookmark, this); + storage = SessionFactory.create(bookmark, session.getFeature(X509TrustManager.class), session.getFeature(X509KeyManager.class)); + log.debug("Connect to {}", storage); + storage.open(ProxyFactory.get(), new DisabledHostKeyCallback(), new DisabledLoginCallback(), new DisabledCancelCallback()); + storage.login(new DisabledLoginCallback(), new DisabledCancelCallback()); + final PathAttributes attr = storage.getFeature(AttributesFinder.class).find(home); + attr.setDisplayname(vaultMetadata.storage().getNickname()); + home.setAttributes(attr); + log.debug("Initialize vault {} with metadata {}", this, vaultMetadata); + // Initialize cryptors + super.load(storage, new UvfMetadataPayloadPasswordCallback(vaultMetadata.toJSON())); + return this; + } + catch(ApiException e) { + if(HttpStatus.SC_FORBIDDEN == e.getCode()) { + throw new VaultUnlockCancelException(this, e); + } + throw new HubExceptionMappingService().map(e); + } + catch(JsonProcessingException | SecurityFailure | AccessException e) { + throw new InteroperabilityException(e.getMessage(), e); } - - // See https://github.com/cryptomator/hub/blob/develop/frontend/src/common/vaultconfig.ts - // zip.file('vault.cryptomator', this.vaultConfigToken); - // zip.folder('d')?.folder(this.rootDirHash.substring(0, 2))?.folder(this.rootDirHash.substring(2)); - - // /vault.uvf - new ContentWriter(session).write(new Path(home, PreferencesFactory.get().getProperty("cryptomator.vault.config.filename"), EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.vault)), metadata.getBytes(StandardCharsets.US_ASCII)); - Directory directory = (Directory) session._getFeature(Directory.class); - - // TODO https://github.com/shift7-ch/cipherduck-hub/issues/19 implement CryptoDirectory for uvf - // Path secondLevel = this.directoryProvider.toEncrypted(session, this.home.attributes().getDirectoryId(), this.home); - final Path secondLevel = new Path(String.format("/%s/d/%s/%s/", session.getHost().getDefaultPath(), hashedRootDirId.substring(0, 2), hashedRootDirId.substring(2)), EnumSet.of(AbstractPath.Type.directory)); - final Path firstLevel = secondLevel.getParent(); - final Path dataDir = firstLevel.getParent(); - log.debug("Create vault root directory at {}", secondLevel); - final TransferStatus status = (new TransferStatus()).setRegion(region); - - directory.mkdir(session._getFeature(Write.class), dataDir, status); - directory.mkdir(session._getFeature(Write.class), firstLevel, status); - directory.mkdir(session._getFeature(Write.class), secondLevel, status); - new ContentWriter(session).write(new Path(secondLevel, "dir.uvf", EnumSet.of(AbstractPath.Type.file)), rootDirUvf); - return home; } @Override - public HubUVFVault load(final Session ignore, final PasswordCallback prompt) throws BackgroundException { - log.debug("Connect to {}", storage); - storage.open(ProxyFactory.get(), new DisabledHostKeyCallback(), new DisabledLoginCallback(), new DisabledCancelCallback()); - storage.login(new DisabledLoginCallback(), new DisabledCancelCallback()); - super.load(storage, prompt); - return this; + public String toString() { + final StringBuilder sb = new StringBuilder("HubUVFVault{"); + sb.append("storage=").append(storage); + sb.append(", vaultId=").append(vaultId); + sb.append('}'); + return sb.toString(); } } diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java index 1dc83718..6a277ecc 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java @@ -5,116 +5,97 @@ package cloud.katta.protocols.hub; import ch.cyberduck.core.AttributedList; -import ch.cyberduck.core.Host; import ch.cyberduck.core.ListProgressListener; import ch.cyberduck.core.ListService; import ch.cyberduck.core.LocaleFactory; -import ch.cyberduck.core.OAuthTokens; +import ch.cyberduck.core.PasswordCallback; import ch.cyberduck.core.Path; -import ch.cyberduck.core.PathAttributes; +import ch.cyberduck.core.Profile; +import ch.cyberduck.core.Protocol; import ch.cyberduck.core.ProtocolFactory; -import ch.cyberduck.core.Session; -import ch.cyberduck.core.SessionFactory; import ch.cyberduck.core.exception.AccessDeniedException; import ch.cyberduck.core.exception.BackgroundException; -import ch.cyberduck.core.exception.InteroperabilityException; -import ch.cyberduck.core.exception.NotfoundException; -import ch.cyberduck.core.features.AttributesFinder; -import ch.cyberduck.core.shared.DefaultPathHomeFeature; -import ch.cyberduck.core.ssl.X509KeyManager; -import ch.cyberduck.core.ssl.X509TrustManager; +import ch.cyberduck.core.vault.VaultException; import ch.cyberduck.core.vault.VaultRegistry; import org.apache.http.HttpStatus; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.cryptomator.cryptolib.api.UVFMasterkey; import java.text.MessageFormat; -import java.util.EnumSet; import cloud.katta.client.ApiException; -import cloud.katta.client.api.ConfigResourceApi; import cloud.katta.client.api.VaultResourceApi; -import cloud.katta.client.model.ConfigDto; import cloud.katta.client.model.VaultDto; -import cloud.katta.crypto.DeviceKeys; -import cloud.katta.crypto.UserKeys; import cloud.katta.crypto.uvf.UvfMetadataPayload; -import cloud.katta.crypto.uvf.UvfMetadataPayloadPasswordCallback; +import cloud.katta.model.StorageProfileDtoWrapper; import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService; -import cloud.katta.workflows.DeviceKeysServiceImpl; -import cloud.katta.workflows.UserKeysServiceImpl; import cloud.katta.workflows.VaultServiceImpl; import cloud.katta.workflows.exceptions.AccessException; import cloud.katta.workflows.exceptions.SecurityFailure; -import com.fasterxml.jackson.core.JsonProcessingException; public class HubVaultListService implements ListService { private static final Logger log = LogManager.getLogger(HubVaultListService.class); private final HubSession session; - private final OAuthTokens tokens; + private final PasswordCallback prompt; - public HubVaultListService(final HubSession session, final OAuthTokens tokens) { + public HubVaultListService(final HubSession session, final PasswordCallback prompt) { this.session = session; - this.tokens = tokens; + this.prompt = prompt; } @Override public AttributedList list(final Path directory, final ListProgressListener listener) throws BackgroundException { - if(directory.isRoot()) { - try { - final ConfigDto configDto = new ConfigResourceApi(session.getClient()).apiConfigGet(); - log.debug("Read configuration {}", configDto); - final AttributedList vaults = new AttributedList<>(); - for(final VaultDto vaultDto : new VaultResourceApi(session.getClient()).apiVaultsAccessibleGet(null)) { - if(Boolean.TRUE.equals(vaultDto.getArchived())) { - log.debug("Skip archived vault {}", vaultDto); - continue; - } - final DeviceKeys deviceKeys = new DeviceKeysServiceImpl().getDeviceKeys(session.getHost()); - final UserKeys userKeys = new UserKeysServiceImpl(session).getUserKeys(session.getHost(), session.getMe(), deviceKeys); - log.debug("Read vault {}", vaultDto); + try { + final VaultRegistry registry = session.getRegistry(); + final AttributedList vaults = new AttributedList<>(); + for(final VaultDto vaultDto : new VaultResourceApi(session.getClient()).apiVaultsAccessibleGet(null)) { + if(Boolean.TRUE.equals(vaultDto.getArchived())) { + log.debug("Skip archived vault {}", vaultDto); + continue; + } + log.debug("Load vault {}", vaultDto); + try { // Find storage configuration in vault metadata final VaultServiceImpl vaultService = new VaultServiceImpl(session); - final UvfMetadataPayload vaultMetadata; - try { - vaultMetadata = vaultService.getVaultMetadataJWE(vaultDto.getId(), userKeys); + final UvfMetadataPayload vaultMetadata = vaultService.getVaultMetadataJWE(vaultDto.getId(), session.getUserKeys()); + final StorageProfileDtoWrapper storageProfile = new VaultServiceImpl(session).getVaultStorageProfile(vaultMetadata); + log.debug("Read storage profile {}", storageProfile); + switch(storageProfile.getProtocol()) { + case S3: + case S3_STS: + final ProtocolFactory protocols = ProtocolFactory.get(); + final Profile profile = new HubAwareProfile(session, protocols.forType(protocols.find(ProtocolFactory.BUNDLED_PROFILE_PREDICATE), Protocol.Type.s3), + session.getConfig(), storageProfile, vaultDto); + log.debug("Register profile {}", profile); + protocols.register(profile); + break; + default: + throw new VaultException(String.format("Unsupported storage configuration %s", storageProfile.getProtocol().name())); } - catch(ApiException e) { - if(HttpStatus.SC_FORBIDDEN == e.getCode()) { - log.warn("Skip vault {} with insufficient permissions", vaultDto); - continue; - } - throw e; - } - final Host bookmark = vaultService.getStorageBackend(session, configDto, vaultDto.getId(), vaultMetadata, tokens); - log.debug("Configured {} for vault {}", bookmark, vaultDto); - final Session storage = SessionFactory.create(bookmark, - session.getFeature(X509TrustManager.class), session.getFeature(X509KeyManager.class)); - final HubUVFVault vault = new HubUVFVault(storage, new DefaultPathHomeFeature(bookmark).find()); - final VaultRegistry registry = session.getRegistry(); - registry.add(vault.load(session, new UvfMetadataPayloadPasswordCallback(vaultMetadata))); - final PathAttributes attr = storage.getFeature(AttributesFinder.class).find(vault.getHome()); - try (UVFMasterkey masterKey = UVFMasterkey.fromDecryptedPayload(vaultMetadata.toJSON())) { - attr.setDirectoryId(masterKey.rootDirId()); - } - attr.setDisplayname(vaultMetadata.storage().getNickname()); - vaults.add(new Path(vault.getHome()).withType(EnumSet.of(Path.Type.volume, Path.Type.directory)) - .withAttributes(attr)); + final String bucketPrefix = storageProfile.getBucketPrefix(); + final HubUVFVault vault = new HubUVFVault(vaultDto.getId(), String.format("%s%s", bucketPrefix, vaultDto.getId())); + registry.add(vault.load(session, prompt)); + vaults.add(vault.getHome()); listener.chunk(directory, vaults); } - return vaults; - } - catch(ApiException e) { - throw new HubExceptionMappingService().map("Listing directory {0} failed", e, directory); - } - catch(SecurityFailure | AccessException | JsonProcessingException e) { - throw new InteroperabilityException(e.getMessage()); + catch(ApiException e) { + if(HttpStatus.SC_FORBIDDEN == e.getCode()) { + log.warn("Skip vault {} with insufficient permissions", vaultDto); + continue; + } + throw e; + } + catch(AccessException | SecurityFailure e) { + throw new AccessDeniedException(e.getMessage(), e); + } } + return vaults; + } + catch(ApiException e) { + throw new HubExceptionMappingService().map("Listing directory {0} failed", e, directory); } - throw new NotfoundException(directory.getAbsolute()); } @Override diff --git a/hub/src/main/java/cloud/katta/workflows/VaultService.java b/hub/src/main/java/cloud/katta/workflows/VaultService.java index 0bc770e0..aa5b12ea 100644 --- a/hub/src/main/java/cloud/katta/workflows/VaultService.java +++ b/hub/src/main/java/cloud/katta/workflows/VaultService.java @@ -4,18 +4,13 @@ package cloud.katta.workflows; -import ch.cyberduck.core.Host; -import ch.cyberduck.core.OAuthTokens; - import java.util.UUID; import cloud.katta.client.ApiException; -import cloud.katta.client.model.ConfigDto; import cloud.katta.crypto.UserKeys; import cloud.katta.crypto.uvf.UvfAccessTokenPayload; import cloud.katta.crypto.uvf.UvfMetadataPayload; import cloud.katta.model.StorageProfileDtoWrapper; -import cloud.katta.protocols.hub.HubSession; import cloud.katta.workflows.exceptions.AccessException; import cloud.katta.workflows.exceptions.SecurityFailure; @@ -51,17 +46,4 @@ public interface VaultService { * @return Storage profile */ StorageProfileDtoWrapper getVaultStorageProfile(UvfMetadataPayload metadataPayload) throws ApiException, AccessException, SecurityFailure; - - /** - * Prepares (virtual) bookmark for vault to access its configured storage backend. - * - * @param hub Hub API Connection - * @param configDto Hub configuration - * @param vaultId Vault ID - * @param metadataPayload Storage Backend configuration - * @return Configuration - * @throws AccessException Unsupported backend storage protocol - * @throws ApiException Server error accessing storage profile - */ - Host getStorageBackend(final HubSession hub, final ConfigDto configDto, UUID vaultId, final UvfMetadataPayload metadataPayload, final OAuthTokens tokens) throws AccessException, ApiException; } diff --git a/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java b/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java index 4f9eba19..82af08b5 100644 --- a/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java +++ b/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java @@ -4,12 +4,6 @@ package cloud.katta.workflows; -import ch.cyberduck.core.Credentials; -import ch.cyberduck.core.Host; -import ch.cyberduck.core.OAuthTokens; -import ch.cyberduck.core.Profile; -import ch.cyberduck.core.ProtocolFactory; - import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -20,14 +14,11 @@ import cloud.katta.client.ApiException; import cloud.katta.client.api.StorageProfileResourceApi; import cloud.katta.client.api.VaultResourceApi; -import cloud.katta.client.model.ConfigDto; import cloud.katta.client.model.VaultDto; import cloud.katta.crypto.UserKeys; import cloud.katta.crypto.uvf.UvfAccessTokenPayload; import cloud.katta.crypto.uvf.UvfMetadataPayload; -import cloud.katta.crypto.uvf.VaultMetadataJWEBackendDto; import cloud.katta.model.StorageProfileDtoWrapper; -import cloud.katta.protocols.hub.HubAwareProfile; import cloud.katta.protocols.hub.HubSession; import cloud.katta.workflows.exceptions.AccessException; import cloud.katta.workflows.exceptions.SecurityFailure; @@ -36,7 +27,6 @@ import com.nimbusds.jose.jwk.OctetSequenceKey; import static cloud.katta.crypto.uvf.UvfMetadataPayload.UniversalVaultFormatJWKS.memberKeyFromRawKey; -import static cloud.katta.protocols.s3.S3AssumeRoleProtocol.*; public class VaultServiceImpl implements VaultService { private static final Logger log = LogManager.getLogger(VaultServiceImpl.class); @@ -87,41 +77,4 @@ public StorageProfileDtoWrapper getVaultStorageProfile(final UvfMetadataPayload return StorageProfileDtoWrapper.coerce(storageProfileResourceApi .apiStorageprofileProfileIdGet(UUID.fromString(metadataPayload.storage().getProvider()))); } - - @Override - public Host getStorageBackend(final HubSession hub, final ConfigDto configDto, final UUID vaultId, final UvfMetadataPayload vaultMetadataPayload, final OAuthTokens tokens) throws ApiException, AccessException { - final StorageProfileDtoWrapper storageProfile = this.getVaultStorageProfile(vaultMetadataPayload); - log.debug("Read storage profile {}", storageProfile); - final Profile profile; - switch(storageProfile.getProtocol()) { - case S3: - case S3_STS: - final ProtocolFactory protocols = ProtocolFactory.get(); - profile = new HubAwareProfile(hub, protocols.forType(protocols.find(ProtocolFactory.BUNDLED_PROFILE_PREDICATE), Type.s3), - configDto, storageProfile); - log.debug("Loaded profile {}", profile); - break; - default: - throw new AccessException(String.format("Unsupported storage configuration %s", storageProfile.getProtocol().name())); - } - final Host bookmark = new Host(profile); - final VaultMetadataJWEBackendDto vaultStorageMetadata = vaultMetadataPayload.storage(); - log.debug("Configure bookmark for vault {}", vaultStorageMetadata); - bookmark.setNickname(vaultStorageMetadata.getNickname()); - bookmark.setDefaultPath(vaultStorageMetadata.getDefaultPath()); - final Credentials credentials = bookmark.getCredentials(); - credentials.setOauth(tokens); - if(vaultStorageMetadata.getUsername() != null) { - credentials.setUsername(vaultStorageMetadata.getUsername()); - } - if(vaultStorageMetadata.getPassword() != null) { - credentials.setPassword(vaultStorageMetadata.getPassword()); - } - if(profile.getProperties().get(S3_ASSUMEROLE_ROLEARN) != null) { - bookmark.setProperty(OAUTH_TOKENEXCHANGE_VAULT, vaultId.toString()); - } - // region as chosen by user upon vault creation (STS) or as retrieved from bucket (permanent) - bookmark.setRegion(vaultStorageMetadata.getRegion()); - return bookmark; - } } From 69eba9ab1262ef393c4c68388492fcd22ef5305b Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 1 Sep 2025 15:04:21 +0200 Subject: [PATCH 051/133] Resolve deprecated usages. --- .../main/java/cloud/katta/workflows/UserKeysServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hub/src/main/java/cloud/katta/workflows/UserKeysServiceImpl.java b/hub/src/main/java/cloud/katta/workflows/UserKeysServiceImpl.java index 5fc09c71..df01dc8a 100644 --- a/hub/src/main/java/cloud/katta/workflows/UserKeysServiceImpl.java +++ b/hub/src/main/java/cloud/katta/workflows/UserKeysServiceImpl.java @@ -147,7 +147,7 @@ private UserKeys uploadUserKeys(final UserDto me, final UserKeys userKeys, final try { usersResourceApi.apiUsersMePut(me.ecdhPublicKey(userKeys.encodedEcdhPublicKey()) .ecdsaPublicKey(userKeys.encodedEcdsaPublicKey()) - .privateKey(userKeys.encryptWithSetupCode(setupCode)) + .privateKeys(userKeys.encryptWithSetupCode(setupCode)) .setupCode(new SetupCodeJWE(setupCode).encryptForUser(userKeys.ecdhKeyPair().getPublic()))); } catch(JOSEException | JsonProcessingException e) { @@ -175,6 +175,6 @@ private UserKeys uploadDeviceKeys(final String deviceName, final UserKeys userKe } private static boolean validate(final UserDto me) { - return me.getEcdhPublicKey() != null && me.getPrivateKey() != null; + return me.getEcdhPublicKey() != null && me.getPrivateKeys() != null; } } From 154156bca063c46486cd555122bace6131261d3e Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 4 Sep 2025 12:08:35 +0200 Subject: [PATCH 052/133] Default path is bucket name. --- .../java/cloud/katta/protocols/hub/HubVaultListService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java index 6a277ecc..6915a161 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java @@ -74,8 +74,7 @@ public AttributedList list(final Path directory, final ListProgressListene default: throw new VaultException(String.format("Unsupported storage configuration %s", storageProfile.getProtocol().name())); } - final String bucketPrefix = storageProfile.getBucketPrefix(); - final HubUVFVault vault = new HubUVFVault(vaultDto.getId(), String.format("%s%s", bucketPrefix, vaultDto.getId())); + final HubUVFVault vault = new HubUVFVault(vaultDto.getId(), vaultMetadata.storage().getDefaultPath()); registry.add(vault.load(session, prompt)); vaults.add(vault.getHome()); listener.chunk(directory, vaults); From cca05cf7d8fe5418d52fa38d240f35a64934bfeb Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 4 Sep 2025 15:19:51 +0200 Subject: [PATCH 053/133] Logging. --- hub/src/main/java/cloud/katta/protocols/hub/HubSession.java | 1 + 1 file changed, 1 insertion(+) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index fd94eb96..6c632da3 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -167,6 +167,7 @@ public void login(final LoginCallback prompt, final CancelCallback cancel) throw vaults = new HubVaultListService(this, prompt).list(Home.root(), new DisabledListProgressListener()); } finally { + log.debug("Destroyed user keys {}", userKeys); // Short-lived userKeys.destroy(); } From 91b323c9c8366f24cadea42b4f00e94645ff52a9 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 4 Sep 2025 18:11:06 +0200 Subject: [PATCH 054/133] Revert "Set vault ID as vendor key in profile." This reverts commit 4a5d284804c02599421e9235ad8c6f43f5629c17. --- .../katta/protocols/hub/HubAwareProfile.java | 6 +-- .../protocols/hub/HubVaultListService.java | 2 +- .../StorageProfileDtoWrapperDeserializer.java | 3 ++ .../hub/serializer/VaultDtoDeserializer.java | 48 ------------------- 4 files changed, 6 insertions(+), 53 deletions(-) delete mode 100644 hub/src/main/java/cloud/katta/protocols/hub/serializer/VaultDtoDeserializer.java diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubAwareProfile.java b/hub/src/main/java/cloud/katta/protocols/hub/HubAwareProfile.java index 5bfd8cd9..149082b1 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubAwareProfile.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubAwareProfile.java @@ -8,17 +8,15 @@ import ch.cyberduck.core.Protocol; import cloud.katta.client.model.ConfigDto; -import cloud.katta.client.model.VaultDto; import cloud.katta.model.StorageProfileDtoWrapper; import cloud.katta.protocols.hub.serializer.HubConfigDtoDeserializer; import cloud.katta.protocols.hub.serializer.StorageProfileDtoWrapperDeserializer; -import cloud.katta.protocols.hub.serializer.VaultDtoDeserializer; public final class HubAwareProfile extends Profile { private final HubSession hub; - public HubAwareProfile(final HubSession hub, final Protocol parent, final ConfigDto configDto, final StorageProfileDtoWrapper storageProfile, final VaultDto vaultDto) { - super(parent, new HubConfigDtoDeserializer(configDto, new StorageProfileDtoWrapperDeserializer(storageProfile, new VaultDtoDeserializer(vaultDto)))); + public HubAwareProfile(final HubSession hub, final Protocol parent, final ConfigDto configDto, final StorageProfileDtoWrapper storageProfile) { + super(parent, new HubConfigDtoDeserializer(configDto, new StorageProfileDtoWrapperDeserializer(storageProfile))); this.hub = hub; } diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java index 6915a161..ae7dc60f 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java @@ -67,7 +67,7 @@ public AttributedList list(final Path directory, final ListProgressListene case S3_STS: final ProtocolFactory protocols = ProtocolFactory.get(); final Profile profile = new HubAwareProfile(session, protocols.forType(protocols.find(ProtocolFactory.BUNDLED_PROFILE_PREDICATE), Protocol.Type.s3), - session.getConfig(), storageProfile, vaultDto); + session.getConfig(), storageProfile); log.debug("Register profile {}", profile); protocols.register(profile); break; diff --git a/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java b/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java index 9a0d2fcc..7f8ebb83 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java @@ -83,6 +83,8 @@ public String stringForKey(final String key) { return new S3AssumeRoleProtocol().getIdentifier(); } break; + case VENDOR_KEY: + return dto.getId().toString(); case DEFAULT_NICKNAME_KEY: return dto.getName(); case SCHEME_KEY: @@ -119,6 +121,7 @@ public List keys() { final List keys = new ArrayList<>(super.keys()); keys.addAll(Arrays.asList( PROTOCOL_KEY, + VENDOR_KEY, PROPERTIES_KEY, OAUTH_CONFIGURABLE_KEY) ); diff --git a/hub/src/main/java/cloud/katta/protocols/hub/serializer/VaultDtoDeserializer.java b/hub/src/main/java/cloud/katta/protocols/hub/serializer/VaultDtoDeserializer.java deleted file mode 100644 index 43d438a1..00000000 --- a/hub/src/main/java/cloud/katta/protocols/hub/serializer/VaultDtoDeserializer.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2025 shift7 GmbH. All rights reserved. - */ - -package cloud.katta.protocols.hub.serializer; - -import ch.cyberduck.core.serializer.Deserializer; - -import java.util.Collections; -import java.util.List; - -import cloud.katta.client.model.VaultDto; -import com.dd.plist.NSDictionary; - -import static ch.cyberduck.core.Profile.VENDOR_KEY; - -public class VaultDtoDeserializer extends ProxyDeserializer { - - private final VaultDto dto; - - public VaultDtoDeserializer(final VaultDto dto) { - this(dto, ProxyDeserializer.empty()); - } - - /** - * @param dto Storage configuration - */ - public VaultDtoDeserializer(final VaultDto dto, final Deserializer parent) { - super(parent); - this.dto = dto; - } - - @Override - public String stringForKey(final String key) { - // default profile and possible regions for UI: - switch(key) { - case VENDOR_KEY: - // Allow lookup of profile with vault id - return dto.getId().toString(); - } - return super.stringForKey(key); - } - - @Override - public List keys() { - return Collections.singletonList(VENDOR_KEY); - } -} From edc25b1dff2d344a7d5446759f79f264e2c1739d Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 4 Sep 2025 21:48:52 +0200 Subject: [PATCH 055/133] Logging. --- .../protocols/hub/serializer/ProxyDeserializer.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/serializer/ProxyDeserializer.java b/hub/src/main/java/cloud/katta/protocols/hub/serializer/ProxyDeserializer.java index 980f85fb..f3676116 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/serializer/ProxyDeserializer.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/serializer/ProxyDeserializer.java @@ -56,31 +56,31 @@ public static Deserializer empty() { return new Deserializer() { @Override public String stringForKey(final String key) { - log.warn("Unknown key {}", key); + log.trace("Unknown key {}", key); return null; } @Override public T objectForKey(final String key) { - log.warn("Unknown key {}", key); + log.trace("Unknown key {}", key); return null; } @Override public List listForKey(final String key) { - log.warn("Unknown key {}", key); + log.trace("Unknown key {}", key); return Collections.emptyList(); } @Override public Map mapForKey(final String key) { - log.warn("Unknown key {}", key); + log.trace("Unknown key {}", key); return Collections.emptyMap(); } @Override public Boolean booleanForKey(final String key) { - log.warn("Unknown key {}", key); + log.trace("Unknown key {}", key); return null; } From c86b6e44cf24210af8af609bf8b476c3be2e0af3 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 11 Sep 2025 10:34:02 +0200 Subject: [PATCH 056/133] Handle load failure. --- .../katta/protocols/hub/HubVaultListService.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java index ae7dc60f..84b76880 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java @@ -17,6 +17,7 @@ import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.vault.VaultException; import ch.cyberduck.core.vault.VaultRegistry; +import ch.cyberduck.core.vault.VaultUnlockCancelException; import org.apache.http.HttpStatus; import org.apache.logging.log4j.LogManager; @@ -75,13 +76,19 @@ public AttributedList list(final Path directory, final ListProgressListene throw new VaultException(String.format("Unsupported storage configuration %s", storageProfile.getProtocol().name())); } final HubUVFVault vault = new HubUVFVault(vaultDto.getId(), vaultMetadata.storage().getDefaultPath()); - registry.add(vault.load(session, prompt)); - vaults.add(vault.getHome()); - listener.chunk(directory, vaults); + try { + registry.add(vault.load(session, prompt)); + vaults.add(vault.getHome()); + listener.chunk(directory, vaults); + } + catch(VaultUnlockCancelException e) { + log.warn("Skip vault {} with failure {} loading", vaultDto, e); + continue; + } } catch(ApiException e) { if(HttpStatus.SC_FORBIDDEN == e.getCode()) { - log.warn("Skip vault {} with insufficient permissions", vaultDto); + log.warn("Skip vault {} with insufficient permissions {}", vaultDto, e); continue; } throw e; From 7963bb4bfaed7bc0eb830f025e711203d28d2086 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 29 Aug 2025 14:27:55 +0200 Subject: [PATCH 057/133] Add required argument. --- hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java | 1 + 1 file changed, 1 insertion(+) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 50799364..8ea8504e 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -20,6 +20,7 @@ import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.exception.InteroperabilityException; import ch.cyberduck.core.exception.UnsupportedException; +import ch.cyberduck.core.features.Write; import ch.cyberduck.core.features.AttributesFinder; import ch.cyberduck.core.proxy.ProxyFactory; import ch.cyberduck.core.ssl.X509KeyManager; From 5bb434ddbe1c3d06bf3b17a9f71796bc51da6b4e Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 29 Aug 2025 16:30:25 +0200 Subject: [PATCH 058/133] Fix test. Attributes retrieved for bucket have region set. --- .../test/java/cloud/katta/core/AbstractHubSynchronizeTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java index 08ac5070..e38df715 100644 --- a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java +++ b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java @@ -245,7 +245,6 @@ public void test03AddVault(final HubTestConfig config) throws Exception { assertInstanceOf(HubVaultRegistry.class, vaultRegistry); { assertNotNull(vaults.find(new SimplePathPredicate(bucket))); - assertTrue(hubSession.getFeature(Find.class).find(bucket)); assertEquals(config.vault.region, hubSession.getFeature(AttributesFinder.class).find(bucket).getRegion()); From d0c91fbadbe9643697bd3248877b2803e64311dc Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 12 May 2025 16:52:53 +0200 Subject: [PATCH 059/133] Implement location service as combo of storage profile configuration name and bucket region. --- .../cloud/katta/protocols/hub/HubSession.java | 4 + .../hub/HubStorageLocationService.java | 79 +++++++++++++++++++ .../protocols/hub/HubVaultListService.java | 1 + 3 files changed, 84 insertions(+) create mode 100644 hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 6c632da3..0fee38b9 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -16,6 +16,7 @@ import ch.cyberduck.core.features.Directory; import ch.cyberduck.core.features.Find; import ch.cyberduck.core.features.Home; +import ch.cyberduck.core.features.Location; import ch.cyberduck.core.features.Move; import ch.cyberduck.core.features.Read; import ch.cyberduck.core.features.Scheduler; @@ -242,6 +243,9 @@ public T _getFeature(final Class type) { if(type == AttributesFinder.class) { return (T) (AttributesFinder) (f, l) -> f.attributes(); } + if(type == Location.class) { + return (T) new HubStorageLocationService(this); + } if(type == Find.class) { return (T) (Find) (file, listener) -> new SimplePathPredicate(registry.find(HubSession.this, file).getHome()).test(file); } diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java new file mode 100644 index 00000000..4e4d3be8 --- /dev/null +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2025 shift7 GmbH. All rights reserved. + */ + +package cloud.katta.protocols.hub; + +import ch.cyberduck.core.Path; +import ch.cyberduck.core.features.Location; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import cloud.katta.client.ApiException; +import cloud.katta.client.api.StorageProfileResourceApi; +import cloud.katta.client.model.StorageProfileDto; +import cloud.katta.model.StorageProfileDtoWrapper; + +public class HubStorageLocationService implements Location { + + private final HubSession session; + + public HubStorageLocationService(final HubSession session) { + this.session = session; + } + + @Override + public Name getDefault() { + return Location.unknown; + } + + @Override + public Set getLocations() { + try { + final Set regions = new HashSet<>(); + final List storageProfileDtos = new StorageProfileResourceApi(session.getClient()) + .apiStorageprofileGet(false); + for(StorageProfileDto storageProfileDto : storageProfileDtos) { + final StorageProfileDtoWrapper storageProfile = StorageProfileDtoWrapper.coerce(storageProfileDto); + for(String region : storageProfile.getRegions()) { + regions.add(new StorageLocation(storageProfile.getName(), region)); + } + } + return regions; + } + catch(ApiException e) { + throw new RuntimeException(e); + } + } + + @Override + public Name getLocation(final Path file) { + return StorageLocation.fromIdentifier(file.attributes().getRegion()); + } + + public static final class StorageLocation extends Name { + private final String storageProfile; + private final String region; + + public StorageLocation(final String storageProfile, final String region) { + super(String.format("%s-%s", storageProfile, region)); + this.storageProfile = storageProfile; + this.region = region; + } + + @Override + public String toString() { + return String.format("%s (%s)", storageProfile, region); + } + + public static Name fromIdentifier(final String identifier) { + final String[] parts = identifier.split("-"); + if(parts.length != 2) { + return Location.unknown; + } + return new StorageLocation(parts[0], parts[1]); + } + } +} diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java index 84b76880..daf1af6a 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java @@ -30,6 +30,7 @@ import cloud.katta.client.model.VaultDto; import cloud.katta.crypto.uvf.UvfMetadataPayload; import cloud.katta.model.StorageProfileDtoWrapper; +import cloud.katta.crypto.uvf.VaultMetadataJWEBackendDto; import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService; import cloud.katta.workflows.VaultServiceImpl; import cloud.katta.workflows.exceptions.AccessException; From 88e05b88f477a5f4709b5ec124e05360517954a2 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 26 Jun 2025 14:30:02 +0200 Subject: [PATCH 060/133] Log failure. --- .../protocols/hub/HubStorageLocationService.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java index 4e4d3be8..0318a8d1 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java @@ -7,6 +7,10 @@ import ch.cyberduck.core.Path; import ch.cyberduck.core.features.Location; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -17,6 +21,7 @@ import cloud.katta.model.StorageProfileDtoWrapper; public class HubStorageLocationService implements Location { + private static final Logger log = LogManager.getLogger(HubStorageLocationService.class); private final HubSession session; @@ -38,13 +43,14 @@ public Set getLocations() { for(StorageProfileDto storageProfileDto : storageProfileDtos) { final StorageProfileDtoWrapper storageProfile = StorageProfileDtoWrapper.coerce(storageProfileDto); for(String region : storageProfile.getRegions()) { - regions.add(new StorageLocation(storageProfile.getName(), region)); + regions.add(new StorageLocation(storageProfile.getId().toString(), region)); } } return regions; } catch(ApiException e) { - throw new RuntimeException(e); + log.warn("Failed to retrieve storage locations from server", e); + return Collections.emptySet(); } } @@ -54,6 +60,9 @@ public Name getLocation(final Path file) { } public static final class StorageLocation extends Name { + /** + * UUID of storage profile configuration + */ private final String storageProfile; private final String region; From fb91071223d9e5ee6864b9f2bf8818eec4698896 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 1 Sep 2025 14:20:38 +0200 Subject: [PATCH 061/133] Adtop updated interface. --- .../cloud/katta/protocols/hub/HubStorageLocationService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java index 0318a8d1..06d8ebd6 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java @@ -30,12 +30,12 @@ public HubStorageLocationService(final HubSession session) { } @Override - public Name getDefault() { + public Name getDefault(final Path file) { return Location.unknown; } @Override - public Set getLocations() { + public Set getLocations(final Path file) { try { final Set regions = new HashSet<>(); final List storageProfileDtos = new StorageProfileResourceApi(session.getClient()) From b3430906e49d58a4e58f4838fd664ecbf7e2d4de Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 4 Sep 2025 14:05:55 +0200 Subject: [PATCH 062/133] Default to allow configuration of transfer acceleration. --- .../java/cloud/katta/workflows/CreateVaultService.java | 7 +------ .../sts_create_bucket_inline_policy_template.json | 4 +++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/hub/src/main/java/cloud/katta/workflows/CreateVaultService.java b/hub/src/main/java/cloud/katta/workflows/CreateVaultService.java index e1edd0d4..3800f304 100644 --- a/hub/src/main/java/cloud/katta/workflows/CreateVaultService.java +++ b/hub/src/main/java/cloud/katta/workflows/CreateVaultService.java @@ -13,7 +13,6 @@ import ch.cyberduck.core.OAuthTokens; import ch.cyberduck.core.PasswordStoreFactory; import ch.cyberduck.core.Path; -import ch.cyberduck.core.ProtocolFactory; import ch.cyberduck.core.TemporaryAccessTokens; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.proxy.DisabledProxyFinder; @@ -213,11 +212,7 @@ TemporaryAccessTokens getSTSTokensFromAccessTokenWithCreateBucketInlinePolicy(fi request.setWebIdentityToken(token); - String inlinePolicy = IOUtils.toString(CreateVaultService.class.getResourceAsStream("/sts_create_bucket_inline_policy_template.json"), Charset.defaultCharset()).replace("{}", bucketName); - if((bucketAcceleration != null) && bucketAcceleration) { - inlinePolicy = inlinePolicy.replace("s3:PutEncryptionConfiguration\"", "s3:PutEncryptionConfiguration\", \"s3:GetAccelerateConfiguration\",\n \"s3:PutAccelerateConfiguration\""); - } - + final String inlinePolicy = IOUtils.toString(CreateVaultService.class.getResourceAsStream("/sts_create_bucket_inline_policy_template.json"), Charset.defaultCharset()).replace("{}", bucketName); request.setPolicy(inlinePolicy); request.setRoleArn(roleArn); request.setRoleSessionName(roleSessionName); diff --git a/hub/src/main/resources/sts_create_bucket_inline_policy_template.json b/hub/src/main/resources/sts_create_bucket_inline_policy_template.json index 72c96460..d94b2374 100644 --- a/hub/src/main/resources/sts_create_bucket_inline_policy_template.json +++ b/hub/src/main/resources/sts_create_bucket_inline_policy_template.json @@ -9,7 +9,9 @@ "s3:PutBucketVersioning", "s3:GetBucketVersioning", "s3:GetEncryptionConfiguration", - "s3:PutEncryptionConfiguration" + "s3:PutEncryptionConfiguration", + "s3:GetAccelerateConfiguration", + "s3:PutAccelerateConfiguration" ], "Resource": "arn:aws:s3:::{}" }, From 34ff3ddb75392ac592934cb9251750782dae4ca9 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 4 Sep 2025 14:09:16 +0200 Subject: [PATCH 063/133] Numbered formatting. --- .../main/java/cloud/katta/workflows/CreateVaultService.java | 5 ++++- .../resources/sts_create_bucket_inline_policy_template.json | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/hub/src/main/java/cloud/katta/workflows/CreateVaultService.java b/hub/src/main/java/cloud/katta/workflows/CreateVaultService.java index 3800f304..ba657dd9 100644 --- a/hub/src/main/java/cloud/katta/workflows/CreateVaultService.java +++ b/hub/src/main/java/cloud/katta/workflows/CreateVaultService.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.nio.charset.Charset; +import java.text.MessageFormat; import java.util.Base64; import java.util.Collections; import java.util.EnumSet; @@ -212,7 +213,9 @@ TemporaryAccessTokens getSTSTokensFromAccessTokenWithCreateBucketInlinePolicy(fi request.setWebIdentityToken(token); - final String inlinePolicy = IOUtils.toString(CreateVaultService.class.getResourceAsStream("/sts_create_bucket_inline_policy_template.json"), Charset.defaultCharset()).replace("{}", bucketName); + final String inlinePolicy = MessageFormat.format( + IOUtils.toString(CreateVaultService.class.getResourceAsStream("/sts_create_bucket_inline_policy_template.json"), Charset.defaultCharset()), + bucketName); request.setPolicy(inlinePolicy); request.setRoleArn(roleArn); request.setRoleSessionName(roleSessionName); diff --git a/hub/src/main/resources/sts_create_bucket_inline_policy_template.json b/hub/src/main/resources/sts_create_bucket_inline_policy_template.json index d94b2374..15a46708 100644 --- a/hub/src/main/resources/sts_create_bucket_inline_policy_template.json +++ b/hub/src/main/resources/sts_create_bucket_inline_policy_template.json @@ -21,8 +21,8 @@ "s3:PutObject" ], "Resource": [ - "arn:aws:s3:::{}/*.uvf", - "arn:aws:s3:::{}/*/" + "arn:aws:s3:::{0}/*.uvf", + "arn:aws:s3:::{0}/*/" ] } ] From b58cd4b9419bbc9eb67b44a2e3eae52970ab56c6 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 4 Sep 2025 14:12:07 +0200 Subject: [PATCH 064/133] Add constructors. --- .../cloud/katta/protocols/hub/HubVaultRegistry.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultRegistry.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultRegistry.java index 16a03b64..223acd25 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultRegistry.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultRegistry.java @@ -5,13 +5,23 @@ package cloud.katta.protocols.hub; import ch.cyberduck.core.DisabledPasswordCallback; +import ch.cyberduck.core.PasswordCallback; import ch.cyberduck.core.Session; +import ch.cyberduck.core.features.Vault; import ch.cyberduck.core.vault.DefaultVaultRegistry; public class HubVaultRegistry extends DefaultVaultRegistry { public HubVaultRegistry() { - super(new DisabledPasswordCallback()); + this(new DisabledPasswordCallback()); + } + + public HubVaultRegistry(final PasswordCallback prompt) { + super(prompt); + } + + public HubVaultRegistry(final PasswordCallback prompt, final Vault... vaults) { + super(prompt, vaults); } @Override From b39e4d71306aa98f390a3df05fb73f2a1038a31c Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 4 Sep 2025 14:39:15 +0200 Subject: [PATCH 065/133] Javadoc. --- .../katta/protocols/hub/HubStorageLocationService.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java index 06d8ebd6..88a20401 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java @@ -64,6 +64,9 @@ public static final class StorageLocation extends Name { * UUID of storage profile configuration */ private final String storageProfile; + /** + * AWS location + */ private final String region; public StorageLocation(final String storageProfile, final String region) { @@ -77,6 +80,11 @@ public String toString() { return String.format("%s (%s)", storageProfile, region); } + /** + * Parse a storage location from an identifier containing storage profile and AWS location. + * @param identifier Storage profile identifier and AWS region separated by dash + * @return Location with storage profile as identifier and AWS location as region + */ public static Name fromIdentifier(final String identifier) { final String[] parts = identifier.split("-"); if(parts.length != 2) { From 0da90c9382b12d35d3c47e7f63e41c022608595a Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 4 Sep 2025 14:59:23 +0200 Subject: [PATCH 066/133] Add accessor. --- .../katta/protocols/hub/HubStorageLocationService.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java index 88a20401..d159c82f 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java @@ -75,6 +75,10 @@ public StorageLocation(final String storageProfile, final String region) { this.region = region; } + public String getRegion() { + return region; + } + @Override public String toString() { return String.format("%s (%s)", storageProfile, region); @@ -85,10 +89,10 @@ public String toString() { * @param identifier Storage profile identifier and AWS region separated by dash * @return Location with storage profile as identifier and AWS location as region */ - public static Name fromIdentifier(final String identifier) { + public static StorageLocation fromIdentifier(final String identifier) { final String[] parts = identifier.split("-"); if(parts.length != 2) { - return Location.unknown; + return new StorageLocation(identifier, null); } return new StorageLocation(parts[0], parts[1]); } From 2194d6a8e5c88368a2813ffbdff23197d4b4ab90 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 4 Sep 2025 15:15:19 +0200 Subject: [PATCH 067/133] Allow to create vault. --- .../katta/protocols/hub/HubUVFVault.java | 136 +++++- .../protocols/hub/HubVaultListService.java | 7 +- .../katta/workflows/CreateVaultService.java | 316 -------------- ..._create_bucket_inline_policy_template.json | 29 -- .../core/AbstractHubSynchronizeTest.java | 35 +- .../workflows/AbstractHubWorkflowTest.java | 21 +- .../workflows/CreateVaultServiceTest.java | 116 ----- .../CreateVaultBookmarkController.java | 410 ------------------ 8 files changed, 151 insertions(+), 919 deletions(-) delete mode 100644 hub/src/main/java/cloud/katta/workflows/CreateVaultService.java delete mode 100644 hub/src/main/resources/sts_create_bucket_inline_policy_template.json delete mode 100644 hub/src/test/java/cloud/katta/workflows/CreateVaultServiceTest.java delete mode 100644 osx/src/main/java/cloud/katta/controller/CreateVaultBookmarkController.java diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 8ea8504e..29fb1710 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -4,45 +4,53 @@ package cloud.katta.protocols.hub; -import ch.cyberduck.core.Credentials; -import ch.cyberduck.core.DisabledCancelCallback; -import ch.cyberduck.core.DisabledHostKeyCallback; -import ch.cyberduck.core.DisabledLoginCallback; -import ch.cyberduck.core.Host; -import ch.cyberduck.core.PasswordCallback; -import ch.cyberduck.core.Path; -import ch.cyberduck.core.PathAttributes; -import ch.cyberduck.core.Protocol; -import ch.cyberduck.core.ProtocolFactory; -import ch.cyberduck.core.Session; -import ch.cyberduck.core.SessionFactory; +import ch.cyberduck.core.*; +import ch.cyberduck.core.cryptomator.ContentWriter; import ch.cyberduck.core.cryptomator.UVFVault; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.exception.InteroperabilityException; import ch.cyberduck.core.exception.UnsupportedException; import ch.cyberduck.core.features.Write; import ch.cyberduck.core.features.AttributesFinder; +import ch.cyberduck.core.features.Directory; +import ch.cyberduck.core.features.Write; +import ch.cyberduck.core.preferences.PreferencesFactory; import ch.cyberduck.core.proxy.ProxyFactory; import ch.cyberduck.core.ssl.X509KeyManager; import ch.cyberduck.core.ssl.X509TrustManager; +import ch.cyberduck.core.transfer.TransferStatus; +import ch.cyberduck.core.vault.VaultCredentials; import ch.cyberduck.core.vault.VaultUnlockCancelException; import org.apache.http.HttpStatus; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.joda.time.DateTime; +import java.nio.charset.StandardCharsets; +import java.util.Collections; import java.util.EnumSet; import java.util.UUID; import cloud.katta.client.ApiException; +import cloud.katta.client.api.UsersResourceApi; +import cloud.katta.client.api.VaultResourceApi; +import cloud.katta.client.model.UserDto; +import cloud.katta.client.model.VaultDto; +import cloud.katta.crypto.DeviceKeys; +import cloud.katta.crypto.UserKeys; import cloud.katta.crypto.uvf.UvfMetadataPayload; import cloud.katta.crypto.uvf.UvfMetadataPayloadPasswordCallback; +import cloud.katta.crypto.uvf.VaultMetadataJWEAutomaticAccessGrantDto; import cloud.katta.crypto.uvf.VaultMetadataJWEBackendDto; import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService; +import cloud.katta.workflows.DeviceKeysServiceImpl; +import cloud.katta.workflows.UserKeysServiceImpl; import cloud.katta.workflows.VaultServiceImpl; import cloud.katta.workflows.exceptions.AccessException; import cloud.katta.workflows.exceptions.SecurityFailure; import com.fasterxml.jackson.core.JsonProcessingException; +import com.nimbusds.jose.JOSEException; import static cloud.katta.protocols.s3.S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_VAULT; @@ -59,14 +67,18 @@ public class HubUVFVault extends UVFVault { */ private Session storage; + public HubUVFVault(final Path home) { + this(home, null, null, null); + } + /** * Constructor for factory creating new vault * * @param home Bucket */ - public HubUVFVault(final Path home) { + public HubUVFVault(final Path home, final String masterkey, final String config, final byte[] pepper) { super(home); - this.vaultId = UUID.fromString(home.getName()); + this.vaultId = UUID.fromString(new UUIDRandomStringService().random()); } /** @@ -75,8 +87,8 @@ public HubUVFVault(final Path home) { * @param vaultId Vault ID Used to lookup profile * @param bucket Bucket name */ - public HubUVFVault(final UUID vaultId, final String bucket) { - super(new Path(bucket, EnumSet.of(Path.Type.directory, Path.Type.volume))); + public HubUVFVault(final UUID vaultId, final Path bucket) { + super(bucket); this.vaultId = vaultId; } @@ -85,7 +97,7 @@ public Session getStorage() { } @Override - public T getFeature(final Session ignore, final Class type, final T delegate) throws UnsupportedException { + public T getFeature(final Session hub, final Class type, final T delegate) throws UnsupportedException { log.debug("Delegate to {} for feature {}", storage, type); // Ignore feature implementation but delegate to storage backend final T feature = storage._getFeature(type); @@ -108,6 +120,94 @@ public synchronized void close() { super.close(); } + @Override + public Path create(final Session session, final String region, final VaultCredentials credentials) throws BackgroundException { + try { + final HubStorageLocationService.StorageLocation location = HubStorageLocationService.StorageLocation.fromIdentifier(region); + final String storageProfile = location.getProfile(); + final UvfMetadataPayload metadataPayload = UvfMetadataPayload.create() + .withStorage(new VaultMetadataJWEBackendDto() + .provider(storageProfile) + .defaultPath(session.getFeature(PathContainerService.class).getContainer(home).getName()) + .region(location.getRegion()) + .nickname(null != home.attributes().getDisplayname() ? home.attributes().getDisplayname() : "Vault")) + .withAutomaticAccessGrant(new VaultMetadataJWEAutomaticAccessGrantDto() + .enabled(true) + .maxWotDepth(null)); + log.debug("Created metadata JWE {}", metadataPayload); + final UvfMetadataPayload.UniversalVaultFormatJWKS jwks = UvfMetadataPayload.createKeys(); + final VaultDto vaultDto = new VaultDto() + .id(vaultId) + .name(metadataPayload.storage().getNickname()) + .description(null) + .archived(false) + .creationTime(DateTime.now()) + .uvfMetadataFile(metadataPayload.encrypt( + String.format("%s/api", new HostUrlProvider(false, true).get(session.getHost())), + vaultId, + jwks.toJWKSet() + )) + .uvfKeySet(jwks.serializePublicRecoverykey()); + final HubSession hub = (HubSession) session; + // Create vault in Hub + final VaultResourceApi vaultResourceApi = new VaultResourceApi(hub.getClient()); + log.debug("Create vault {}", vaultDto); + vaultResourceApi.apiVaultsVaultIdPut(vaultDto.getId(), vaultDto); + // Upload JWE + log.debug("Grant access to vault {}", vaultDto); + final UserDto userDto = new UsersResourceApi(hub.getClient()).apiUsersMeGet(false, false); + final DeviceKeys deviceKeys = new DeviceKeysServiceImpl().getDeviceKeys(session.getHost()); + final UserKeys userKeys = new UserKeysServiceImpl(hub).getUserKeys(session.getHost(), hub.getMe(), deviceKeys); + vaultResourceApi.apiVaultsVaultIdAccessTokensPost(vaultDto.getId(), + Collections.singletonMap(userDto.getId(), jwks.toOwnerAccessToken().encryptForUser(userKeys.ecdhKeyPair().getPublic()))); + // Upload vault template to storage + final Protocol profile = ProtocolFactory.get().forName(storageProfile); + log.debug("Loaded profile {} for vault {}", profile, this); + final Host bookmark = new Host(profile); + bookmark.setProperty(OAUTH_TOKENEXCHANGE_VAULT, vaultId.toString()); + bookmark.setRegion(location.getRegion()); + log.debug("Configured {} for vault {}", bookmark, this); + storage = SessionFactory.create(bookmark, session.getFeature(X509TrustManager.class), session.getFeature(X509KeyManager.class)); + log.debug("Connect to {}", storage); + storage.open(ProxyFactory.get(), new DisabledHostKeyCallback(), new DisabledLoginCallback(), new DisabledCancelCallback()); + storage.login(new DisabledLoginCallback(), new DisabledCancelCallback()); + log.debug("Upload vault template to {}", storage); + final Path vault; + if(false) { + vault = super.create(storage, region, credentials); + } + else { // Obsolete when implemented in super + final Directory directory = (Directory) storage._getFeature(Directory.class); + log.debug("Create vault root directory at {}", home); + final TransferStatus status = (new TransferStatus()).setRegion(region); + vault = directory.mkdir(storage._getFeature(Write.class), home, status); + + final String hashedRootDirId = metadataPayload.computeRootDirIdHash(); + final Path dataDir = new Path(vault, "d", EnumSet.of(Path.Type.directory)); + final Path firstLevel = new Path(dataDir, hashedRootDirId.substring(0, 2), EnumSet.of(Path.Type.directory)); + final Path secondLevel = new Path(firstLevel, hashedRootDirId.substring(2), EnumSet.of(Path.Type.directory)); + + directory.mkdir(storage._getFeature(Write.class), dataDir, status); + directory.mkdir(storage._getFeature(Write.class), firstLevel, status); + directory.mkdir(storage._getFeature(Write.class), secondLevel, status); + + // vault.uvf + new ContentWriter(storage).write(new Path(home, PreferencesFactory.get().getProperty("cryptomator.vault.config.filename"), + EnumSet.of(Path.Type.file, Path.Type.vault)), vaultDto.getUvfMetadataFile().getBytes(StandardCharsets.US_ASCII)); + // dir.uvf + new ContentWriter(storage).write(new Path(secondLevel, "dir.uvf", EnumSet.of(Path.Type.file)), + metadataPayload.computeRootDirUvf()); + } + return vault; + } + catch(JOSEException | JsonProcessingException | AccessException | SecurityFailure e) { + throw new InteroperabilityException(e.getMessage(), e); + } + catch(ApiException e) { + throw new HubExceptionMappingService().map(e); + } + } + /** * * @param session Hub Connection @@ -121,7 +221,7 @@ public HubUVFVault load(final Session session, final PasswordCallback prompt) // Find storage configuration in vault metadata final VaultServiceImpl vaultService = new VaultServiceImpl(hub); final UvfMetadataPayload vaultMetadata = vaultService.getVaultMetadataJWE(vaultId, hub.getUserKeys()); - final Protocol profile = ProtocolFactory.get().forName(vaultId.toString()); + final Protocol profile = ProtocolFactory.get().forName(vaultMetadata.storage().getProvider()); log.debug("Loaded profile {} for vault {}", profile, this); final Credentials credentials = new Credentials(hub.getHost().getCredentials()); log.debug("Copy credentials {}", credentials); diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java index daf1af6a..0aad78eb 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java @@ -10,6 +10,7 @@ import ch.cyberduck.core.LocaleFactory; import ch.cyberduck.core.PasswordCallback; import ch.cyberduck.core.Path; +import ch.cyberduck.core.PathAttributes; import ch.cyberduck.core.Profile; import ch.cyberduck.core.Protocol; import ch.cyberduck.core.ProtocolFactory; @@ -24,13 +25,13 @@ import org.apache.logging.log4j.Logger; import java.text.MessageFormat; +import java.util.EnumSet; import cloud.katta.client.ApiException; import cloud.katta.client.api.VaultResourceApi; import cloud.katta.client.model.VaultDto; import cloud.katta.crypto.uvf.UvfMetadataPayload; import cloud.katta.model.StorageProfileDtoWrapper; -import cloud.katta.crypto.uvf.VaultMetadataJWEBackendDto; import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService; import cloud.katta.workflows.VaultServiceImpl; import cloud.katta.workflows.exceptions.AccessException; @@ -76,7 +77,9 @@ public AttributedList list(final Path directory, final ListProgressListene default: throw new VaultException(String.format("Unsupported storage configuration %s", storageProfile.getProtocol().name())); } - final HubUVFVault vault = new HubUVFVault(vaultDto.getId(), vaultMetadata.storage().getDefaultPath()); + final HubUVFVault vault = new HubUVFVault(vaultDto.getId(), + new Path(vaultMetadata.storage().getDefaultPath(), EnumSet.of(Path.Type.directory, Path.Type.volume), + new PathAttributes().setDisplayname(vaultMetadata.storage().getNickname()))); try { registry.add(vault.load(session, prompt)); vaults.add(vault.getHome()); diff --git a/hub/src/main/java/cloud/katta/workflows/CreateVaultService.java b/hub/src/main/java/cloud/katta/workflows/CreateVaultService.java deleted file mode 100644 index ba657dd9..00000000 --- a/hub/src/main/java/cloud/katta/workflows/CreateVaultService.java +++ /dev/null @@ -1,316 +0,0 @@ -/* - * Copyright (c) 2025 shift7 GmbH. All rights reserved. - */ - -package cloud.katta.workflows; - -import ch.cyberduck.core.DisabledCancelCallback; -import ch.cyberduck.core.DisabledHostKeyCallback; -import ch.cyberduck.core.DisabledLoginCallback; -import ch.cyberduck.core.Host; -import ch.cyberduck.core.HostPasswordStore; -import ch.cyberduck.core.HostUrlProvider; -import ch.cyberduck.core.OAuthTokens; -import ch.cyberduck.core.PasswordStoreFactory; -import ch.cyberduck.core.Path; -import ch.cyberduck.core.TemporaryAccessTokens; -import ch.cyberduck.core.exception.BackgroundException; -import ch.cyberduck.core.proxy.DisabledProxyFinder; -import ch.cyberduck.core.s3.S3Session; - -import org.apache.commons.io.IOUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.joda.time.DateTime; - -import java.io.IOException; -import java.nio.charset.Charset; -import java.text.MessageFormat; -import java.util.Base64; -import java.util.Collections; -import java.util.EnumSet; -import java.util.UUID; - -import cloud.katta.client.ApiException; -import cloud.katta.client.api.ConfigResourceApi; -import cloud.katta.client.api.StorageProfileResourceApi; -import cloud.katta.client.api.StorageResourceApi; -import cloud.katta.client.api.UsersResourceApi; -import cloud.katta.client.api.VaultResourceApi; -import cloud.katta.client.model.CreateS3STSBucketDto; -import cloud.katta.client.model.Protocol; -import cloud.katta.client.model.UserDto; -import cloud.katta.client.model.VaultDto; -import cloud.katta.crypto.UserKeys; -import cloud.katta.crypto.uvf.UvfMetadataPayload; -import cloud.katta.crypto.uvf.VaultMetadataJWEAutomaticAccessGrantDto; -import cloud.katta.crypto.uvf.VaultMetadataJWEBackendDto; -import cloud.katta.model.StorageProfileDtoWrapper; -import cloud.katta.protocols.hub.HubSession; -import cloud.katta.protocols.hub.HubUVFVault; -import cloud.katta.workflows.exceptions.AccessException; -import cloud.katta.workflows.exceptions.SecurityFailure; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.AnonymousAWSCredentials; -import com.amazonaws.client.builder.AwsClientBuilder; -import com.amazonaws.services.securitytoken.AWSSecurityTokenService; -import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder; -import com.amazonaws.services.securitytoken.model.AssumeRoleWithWebIdentityRequest; -import com.amazonaws.services.securitytoken.model.AssumeRoleWithWebIdentityResult; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.nimbusds.jose.JOSEException; - -/** - * Create a vault in hub from CreateVaultModel. - */ -public class CreateVaultService { - private static final Logger log = LogManager.getLogger(CreateVaultService.class); - - private final HubSession hubSession; - private final ConfigResourceApi configResource; - private final VaultResourceApi vaultResource; - private final StorageProfileResourceApi storageProfileResource; - private final StorageResourceApi storageResource; - private final UsersResourceApi users; - private final TemplateUploadService templateUploadService; - private final STSInlinePolicyService stsInlinePolicyService; - - public CreateVaultService(final HubSession hubSession) { - this(hubSession, new ConfigResourceApi(hubSession.getClient()), new VaultResourceApi(hubSession.getClient()), new StorageProfileResourceApi(hubSession.getClient()), new UsersResourceApi(hubSession.getClient()), new StorageResourceApi(hubSession.getClient()), new TemplateUploadService(), new STSInlinePolicyService()); - } - - CreateVaultService(final HubSession hubSession, final ConfigResourceApi configResource, final VaultResourceApi vaultResource, final StorageProfileResourceApi storageProfileResource, final UsersResourceApi users, final StorageResourceApi storageResource, final TemplateUploadService templateUploadService, final STSInlinePolicyService stsInlinePolicyService) { - this.hubSession = hubSession; - this.configResource = configResource; - this.vaultResource = vaultResource; - this.storageProfileResource = storageProfileResource; - this.storageResource = storageResource; - this.users = users; - this.templateUploadService = templateUploadService; - this.stsInlinePolicyService = stsInlinePolicyService; - } - - public void createVault(final UserKeys userKeys, final StorageProfileDtoWrapper storageProfileWrapper, final CreateVaultModel vaultModel) throws ApiException, AccessException, SecurityFailure, BackgroundException { - try { - final UvfMetadataPayload.UniversalVaultFormatJWKS jwks = UvfMetadataPayload.createKeys(); - final UvfMetadataPayload metadataPayload = UvfMetadataPayload.create() - .withStorage(new VaultMetadataJWEBackendDto() - .provider(storageProfileWrapper.getId().toString()) - .defaultPath(storageProfileWrapper.getProtocol() == Protocol.S3_STS ? storageProfileWrapper.getBucketPrefix() + vaultModel.vaultId() : vaultModel.bucketName()) - .region(vaultModel.region()) - .nickname(vaultModel.vaultName()) - .username(vaultModel.accessKeyId()) - .password(vaultModel.secretKey())) - .withAutomaticAccessGrant(new VaultMetadataJWEAutomaticAccessGrantDto() - .enabled(vaultModel.automaticAccessGrant()) - .maxWotDepth(vaultModel.maxWotLevel()) - ); - log.debug("Created metadata JWE {}", metadataPayload); - final String uvfMetadataFile = metadataPayload.encrypt( - String.format("%s/api", new HostUrlProvider(false, true).get(hubSession.getHost())), - vaultModel.vaultId(), - jwks.toJWKSet() - ); - final VaultDto vaultDto = new VaultDto() - .id(vaultModel.vaultId()) - .name(metadataPayload.storage().getNickname()) - .description(vaultModel.vaultDescription()) - .archived(false) - .creationTime(DateTime.now()) - .uvfMetadataFile(uvfMetadataFile) - .uvfKeySet(jwks.serializePublicRecoverykey()); - - // create storage dto - final String hashedRootDirId = metadataPayload.computeRootDirIdHash(); - final CreateS3STSBucketDto storageDto = new CreateS3STSBucketDto() - .vaultId(vaultModel.vaultId().toString()) - .storageConfigId(storageProfileWrapper.getId()) - .vaultUvf(uvfMetadataFile) - .rootDirHash(hashedRootDirId) - .dirUvf(Base64.getUrlEncoder().encodeToString(metadataPayload.computeRootDirUvf())) - .region(metadataPayload.storage().getRegion()); - log.debug("Created storage dto {}", storageDto); - - // (1) create vault in hub, incl. Keycloak sync - final boolean minio = storageProfileWrapper.getStsRoleArn() != null && storageProfileWrapper.getStsRoleArn2() == null; - final boolean aws = storageProfileWrapper.getStsRoleArn() != null && storageProfileWrapper.getStsRoleArn2() != null; - log.debug("Create vault {}, minio={}, aws={}", vaultDto, minio, aws); - vaultResource.apiVaultsVaultIdPut(vaultDto.getId(), vaultDto, minio, aws); - - // (2) create bucket - final HostPasswordStore keychain = PasswordStoreFactory.get(); - - final OAuthTokens tokens = keychain.findOAuthTokens(hubSession.getHost()); - final Host bookmark = new VaultServiceImpl(vaultResource, storageProfileResource).getStorageBackend( - hubSession, configResource.apiConfigGet(), vaultDto.getId(), metadataPayload, tokens); - if(storageProfileWrapper.getProtocol() == Protocol.S3) { - // permanent: template upload into existing bucket from client (not backend) - templateUploadService.uploadTemplate(bookmark, metadataPayload, storageDto, hashedRootDirId); - } - else { - // non-permanent: pass STS tokens (restricted by inline policy) to hub backend and have bucket created from there - final TemporaryAccessTokens stsTokens = stsInlinePolicyService.getSTSTokensFromAccessTokenWithCreateBucketInlinePolicy( - tokens.getAccessToken(), - storageProfileWrapper.getStsRoleArnClient(), - vaultDto.getId().toString(), - storageProfileWrapper.getStsEndpoint(), - String.format("%s%s", storageProfileWrapper.getBucketPrefix(), vaultDto.getId()), - vaultModel.region(), - storageProfileWrapper.getBucketAcceleration() - ); - log.debug("Create STS bucket {} for vault {}", storageDto, vaultDto); - storageResource.apiStorageVaultIdPut(vaultDto.getId(), - storageDto.awsAccessKey(stsTokens.getAccessKeyId()) - .awsSecretKey(stsTokens.getSecretAccessKey()) - .sessionToken(stsTokens.getSessionToken())); - } - - // (3) upload JWE to hub - log.debug("Upload JWE {} for vault {}", uvfMetadataFile, vaultDto); - final UserDto userDto = users.apiUsersMeGet(false, false); - vaultResource.apiVaultsVaultIdAccessTokensPost(vaultDto.getId(), Collections.singletonMap(userDto.getId(), jwks.toOwnerAccessToken().encryptForUser(userKeys.ecdhKeyPair().getPublic()))); - } - catch(JOSEException | JsonProcessingException e) { - throw new SecurityFailure(e); - } - catch(IOException e) { - throw new AccessException(e); - } - } - - static class TemplateUploadService { - static TemplateUploadService disabled = new TemplateUploadService() { - @Override - void uploadTemplate(final Host bookmark, final UvfMetadataPayload metadataPayload, final CreateS3STSBucketDto storageDto, final String hashedRootDirId) { - // do nothing - } - }; - - void uploadTemplate(final Host bookmark, final UvfMetadataPayload metadataPayload, final CreateS3STSBucketDto storageDto, final String hashedRootDirId) throws BackgroundException { - final S3Session session = new S3Session(bookmark); - session.open(new DisabledProxyFinder(), new DisabledHostKeyCallback(), new DisabledLoginCallback(), new DisabledCancelCallback()); - session.login(new DisabledLoginCallback(), new DisabledCancelCallback()); - - // upload vault template - new HubUVFVault(session, new Path(metadataPayload.storage().getDefaultPath(), EnumSet.of(Path.Type.directory, Path.Type.vault))) - .create(session, metadataPayload.storage().getRegion(), storageDto.getVaultUvf(), hashedRootDirId, Base64.getUrlDecoder().decode(storageDto.getDirUvf())); - session.close(); - } - } - - static class STSInlinePolicyService { - static STSInlinePolicyService disabled = new STSInlinePolicyService() { - @Override - TemporaryAccessTokens getSTSTokensFromAccessTokenWithCreateBucketInlinePolicy(final String token, final String roleArn, final String roleSessionName, final String stsEndpoint, final String bucketName, final String region, final Boolean bucketAcceleration) { - return TemporaryAccessTokens.EMPTY; - } - }; - - TemporaryAccessTokens getSTSTokensFromAccessTokenWithCreateBucketInlinePolicy(final String token, final String roleArn, final String roleSessionName, final String stsEndpoint, final String bucketName, final String region, final Boolean bucketAcceleration) throws IOException { - log.debug("Get STS tokens from {} to pass to backend {} with role {} and session name {}", token, stsEndpoint, roleArn, roleSessionName); - - final AssumeRoleWithWebIdentityRequest request = new AssumeRoleWithWebIdentityRequest(); - - request.setWebIdentityToken(token); - - final String inlinePolicy = MessageFormat.format( - IOUtils.toString(CreateVaultService.class.getResourceAsStream("/sts_create_bucket_inline_policy_template.json"), Charset.defaultCharset()), - bucketName); - request.setPolicy(inlinePolicy); - request.setRoleArn(roleArn); - request.setRoleSessionName(roleSessionName); - - AWSSecurityTokenServiceClientBuilder serviceBuild = AWSSecurityTokenServiceClientBuilder - .standard(); - // Exactly only one of Region or EndpointConfiguration may be set. - if(stsEndpoint != null) { - serviceBuild.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(stsEndpoint, null)); - } - else { - serviceBuild.withRegion(region); - } - final AWSSecurityTokenService service = serviceBuild - .withCredentials(new AWSStaticCredentialsProvider(new AnonymousAWSCredentials())) - .build(); - - log.debug("Use request {}", request); - final AssumeRoleWithWebIdentityResult result = service.assumeRoleWithWebIdentity(request); - log.debug("Received assume role identity result {}", result); - return new TemporaryAccessTokens(result.getCredentials().getAccessKeyId(), - result.getCredentials().getSecretAccessKey(), - result.getCredentials().getSessionToken(), - result.getCredentials().getExpiration().getTime()); - } - } - - public static class CreateVaultModel { - - private final UUID vaultId; - private final String vaultName; - private final String vaultDescription; - private final String storageProfileId; - private final String accessKeyId; - private final String secretKey; - private final String bucketName; - private final String region; - private final boolean automaticAccessGrant; - private final int maxWotLevel; - - - public CreateVaultModel(final UUID vaultId, final String vaultName, final String vaultDescription, final String storageProfileId, - final String accessKeyId, final String secretKey, - final String bucketName, final String region, - final boolean automaticAccessGrant, final int maxWotLevel) { - this.vaultId = vaultId; - this.vaultName = vaultName; - this.vaultDescription = vaultDescription; - this.storageProfileId = storageProfileId; - this.accessKeyId = accessKeyId; - this.secretKey = secretKey; - this.bucketName = bucketName; - this.region = region; - this.automaticAccessGrant = automaticAccessGrant; - this.maxWotLevel = maxWotLevel; - } - - public UUID vaultId() { - return vaultId; - } - - public String vaultName() { - return vaultName; - } - - public String vaultDescription() { - return vaultDescription; - } - - public String storageProfileId() { - return storageProfileId; - } - - public String accessKeyId() { - return accessKeyId; - } - - public String secretKey() { - return secretKey; - } - - public String bucketName() { - return bucketName; - } - - public String region() { - return region; - } - - public boolean automaticAccessGrant() { - return automaticAccessGrant; - } - - public int maxWotLevel() { - return maxWotLevel; - } - } -} diff --git a/hub/src/main/resources/sts_create_bucket_inline_policy_template.json b/hub/src/main/resources/sts_create_bucket_inline_policy_template.json deleted file mode 100644 index 15a46708..00000000 --- a/hub/src/main/resources/sts_create_bucket_inline_policy_template.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "s3:CreateBucket", - "s3:GetBucketPolicy", - "s3:PutBucketVersioning", - "s3:GetBucketVersioning", - "s3:GetEncryptionConfiguration", - "s3:PutEncryptionConfiguration", - "s3:GetAccelerateConfiguration", - "s3:PutAccelerateConfiguration" - ], - "Resource": "arn:aws:s3:::{}" - }, - { - "Effect": "Allow", - "Action": [ - "s3:PutObject" - ], - "Resource": [ - "arn:aws:s3:::{0}/*.uvf", - "arn:aws:s3:::{0}/*/" - ] - } - ] -} diff --git a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java index e38df715..99282aaa 100644 --- a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java +++ b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java @@ -4,7 +4,15 @@ package cloud.katta.core; -import ch.cyberduck.core.*; +import ch.cyberduck.core.AlphanumericRandomStringService; +import ch.cyberduck.core.AttributedList; +import ch.cyberduck.core.DisabledConnectionCallback; +import ch.cyberduck.core.DisabledListProgressListener; +import ch.cyberduck.core.ListService; +import ch.cyberduck.core.OAuthTokens; +import ch.cyberduck.core.Path; +import ch.cyberduck.core.Session; +import ch.cyberduck.core.SimplePathPredicate; import ch.cyberduck.core.exception.AccessDeniedException; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.exception.NotfoundException; @@ -19,13 +27,11 @@ import ch.cyberduck.core.features.Vault; import ch.cyberduck.core.features.Write; import ch.cyberduck.core.io.StatusOutputStream; -import ch.cyberduck.core.proxy.DisabledProxyFinder; -import ch.cyberduck.core.s3.S3Session; import ch.cyberduck.core.transfer.Transfer; import ch.cyberduck.core.transfer.TransferItem; import ch.cyberduck.core.transfer.TransferStatus; +import ch.cyberduck.core.vault.VaultCredentials; import ch.cyberduck.core.vault.VaultRegistry; -import ch.cyberduck.core.worker.DeleteWorker; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.RandomUtils; @@ -47,14 +53,12 @@ import java.util.EnumSet; import java.util.List; import java.util.UUID; -import java.util.stream.Collectors; import cloud.katta.client.ApiClient; import cloud.katta.client.ApiException; import cloud.katta.client.api.ConfigResourceApi; import cloud.katta.client.api.StorageProfileResourceApi; import cloud.katta.client.api.UsersResourceApi; -import cloud.katta.client.api.VaultResourceApi; import cloud.katta.client.model.ConfigDto; import cloud.katta.client.model.Protocol; import cloud.katta.client.model.S3SERVERSIDEENCRYPTION; @@ -62,18 +66,13 @@ import cloud.katta.client.model.StorageProfileDto; import cloud.katta.client.model.StorageProfileS3Dto; import cloud.katta.client.model.StorageProfileS3STSDto; -import cloud.katta.crypto.UserKeys; -import cloud.katta.crypto.uvf.VaultMetadataJWEBackendDto; import cloud.katta.model.StorageProfileDtoWrapper; import cloud.katta.protocols.hub.HubSession; +import cloud.katta.protocols.hub.HubUVFVault; import cloud.katta.protocols.hub.HubVaultRegistry; import cloud.katta.testsetup.AbstractHubTest; import cloud.katta.testsetup.HubTestConfig; import cloud.katta.testsetup.MethodIgnorableSource; -import cloud.katta.workflows.CreateVaultService; -import cloud.katta.workflows.DeviceKeysServiceImpl; -import cloud.katta.workflows.UserKeysServiceImpl; -import cloud.katta.workflows.VaultServiceImpl; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; @@ -230,17 +229,14 @@ public void test03AddVault(final HubTestConfig config) throws Exception { log.info("Creating vault in {}", hubSession); final UUID vaultId = UUID.randomUUID(); - final UserKeys userKeys = new UserKeysServiceImpl(hubSession).getUserKeys(hubSession.getHost(), hubSession.getMe(), - new DeviceKeysServiceImpl().getDeviceKeys(hubSession.getHost())); - new CreateVaultService(hubSession).createVault(userKeys, storageProfileWrapper, new CreateVaultService.CreateVaultModel( - vaultId, "vault", null, - config.vault.storageProfileId, config.vault.username, config.vault.password, config.vault.bucketName, config.vault.region, true, 3)); + final Path bucket = new Path(storageProfileWrapper.getProtocol() == Protocol.S3_STS ? storageProfileWrapper.getBucketPrefix() + vaultId : config.vault.bucketName, + EnumSet.of(Path.Type.volume, Path.Type.directory)); + final HubUVFVault cryptomator = new HubUVFVault(bucket); + cryptomator.create(hubSession, storageProfileWrapper.getRegion(), new VaultCredentials("test")); final AttributedList vaults = hubSession.getFeature(ListService.class).list(Home.root(), new DisabledListProgressListener()); assertFalse(vaults.isEmpty()); - final Path bucket = new Path(storageProfileWrapper.getProtocol() == Protocol.S3_STS ? storageProfileWrapper.getBucketPrefix() + vaultId : config.vault.bucketName, - EnumSet.of(Path.Type.volume, Path.Type.directory)); final VaultRegistry vaultRegistry = hubSession.getRegistry(); assertInstanceOf(HubVaultRegistry.class, vaultRegistry); { @@ -251,7 +247,6 @@ public void test03AddVault(final HubTestConfig config) throws Exception { assertNotSame(Vault.DISABLED, vaultRegistry.find(hubSession, bucket)); // listing decrypted file names - assertFalse(vaultRegistry.isEmpty()); assertNotSame(Vault.DISABLED, vaultRegistry.find(hubSession, bucket)); } diff --git a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java index c7900a01..38efde43 100644 --- a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java +++ b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java @@ -4,11 +4,15 @@ package cloud.katta.workflows; +import ch.cyberduck.core.Path; +import ch.cyberduck.core.vault.VaultCredentials; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.params.ParameterizedTest; import java.io.IOException; +import java.util.EnumSet; import java.util.List; import java.util.UUID; @@ -18,6 +22,7 @@ import cloud.katta.client.api.UsersResourceApi; import cloud.katta.client.api.VaultResourceApi; import cloud.katta.client.model.MemberDto; +import cloud.katta.client.model.Protocol; import cloud.katta.client.model.Role; import cloud.katta.client.model.StorageProfileDto; import cloud.katta.client.model.UserDto; @@ -26,6 +31,7 @@ import cloud.katta.model.SetupCodeJWE; import cloud.katta.model.StorageProfileDtoWrapper; import cloud.katta.protocols.hub.HubSession; +import cloud.katta.protocols.hub.HubUVFVault; import cloud.katta.testsetup.AbstractHubTest; import cloud.katta.testsetup.HubTestConfig; import cloud.katta.testsetup.MethodIgnorableSource; @@ -52,17 +58,14 @@ public void testHubWorkflow(final HubTestConfig config) throws Exception { .filter(p -> p.getId().toString().equals(config.vault.storageProfileId.toLowerCase())).findFirst().get(); final UUID vaultId = UUID.randomUUID(); - final boolean automaticAccessGrant = true; // upload template (STS: create bucket first, static: existing bucket) // TODO test with multiple wot levels? - final UserKeys userKeys = new UserKeysServiceImpl(hubSession).getUserKeys(hubSession.getHost(), hubSession.getMe(), - new DeviceKeysServiceImpl().getDeviceKeys(hubSession.getHost())); - new CreateVaultService(hubSession).createVault(userKeys, storageProfileWrapper, - new CreateVaultService.CreateVaultModel(vaultId, - "vault", null, - config.vault.storageProfileId, config.vault.username, config.vault.password, config.vault.bucketName, - config.vault.region, automaticAccessGrant, 3)); + final Path bucket = new Path(storageProfileWrapper.getProtocol() == Protocol.S3_STS ? storageProfileWrapper.getBucketPrefix() + vaultId : config.vault.bucketName, + EnumSet.of(Path.Type.volume, Path.Type.directory)); + final HubUVFVault cryptomator = new HubUVFVault(bucket); + cryptomator.create(hubSession, storageProfileWrapper.getRegion(), new VaultCredentials("test")); + checkNumberOfVaults(hubSession, config, vaultId, 0, 0, 1, 0, 0); log.info("S02 {} alice shares vault with admin as owner", setup); @@ -96,6 +99,8 @@ public void testHubWorkflow(final HubTestConfig config) throws Exception { new DeviceKeysServiceImpl().getDeviceKeys(hubSession.getHost())), admin); log.info("S04 {} alice grants access to admin", setup); + final UserKeys userKeys = new UserKeysServiceImpl(hubSession).getUserKeys(hubSession.getHost(), hubSession.getMe(), + new DeviceKeysServiceImpl().getDeviceKeys(hubSession.getHost())); new GrantAccessServiceImpl(hubSession).grantAccessToUsersRequiringAccessGrant(vaultId, userKeys); checkNumberOfVaults(hubSession, config, vaultId, 1, 0, 1, 0, 0); } diff --git a/hub/src/test/java/cloud/katta/workflows/CreateVaultServiceTest.java b/hub/src/test/java/cloud/katta/workflows/CreateVaultServiceTest.java deleted file mode 100644 index b592a8e3..00000000 --- a/hub/src/test/java/cloud/katta/workflows/CreateVaultServiceTest.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (c) 2025 shift7 GmbH. All rights reserved. - */ - -package cloud.katta.workflows; - -import ch.cyberduck.core.Credentials; -import ch.cyberduck.core.Host; -import ch.cyberduck.core.Local; -import ch.cyberduck.core.ProtocolFactory; -import ch.cyberduck.core.exception.BackgroundException; -import ch.cyberduck.core.profiles.LocalProfilesFinder; - -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.util.UUID; - -import cloud.katta.client.ApiException; -import cloud.katta.client.HubApiClient; -import cloud.katta.client.api.ConfigResourceApi; -import cloud.katta.client.api.StorageProfileResourceApi; -import cloud.katta.client.api.StorageResourceApi; -import cloud.katta.client.api.UsersResourceApi; -import cloud.katta.client.api.VaultResourceApi; -import cloud.katta.client.model.ConfigDto; -import cloud.katta.client.model.Protocol; -import cloud.katta.client.model.StorageProfileDto; -import cloud.katta.client.model.StorageProfileS3STSDto; -import cloud.katta.client.model.UserDto; -import cloud.katta.crypto.UserKeys; -import cloud.katta.model.StorageProfileDtoWrapper; -import cloud.katta.protocols.hub.HubProtocol; -import cloud.katta.protocols.hub.HubSession; -import cloud.katta.protocols.s3.S3AssumeRoleProtocol; -import cloud.katta.testsetup.AbstractHubTest; -import cloud.katta.workflows.exceptions.AccessException; -import cloud.katta.workflows.exceptions.SecurityFailure; -import com.nimbusds.jose.JOSEException; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.times; - -class CreateVaultServiceTest { - - @Test - void createVault() throws AccessException, SecurityFailure, BackgroundException, ApiException, JOSEException, IOException, URISyntaxException { - final HubSession hubSession = Mockito.mock(HubSession.class); - final Host hub = Mockito.mock(Host.class); - final VaultResourceApi vaults = Mockito.mock(VaultResourceApi.class); - final UsersResourceApi users = Mockito.mock(UsersResourceApi.class); - final ConfigResourceApi config = Mockito.mock(ConfigResourceApi.class); - - final UserKeys userKeys = UserKeys.create(); - - final HubApiClient apiClient = Mockito.mock(HubApiClient.class); - final StorageProfileResourceApi storageProfiles = Mockito.mock(StorageProfileResourceApi.class); - final StorageResourceApi storage = Mockito.mock(StorageResourceApi.class); - - final UUID vaultId = UUID.randomUUID(); - final UUID storageProfileId = UUID.randomUUID(); - final StorageProfileDto storageProfile = new StorageProfileDto( - new StorageProfileS3STSDto() - .id(storageProfileId) - .protocol(Protocol.S3_STS) - .stsEndpoint("http://audley.end.point") - // AWS has both role arns filled in - .stsRoleArn("arnaud") - .stsRoleArn2("ducret") - - ); - final StorageProfileDtoWrapper storageProfileWrapper = StorageProfileDtoWrapper.coerce(storageProfile); - - Mockito.when(hubSession.getHost()).thenReturn(hub); - Mockito.when(hub.getProtocol()).thenReturn(new HubProtocol() { - @Override - public String getOAuthTokenUrl() { - return "http://tok-tok.dev.null/auth/token"; - } - }); - Mockito.when(hub.getCredentials()).thenReturn(new Credentials()); - Mockito.when(hub.getHostname()).thenReturn("storage"); - Mockito.when(apiClient.getBasePath()).thenReturn("http://nix.com/api"); - Mockito.when(vaults.getApiClient()).thenReturn(apiClient); - - Mockito.when(storageProfiles.apiStorageprofileProfileIdGet(storageProfileId)).thenReturn(storageProfile); - Mockito.when(config.apiConfigGet()).thenReturn(new ConfigDto().keycloakClientIdCryptomatorVaults("hex")); - - final UserDto me = new UserDto(); - Mockito.when(users.apiUsersMeGet(false, false)).thenReturn(me); - - final ProtocolFactory factory = ProtocolFactory.get(); - // Register parent protocol definitions - factory.register( - new HubProtocol(), - new S3AssumeRoleProtocol("PasswordGrant") - ); - // Load bundled profiles - factory.load(new LocalProfilesFinder(factory, new Local(AbstractHubTest.class.getResource("/").toURI().getPath()))); - - final CreateVaultService.CreateVaultModel createVaultModel = new CreateVaultService.CreateVaultModel(vaultId, null, null, null, null, null, null, null, true, 66); - - final CreateVaultService createVaultService = new CreateVaultService(hubSession, config, vaults, storageProfiles, users, storage, CreateVaultService.TemplateUploadService.disabled, CreateVaultService.STSInlinePolicyService.disabled); - - createVaultService.createVault(userKeys, storageProfileWrapper, createVaultModel); - - final boolean expectedMinio = false; - final boolean expectedAWS = true; - Mockito.verify(vaults, times(1)).apiVaultsVaultIdPut(eq(vaultId), any(), eq(expectedMinio), eq(expectedAWS)); - Mockito.verify(vaults, times(1)).apiVaultsVaultIdAccessTokensPost(eq(vaultId), any()); - Mockito.verify(storage, times(1)).apiStorageVaultIdPut(eq(vaultId), any()); - } -} diff --git a/osx/src/main/java/cloud/katta/controller/CreateVaultBookmarkController.java b/osx/src/main/java/cloud/katta/controller/CreateVaultBookmarkController.java deleted file mode 100644 index 22205291..00000000 --- a/osx/src/main/java/cloud/katta/controller/CreateVaultBookmarkController.java +++ /dev/null @@ -1,410 +0,0 @@ -/* - * Copyright (c) 2025 shift7 GmbH. All rights reserved. - */ - -package cloud.katta.controller; - -import ch.cyberduck.binding.Action; -import ch.cyberduck.binding.BundleController; -import ch.cyberduck.binding.Outlet; -import ch.cyberduck.binding.SheetController; -import ch.cyberduck.binding.application.NSButton; -import ch.cyberduck.binding.application.NSImage; -import ch.cyberduck.binding.application.NSImageView; -import ch.cyberduck.binding.application.NSPopUpButton; -import ch.cyberduck.binding.application.NSTextField; -import ch.cyberduck.binding.application.SheetCallback; -import ch.cyberduck.binding.foundation.NSAttributedString; -import ch.cyberduck.core.Controller; -import ch.cyberduck.core.LocaleFactory; -import ch.cyberduck.core.StringAppender; -import ch.cyberduck.core.resources.IconCacheFactory; -import ch.cyberduck.core.threading.AbstractBackgroundAction; - -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.rococoa.Foundation; -import org.rococoa.cocoa.foundation.NSPoint; -import org.rococoa.cocoa.foundation.NSRect; -import org.rococoa.cocoa.foundation.NSSize; - -import java.util.List; -import java.util.UUID; - -import cloud.katta.client.model.Protocol; -import cloud.katta.model.StorageProfileDtoWrapper; -import cloud.katta.workflows.CreateVaultService; - - -/** - * Fetch user input for vault bookmark creation. - */ -public class CreateVaultBookmarkController extends SheetController { - private static final Logger log = LogManager.getLogger(CreateVaultBookmarkController.class.getName()); - - private final String title_; - private final String reason_; - private final String icon_; - private final String vaultNameLabel_; - private final String vaultDescriptionLabel_; - private final String backendLabel_; - private final String regionLabel_; - private final String bucketNameLabel_; - private final String accessKeyIdLabel_; - private final String secretKeyLabel_; - private final String maxWotLevelLabel_; - - private final CreateVaultService.CreateVaultModel model; - private final Callback callback; - - private final Controller controller; - - private final List storageProfiles; - - @Outlet - private NSImageView iconView; - @Outlet - private NSTextField titleField; - @Outlet - private NSTextField messageField; - @Outlet - private NSTextField vaultNameLabel; - @Outlet - private NSTextField vaultNameField; - @Outlet - private NSTextField vaultDescriptionLabel; - @Outlet - private NSTextField vaultDescriptionField; - @Outlet - private NSTextField backendLabel; - @Outlet - private NSPopUpButton backendCombobox; - @Outlet - private NSTextField regionLabel; - @Outlet - private NSPopUpButton regionCombobox; - @Outlet - private NSTextField bucketNameLabel; - @Outlet - private NSTextField bucketNameField; - @Outlet - private NSTextField accessKeyIdLabel; - @Outlet - private NSTextField accessKeyIdField; - @Outlet - private NSTextField secretKeyLabel; - @Outlet - private NSTextField secretKeyField; - @Outlet - private NSTextField automaticAccessGrantCheckboxLabel; - @Outlet - private NSButton automaticAccessGrantCheckbox; - @Outlet - private NSTextField maxWotLevelLabel; - @Outlet - private NSTextField maxWotLevel; - @Outlet - private NSButton helpButton; - @Outlet - private NSButton cancelButton; - @Outlet - private NSButton createVaultButton; - - public CreateVaultBookmarkController(final List storageProfiles, final Controller controller, final CreateVaultService.CreateVaultModel model, final Callback callback) { - this.model = model; - this.callback = callback; - this.title_ = LocaleFactory.localizedString("Create Vault", "Cipherduck"); - this.reason_ = LocaleFactory.localizedString("Enter a name and description for your new vault. You can change these later.", "Cipherduck"); - this.vaultNameLabel_ = LocaleFactory.localizedString("Vault Name", "Cipherduck"); - this.vaultDescriptionLabel_ = LocaleFactory.localizedString("Description (optional)", "Cipherduck"); - this.backendLabel_ = LocaleFactory.localizedString("Vault storage location", "Cipherduck"); - this.regionLabel_ = LocaleFactory.localizedString("Region", "Cipherduck"); - this.icon_ = "cryptomator.tiff"; - this.storageProfiles = storageProfiles; - this.bucketNameLabel_ = LocaleFactory.localizedString("Bucket Name", "Cipherduck"); - this.accessKeyIdLabel_ = LocaleFactory.localizedString("Access Key ID", "Cipherduck"); - this.secretKeyLabel_ = LocaleFactory.localizedString("Secret Key", "Cipherduck"); - this.maxWotLevelLabel_ = LocaleFactory.localizedString("Max WoT Level", "Cipherduck"); - this.controller = controller; - } - - @Override - public void awakeFromNib() { - super.awakeFromNib(); - window.makeFirstResponder(titleField); - updateRegions(); - } - - @Override - protected String getBundleName() { - return "CreateVault"; - } - - public void setIconView(NSImageView iconView) { - this.iconView = iconView; - this.iconView.setImage(IconCacheFactory.get().iconNamed(this.icon_, 64)); - } - - public void setTitleField(NSTextField titleField) { - this.titleField = titleField; - this.updateField(this.titleField, LocaleFactory.localizedString(this.title_, "Cipherduck")); - } - - public void setMessageField(NSTextField messageField) { - this.messageField = messageField; - this.messageField.setSelectable(true); - this.updateField(this.messageField, new StringAppender().append(reason_).toString()); - } - - public void setVaultNameLabel(final NSTextField f) { - this.vaultNameLabel = f; - this.vaultNameLabel.setAttributedStringValue(NSAttributedString.attributedStringWithAttributes( - String.format("%s:", this.vaultNameLabel_), - BundleController.TRUNCATE_TAIL_ATTRIBUTES - )); - } - - public void setVaultNameField(final NSTextField f) { - this.vaultNameField = f; - this.vaultNameField.setStringValue(this.model.vaultName()); - } - - public void setVaultDescriptionLabel(final NSTextField f) { - this.vaultDescriptionLabel = f; - vaultDescriptionLabel.setAttributedStringValue(NSAttributedString.attributedStringWithAttributes( - String.format("%s:", this.vaultDescriptionLabel_), - BundleController.TRUNCATE_TAIL_ATTRIBUTES - )); - } - - public void setVaultDescriptionField(final NSTextField f) { - this.vaultDescriptionField = f; - } - - public void setBackendLabel(final NSTextField f) { - this.backendLabel = f; - backendLabel.setAttributedStringValue(NSAttributedString.attributedStringWithAttributes( - String.format("%s:", this.backendLabel_), - BundleController.TRUNCATE_TAIL_ATTRIBUTES - )); - } - - public void setBackendCombobox(final NSPopUpButton b) { - this.backendCombobox = b; - this.backendCombobox.removeAllItems(); - this.backendCombobox.setTarget(this.id()); - this.backendCombobox.setAction(Foundation.selector("backendComboboxClicked:")); - for(final StorageProfileDtoWrapper backend : storageProfiles) { - this.backendCombobox.addItemWithTitle(backend.getName()); - this.backendCombobox.lastItem().setRepresentedObject(backend.getId().toString()); - } - if(StringUtils.isNotBlank(this.model.storageProfileId())) { - this.backendCombobox.selectItemAtIndex(this.backendCombobox.indexOfItemWithRepresentedObject(this.model.storageProfileId())); - } - } - - @Action - public void backendComboboxClicked(NSPopUpButton sender) { - LocaleFactory.get().setDefault(sender.selectedItem().representedObject()); - updateRegions(); - } - - private void updateRegions() { - synchronized(storageProfiles) { - if(regionCombobox == null) { - return; - } - if(backendCombobox == null) { - return; - } - final String selectedStorageId = this.backendCombobox.selectedItem().representedObject(); - final StorageProfileDtoWrapper config = storageProfiles.stream().filter(c -> c.getId().toString().equals(selectedStorageId)).findFirst().get(); - - final List regions = config.getRegions(); - if(null != regions) { - for(final String region : regions) { - this.regionCombobox.addItemWithTitle(LocaleFactory.localizedString(region, "S3")); - this.regionCombobox.lastItem().setRepresentedObject(region); - if(config.getRegion().equals(region)) { - regionCombobox.selectItem(this.regionCombobox.lastItem()); - } - } - } - final boolean isPermanent = config.getProtocol() == Protocol.S3; - final boolean hiddenIfSTS = !isPermanent; - final boolean hiddenIfPermanent = isPermanent; - bucketNameLabel.setHidden(hiddenIfSTS); - bucketNameField.setHidden(hiddenIfSTS); - accessKeyIdLabel.setHidden(hiddenIfSTS); - accessKeyIdField.setHidden(hiddenIfSTS); - secretKeyLabel.setHidden(hiddenIfSTS); - secretKeyField.setHidden(hiddenIfSTS); - regionCombobox.setHidden(hiddenIfPermanent); - regionLabel.setHidden(hiddenIfPermanent); - final NSRect frame = window.frame(); - double height = frame.size.height.doubleValue(); - final int ROW_HEIGHT = 25; - // isPermanent -> hide region row (1) - // STS -> hide Access Key ID/Secret Key/Bucket Name (3) - height = !isPermanent ? 369.0 - 3 * ROW_HEIGHT : 369.0 - 1 * ROW_HEIGHT; - double width = frame.size.width.doubleValue(); - window.setFrame_display_animate(new NSRect(frame.origin, new NSSize(width, height)), true, true); - // set the bottom row elements relative to new window frame after resizing: - bucketNameLabel.setFrameOrigin(new NSPoint(bucketNameLabel.frame().origin.x.doubleValue(), 62 + ROW_HEIGHT)); - bucketNameField.setFrameOrigin(new NSPoint(bucketNameField.frame().origin.x.doubleValue(), 60 + ROW_HEIGHT)); - secretKeyLabel.setFrameOrigin(new NSPoint(secretKeyLabel.frame().origin.x.doubleValue(), 87 + ROW_HEIGHT)); - secretKeyField.setFrameOrigin(new NSPoint(secretKeyField.frame().origin.x.doubleValue(), 85 + ROW_HEIGHT)); - accessKeyIdLabel.setFrameOrigin(new NSPoint(accessKeyIdLabel.frame().origin.x.doubleValue(), 112 + ROW_HEIGHT)); - accessKeyIdField.setFrameOrigin(new NSPoint(accessKeyIdField.frame().origin.x.doubleValue(), 110 + ROW_HEIGHT)); - automaticAccessGrantCheckbox.setFrameOrigin(new NSPoint(automaticAccessGrantCheckbox.frame().origin.x.doubleValue(), 35 + ROW_HEIGHT)); - maxWotLevel.setFrameOrigin(new NSPoint(maxWotLevel.frame().origin.x.doubleValue(), 35)); - maxWotLevelLabel.setFrameOrigin(new NSPoint(maxWotLevelLabel.frame().origin.x.doubleValue(), 35)); - helpButton.setFrameOrigin(new NSPoint(helpButton.frame().origin.x.doubleValue(), 5 + 4)); - cancelButton.setFrameOrigin(new NSPoint(cancelButton.frame().origin.x.doubleValue(), 5)); - createVaultButton.setFrameOrigin(new NSPoint(createVaultButton.frame().origin.x.doubleValue(), 5)); - } - } - - public void setRegionLabel(final NSTextField f) { - this.regionLabel = f; - this.regionLabel.setAttributedStringValue(NSAttributedString.attributedStringWithAttributes( - String.format("%s:", this.regionLabel_), - BundleController.TRUNCATE_TAIL_ATTRIBUTES - )); - } - - - public void setRegionCombobox(final NSPopUpButton b) { - this.regionCombobox = b; - if(StringUtils.isNotBlank(this.model.region())) { - this.regionCombobox.selectItemAtIndex(this.regionCombobox.indexOfItemWithRepresentedObject(this.model.region())); - } - } - - public void setBucketNameLabel(final NSTextField BucketNameLabel) { - this.bucketNameLabel = BucketNameLabel; - BucketNameLabel.setAttributedStringValue(NSAttributedString.attributedStringWithAttributes( - String.format("%s:", this.bucketNameLabel_), - BundleController.TRUNCATE_TAIL_ATTRIBUTES - )); - } - - public void setBucketNameField(final NSTextField f) { - this.bucketNameField = f; - this.bucketNameField.setStringValue(this.model.bucketName()); - } - - public void setAccessKeyIdLabel(final NSTextField f) { - this.accessKeyIdLabel = f; - f.setAttributedStringValue(NSAttributedString.attributedStringWithAttributes( - String.format("%s:", this.accessKeyIdLabel_), - BundleController.TRUNCATE_TAIL_ATTRIBUTES - )); - this.accessKeyIdField.setStringValue(this.model.accessKeyId()); - } - - public void setAccessKeyIdField(final NSTextField f) { - this.accessKeyIdField = f; - } - - public void setMaxWotLevelLabel(final NSTextField f) { - this.maxWotLevelLabel = f; - f.setAttributedStringValue(NSAttributedString.attributedStringWithAttributes( - String.format("%s:", this.maxWotLevelLabel_), - BundleController.TRUNCATE_TAIL_ATTRIBUTES - )); - - } - - public void setMaxWotLevelField(final NSTextField f) { - this.maxWotLevel = f; - this.maxWotLevel.setStringValue(String.valueOf(this.model.maxWotLevel())); - } - - public void setAutomaticAccessGrantCheckbox(final NSButton b) { - this.automaticAccessGrantCheckbox = b; - this.automaticAccessGrantCheckbox.setState(this.model.automaticAccessGrant() ? 1 : 0); - - } - - public void setSecretKeyLabel(final NSTextField f) { - this.secretKeyLabel = f; - f.setAttributedStringValue(NSAttributedString.attributedStringWithAttributes( - String.format("%s:", this.secretKeyLabel_), - BundleController.TRUNCATE_TAIL_ATTRIBUTES - )); - this.secretKeyField.setStringValue(this.model.secretKey()); - } - - public void setSecretKeyField(final NSTextField f) { - this.secretKeyField = f; - } - - public void setHelpButton(final NSButton b) { - this.helpButton = b; - } - - public void setCancelButton(final NSButton b) { - this.cancelButton = b; - this.cancelButton.setTarget(this.id()); - this.cancelButton.setAction(Foundation.selector("closeSheet:")); - } - - public void setCreateVaultButton(final NSButton b) { - this.createVaultButton = b; - this.createVaultButton.setTarget(this.id()); - this.createVaultButton.setAction(Foundation.selector("closeSheet:")); - } - - @Override - public boolean validate(final int returncode) { - if(StringUtils.isBlank(this.vaultNameField.stringValue())) { - return false; - } - final String selectedStorageId = this.backendCombobox.selectedItem().representedObject(); - final StorageProfileDtoWrapper config = storageProfiles.stream().filter(c -> c.getId().toString().equals(selectedStorageId)).findFirst().get(); - final boolean isPermanent = config.getProtocol() == Protocol.S3; - if(isPermanent) { - if(StringUtils.isBlank(this.accessKeyIdField.stringValue())) { - return false; - } - if(StringUtils.isBlank(this.secretKeyField.stringValue())) { - return false; - } - if(StringUtils.isBlank(this.bucketNameField.stringValue())) { - return false; - } - } - return true; - } - - @Override - public void callback(final int returncode) { - if(returncode != SheetCallback.DEFAULT_OPTION) { - return; - } - controller.background(new AbstractBackgroundAction() { - @Override - public Void run() { - final CreateVaultService.CreateVaultModel m = new CreateVaultService.CreateVaultModel(UUID.randomUUID(), vaultNameField.stringValue(), - vaultDescriptionField.stringValue(), - backendCombobox.selectedItem().representedObject(), - StringUtils.isNotBlank(accessKeyIdField.stringValue()) ? accessKeyIdField.stringValue() : null, - StringUtils.isNotBlank(secretKeyField.stringValue()) ? secretKeyField.stringValue() : null, - StringUtils.isNotBlank(bucketNameField.stringValue()) ? bucketNameField.stringValue() : null, - regionCombobox.selectedItem().representedObject(), - automaticAccessGrantCheckbox.integerValue() == 1, - StringUtils.isNotBlank(maxWotLevel.stringValue()) ? Integer.parseInt(maxWotLevel.stringValue()) : 0 - ); - callback.callback(m); - return null; - } - }); - } - - public interface Callback { - void callback(final CreateVaultService.CreateVaultModel selected); - } -} - From 0f05379a0e616d9669df5533bd65492ddfd36c97 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 4 Sep 2025 15:34:50 +0200 Subject: [PATCH 068/133] Show display name. --- .../hub/HubStorageLocationService.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java index d159c82f..7f617d24 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java @@ -43,7 +43,7 @@ public Set getLocations(final Path file) { for(StorageProfileDto storageProfileDto : storageProfileDtos) { final StorageProfileDtoWrapper storageProfile = StorageProfileDtoWrapper.coerce(storageProfileDto); for(String region : storageProfile.getRegions()) { - regions.add(new StorageLocation(storageProfile.getId().toString(), region)); + regions.add(new StorageLocation(storageProfile.getId().toString(), region, storageProfile.getName())); } } return regions; @@ -60,19 +60,22 @@ public Name getLocation(final Path file) { } public static final class StorageLocation extends Name { - /** - * UUID of storage profile configuration - */ - private final String storageProfile; + private final String storageProfileName; /** * AWS location */ private final String region; - public StorageLocation(final String storageProfile, final String region) { + /** + * + * @param storageProfile UUID of storage profile configuration + * @param region AWS location + * @param storageProfileName Description + */ + public StorageLocation(final String storageProfile, final String region, final String storageProfileName) { super(String.format("%s-%s", storageProfile, region)); - this.storageProfile = storageProfile; this.region = region; + this.storageProfileName = storageProfileName; } public String getRegion() { @@ -81,20 +84,21 @@ public String getRegion() { @Override public String toString() { - return String.format("%s (%s)", storageProfile, region); + return String.format("%s (%s)", storageProfileName, region); } /** * Parse a storage location from an identifier containing storage profile and AWS location. + * * @param identifier Storage profile identifier and AWS region separated by dash * @return Location with storage profile as identifier and AWS location as region */ public static StorageLocation fromIdentifier(final String identifier) { final String[] parts = identifier.split("-"); if(parts.length != 2) { - return new StorageLocation(identifier, null); + return new StorageLocation(identifier, null, null); } - return new StorageLocation(parts[0], parts[1]); + return new StorageLocation(parts[0], parts[1], null); } } } From 4e66e13329037bd65ce12e2690cb391bb90ec140 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 4 Sep 2025 16:45:14 +0200 Subject: [PATCH 069/133] Use delimiter different from regions. --- .../protocols/hub/HubStorageLocationService.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java index 7f617d24..68af6222 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java @@ -60,11 +60,12 @@ public Name getLocation(final Path file) { } public static final class StorageLocation extends Name { - private final String storageProfileName; + private final String storageProfile; /** * AWS location */ private final String region; + private final String storageProfileName; /** * @@ -73,11 +74,20 @@ public static final class StorageLocation extends Name { * @param storageProfileName Description */ public StorageLocation(final String storageProfile, final String region, final String storageProfileName) { - super(String.format("%s-%s", storageProfile, region)); + super(String.format("%s,%s", storageProfile, region)); + this.storageProfile = storageProfile; this.region = region; this.storageProfileName = storageProfileName; } + /** + * + * @return Storage Profile Id + */ + public String getProfile() { + return storageProfile; + } + public String getRegion() { return region; } @@ -94,7 +104,7 @@ public String toString() { * @return Location with storage profile as identifier and AWS location as region */ public static StorageLocation fromIdentifier(final String identifier) { - final String[] parts = identifier.split("-"); + final String[] parts = identifier.split(","); if(parts.length != 2) { return new StorageLocation(identifier, null, null); } From b1e5797baf87166e2d6421e51299066ec51cc4aa Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 4 Sep 2025 21:20:31 +0200 Subject: [PATCH 070/133] Reuse OAuth credentials. --- ...HubOAuthTokensCredentialsConfigurator.java | 41 +++++++++++++++++++ .../cloud/katta/protocols/hub/HubSession.java | 3 ++ .../katta/protocols/hub/HubUVFVault.java | 6 ++- 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 hub/src/main/java/cloud/katta/protocols/hub/HubOAuthTokensCredentialsConfigurator.java diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubOAuthTokensCredentialsConfigurator.java b/hub/src/main/java/cloud/katta/protocols/hub/HubOAuthTokensCredentialsConfigurator.java new file mode 100644 index 00000000..3850fac4 --- /dev/null +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubOAuthTokensCredentialsConfigurator.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025 shift7 GmbH. All rights reserved. + */ + +package cloud.katta.protocols.hub; + +import ch.cyberduck.core.Credentials; +import ch.cyberduck.core.CredentialsConfigurator; +import ch.cyberduck.core.Host; +import ch.cyberduck.core.HostPasswordStore; +import ch.cyberduck.core.OAuthTokens; + +public class HubOAuthTokensCredentialsConfigurator implements CredentialsConfigurator { + + private final HostPasswordStore keychain; + private final Host host; + + private OAuthTokens tokens; + + public HubOAuthTokensCredentialsConfigurator(final HostPasswordStore keychain, final Host host) { + this.keychain = keychain; + this.host = host; + final Credentials credentials = host.getCredentials(); + // Copy prior reset of credentials after login + this.tokens = new OAuthTokens(credentials.getOauth().getAccessToken(), + credentials.getOauth().getRefreshToken(), + credentials.getOauth().getExpiryInMilliseconds(), + credentials.getOauth().getIdToken()); + } + + @Override + public Credentials configure(final Host host) { + return new Credentials(host.getCredentials()).withOauth(tokens); + } + + @Override + public CredentialsConfigurator reload() { + tokens = keychain.findOAuthTokens(host); + return this; + } +} diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 0fee38b9..e069b2fb 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -355,6 +355,9 @@ public void preflight(final Path file) throws BackgroundException { if(type == ComparisonService.class) { return (T) new HubVaultStorageAwareComparisonService(this); } + if(type == CredentialsConfigurator.class) { + return (T) new HubOAuthTokensCredentialsConfigurator(keychain, host); + } return super._getFeature(type); } } diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 29fb1710..52eb80ee 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -163,7 +163,8 @@ public Path create(final Session session, final String region, final VaultCre // Upload vault template to storage final Protocol profile = ProtocolFactory.get().forName(storageProfile); log.debug("Loaded profile {} for vault {}", profile, this); - final Host bookmark = new Host(profile); + final Host bookmark = new Host(profile, + session.getFeature(CredentialsConfigurator.class).reload().configure(session.getHost())); bookmark.setProperty(OAUTH_TOKENEXCHANGE_VAULT, vaultId.toString()); bookmark.setRegion(location.getRegion()); log.debug("Configured {} for vault {}", bookmark, this); @@ -223,7 +224,8 @@ public HubUVFVault load(final Session session, final PasswordCallback prompt) final UvfMetadataPayload vaultMetadata = vaultService.getVaultMetadataJWE(vaultId, hub.getUserKeys()); final Protocol profile = ProtocolFactory.get().forName(vaultMetadata.storage().getProvider()); log.debug("Loaded profile {} for vault {}", profile, this); - final Credentials credentials = new Credentials(hub.getHost().getCredentials()); + final Credentials credentials = + session.getFeature(CredentialsConfigurator.class).reload().configure(session.getHost()); log.debug("Copy credentials {}", credentials); final VaultMetadataJWEBackendDto vaultStorageMetadata = vaultMetadata.storage(); if(vaultStorageMetadata.getUsername() != null) { From 255b1bcad85ad11f8a2870284df361673006eeea Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 9 Sep 2025 14:29:13 +0200 Subject: [PATCH 071/133] Only reload when expired. --- .../protocols/hub/HubOAuthTokensCredentialsConfigurator.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubOAuthTokensCredentialsConfigurator.java b/hub/src/main/java/cloud/katta/protocols/hub/HubOAuthTokensCredentialsConfigurator.java index 3850fac4..221af4e5 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubOAuthTokensCredentialsConfigurator.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubOAuthTokensCredentialsConfigurator.java @@ -35,7 +35,9 @@ public Credentials configure(final Host host) { @Override public CredentialsConfigurator reload() { - tokens = keychain.findOAuthTokens(host); + if(tokens.isExpired()) { + tokens = keychain.findOAuthTokens(host); + } return this; } } From 01614b6520aef765e4ea866d39e538be091856c7 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 9 Sep 2025 14:30:10 +0200 Subject: [PATCH 072/133] Logging. --- .../hub/HubOAuthTokensCredentialsConfigurator.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubOAuthTokensCredentialsConfigurator.java b/hub/src/main/java/cloud/katta/protocols/hub/HubOAuthTokensCredentialsConfigurator.java index 221af4e5..76c0ae18 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubOAuthTokensCredentialsConfigurator.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubOAuthTokensCredentialsConfigurator.java @@ -10,7 +10,11 @@ import ch.cyberduck.core.HostPasswordStore; import ch.cyberduck.core.OAuthTokens; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + public class HubOAuthTokensCredentialsConfigurator implements CredentialsConfigurator { + private static final Logger log = LogManager.getLogger(HubOAuthTokensCredentialsConfigurator.class); private final HostPasswordStore keychain; private final Host host; @@ -26,9 +30,10 @@ public HubOAuthTokensCredentialsConfigurator(final HostPasswordStore keychain, f credentials.getOauth().getRefreshToken(), credentials.getOauth().getExpiryInMilliseconds(), credentials.getOauth().getIdToken()); + log.debug("Initialized tokens {}", tokens); } - @Override + @Override public Credentials configure(final Host host) { return new Credentials(host.getCredentials()).withOauth(tokens); } @@ -36,7 +41,9 @@ public Credentials configure(final Host host) { @Override public CredentialsConfigurator reload() { if(tokens.isExpired()) { + log.debug("Reload expired tokens from keychain for {}", host); tokens = keychain.findOAuthTokens(host); + log.debug("Retrieved tokens {}", tokens); } return this; } From ccbdfc96f8404a6f84b6898eb9179caffb882322 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 9 Sep 2025 15:43:48 +0200 Subject: [PATCH 073/133] Skip vault when token exchange fails. --- .../java/cloud/katta/protocols/hub/HubUVFVault.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 52eb80ee..6b0b5ab4 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -244,8 +244,14 @@ public HubUVFVault load(final Session session, final PasswordCallback prompt) log.debug("Configured {} for vault {}", bookmark, this); storage = SessionFactory.create(bookmark, session.getFeature(X509TrustManager.class), session.getFeature(X509KeyManager.class)); log.debug("Connect to {}", storage); - storage.open(ProxyFactory.get(), new DisabledHostKeyCallback(), new DisabledLoginCallback(), new DisabledCancelCallback()); - storage.login(new DisabledLoginCallback(), new DisabledCancelCallback()); + try { + storage.open(ProxyFactory.get(), new DisabledHostKeyCallback(), new DisabledLoginCallback(), new DisabledCancelCallback()); + storage.login(new DisabledLoginCallback(), new DisabledCancelCallback()); + } + catch(BackgroundException e) { + log.warn("Skip loading vault with failure {} connecting to storage", e.toString()); + throw new VaultUnlockCancelException(this, e); + } final PathAttributes attr = storage.getFeature(AttributesFinder.class).find(home); attr.setDisplayname(vaultMetadata.storage().getNickname()); home.setAttributes(attr); From 51c0885990d6a3c6d0695575fc077760556f7ffb Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sun, 12 Oct 2025 17:28:48 +0200 Subject: [PATCH 074/133] Rename field. --- .../protocols/hub/HubStorageLocationService.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java index 68af6222..aff6d191 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java @@ -60,7 +60,7 @@ public Name getLocation(final Path file) { } public static final class StorageLocation extends Name { - private final String storageProfile; + private final String storageProfileId; /** * AWS location */ @@ -69,13 +69,13 @@ public static final class StorageLocation extends Name { /** * - * @param storageProfile UUID of storage profile configuration + * @param storageProfileId UUID of storage profile configuration * @param region AWS location * @param storageProfileName Description */ - public StorageLocation(final String storageProfile, final String region, final String storageProfileName) { - super(String.format("%s,%s", storageProfile, region)); - this.storageProfile = storageProfile; + public StorageLocation(final String storageProfileId, final String region, final String storageProfileName) { + super(String.format("%s,%s", storageProfileId, region)); + this.storageProfileId = storageProfileId; this.region = region; this.storageProfileName = storageProfileName; } @@ -85,7 +85,7 @@ public StorageLocation(final String storageProfile, final String region, final S * @return Storage Profile Id */ public String getProfile() { - return storageProfile; + return storageProfileId; } public String getRegion() { From d24804668dd08d006dec039c8621256e56f0170c Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sun, 12 Oct 2025 17:37:59 +0200 Subject: [PATCH 075/133] Pass location including storage profile id and region. --- .../java/cloud/katta/core/AbstractHubSynchronizeTest.java | 5 ++++- .../java/cloud/katta/workflows/AbstractHubWorkflowTest.java | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java index 99282aaa..ae1ed74d 100644 --- a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java +++ b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java @@ -68,6 +68,7 @@ import cloud.katta.client.model.StorageProfileS3STSDto; import cloud.katta.model.StorageProfileDtoWrapper; import cloud.katta.protocols.hub.HubSession; +import cloud.katta.protocols.hub.HubStorageLocationService; import cloud.katta.protocols.hub.HubUVFVault; import cloud.katta.protocols.hub.HubVaultRegistry; import cloud.katta.testsetup.AbstractHubTest; @@ -232,7 +233,9 @@ public void test03AddVault(final HubTestConfig config) throws Exception { final Path bucket = new Path(storageProfileWrapper.getProtocol() == Protocol.S3_STS ? storageProfileWrapper.getBucketPrefix() + vaultId : config.vault.bucketName, EnumSet.of(Path.Type.volume, Path.Type.directory)); final HubUVFVault cryptomator = new HubUVFVault(bucket); - cryptomator.create(hubSession, storageProfileWrapper.getRegion(), new VaultCredentials("test")); + + cryptomator.create(hubSession, new HubStorageLocationService.StorageLocation(storageProfileWrapper.getId().toString(), storageProfileWrapper.getRegion(), + storageProfileWrapper.getName()).getIdentifier(), new VaultCredentials("test")); final AttributedList vaults = hubSession.getFeature(ListService.class).list(Home.root(), new DisabledListProgressListener()); assertFalse(vaults.isEmpty()); diff --git a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java index 38efde43..eb53dc84 100644 --- a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java +++ b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java @@ -31,6 +31,7 @@ import cloud.katta.model.SetupCodeJWE; import cloud.katta.model.StorageProfileDtoWrapper; import cloud.katta.protocols.hub.HubSession; +import cloud.katta.protocols.hub.HubStorageLocationService; import cloud.katta.protocols.hub.HubUVFVault; import cloud.katta.testsetup.AbstractHubTest; import cloud.katta.testsetup.HubTestConfig; @@ -64,7 +65,8 @@ public void testHubWorkflow(final HubTestConfig config) throws Exception { final Path bucket = new Path(storageProfileWrapper.getProtocol() == Protocol.S3_STS ? storageProfileWrapper.getBucketPrefix() + vaultId : config.vault.bucketName, EnumSet.of(Path.Type.volume, Path.Type.directory)); final HubUVFVault cryptomator = new HubUVFVault(bucket); - cryptomator.create(hubSession, storageProfileWrapper.getRegion(), new VaultCredentials("test")); + cryptomator.create(hubSession, new HubStorageLocationService.StorageLocation(storageProfileWrapper.getId().toString(), storageProfileWrapper.getRegion(), + storageProfileWrapper.getName()).getIdentifier(), new VaultCredentials("test")); checkNumberOfVaults(hubSession, config, vaultId, 0, 0, 1, 0, 0); From 9e5c759e9767f1a611cc4ee61e6deda3018008da Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sun, 12 Oct 2025 18:45:00 +0200 Subject: [PATCH 076/133] Fix return value. --- hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 6b0b5ab4..629fb01f 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -66,6 +66,7 @@ public class HubUVFVault extends UVFVault { * Storage connection only available after loading vault */ private Session storage; + private Path home; public HubUVFVault(final Path home) { this(home, null, null, null); @@ -78,6 +79,7 @@ public HubUVFVault(final Path home) { */ public HubUVFVault(final Path home, final String masterkey, final String config, final byte[] pepper) { super(home); + this.home = home; this.vaultId = UUID.fromString(new UUIDRandomStringService().random()); } @@ -175,7 +177,7 @@ public Path create(final Session session, final String region, final VaultCre log.debug("Upload vault template to {}", storage); final Path vault; if(false) { - vault = super.create(storage, region, credentials); + return super.create(storage, region, credentials); } else { // Obsolete when implemented in super final Directory directory = (Directory) storage._getFeature(Directory.class); From 9fa64957099516d3cc53b5dd79290b4f7dd14da5 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sun, 12 Oct 2025 18:45:17 +0200 Subject: [PATCH 077/133] Register storage configurations on login. --- .../cloud/katta/protocols/hub/HubSession.java | 21 +++++++++++++++++++ .../protocols/hub/HubVaultListService.java | 19 ----------------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index e069b2fb..3b1a638c 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -44,18 +44,22 @@ import org.apache.logging.log4j.Logger; import java.io.InputStream; +import java.util.List; import java.util.Map; import java.util.Optional; import cloud.katta.client.ApiException; import cloud.katta.client.HubApiClient; import cloud.katta.client.api.ConfigResourceApi; +import cloud.katta.client.api.StorageProfileResourceApi; import cloud.katta.client.api.UsersResourceApi; import cloud.katta.client.model.ConfigDto; +import cloud.katta.client.model.StorageProfileDto; import cloud.katta.client.model.UserDto; import cloud.katta.core.DeviceSetupCallback; import cloud.katta.crypto.DeviceKeys; import cloud.katta.crypto.UserKeys; +import cloud.katta.model.StorageProfileDtoWrapper; import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService; import cloud.katta.protocols.hub.serializer.HubConfigDtoDeserializer; import cloud.katta.workflows.DeviceKeysServiceImpl; @@ -163,6 +167,23 @@ public void login(final LoginCallback prompt, final CancelCallback cancel) throw final DeviceSetupCallback setup = prompt.getFeature(DeviceSetupCallback.class); log.debug("Configured with setup prompt {}", setup); userKeys = this.pair(setup); + final List storageProfileDtos = new StorageProfileResourceApi(client).apiStorageprofileGet(false); + for(StorageProfileDto storageProfileDto : storageProfileDtos) { + final StorageProfileDtoWrapper storageProfile = StorageProfileDtoWrapper.coerce(storageProfileDto); + log.debug("Read storage profile {}", storageProfile); + switch(storageProfile.getProtocol()) { + case S3: + case S3_STS: + final ProtocolFactory protocols = ProtocolFactory.get(); + final Profile profile = new HubAwareProfile(this, protocols.forType(protocols.find(ProtocolFactory.BUNDLED_PROFILE_PREDICATE), Protocol.Type.s3), + config, storageProfile); + log.debug("Register profile {}", profile); + protocols.register(profile); + break; + default: + throw new InteroperabilityException(String.format("Unsupported storage configuration %s", storageProfile.getProtocol().name())); + } + } // Ensure vaults are registered try { vaults = new HubVaultListService(this, prompt).list(Home.root(), new DisabledListProgressListener()); diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java index 0aad78eb..953c8039 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java @@ -11,12 +11,8 @@ import ch.cyberduck.core.PasswordCallback; import ch.cyberduck.core.Path; import ch.cyberduck.core.PathAttributes; -import ch.cyberduck.core.Profile; -import ch.cyberduck.core.Protocol; -import ch.cyberduck.core.ProtocolFactory; import ch.cyberduck.core.exception.AccessDeniedException; import ch.cyberduck.core.exception.BackgroundException; -import ch.cyberduck.core.vault.VaultException; import ch.cyberduck.core.vault.VaultRegistry; import ch.cyberduck.core.vault.VaultUnlockCancelException; @@ -31,7 +27,6 @@ import cloud.katta.client.api.VaultResourceApi; import cloud.katta.client.model.VaultDto; import cloud.katta.crypto.uvf.UvfMetadataPayload; -import cloud.katta.model.StorageProfileDtoWrapper; import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService; import cloud.katta.workflows.VaultServiceImpl; import cloud.katta.workflows.exceptions.AccessException; @@ -63,20 +58,6 @@ public AttributedList list(final Path directory, final ListProgressListene // Find storage configuration in vault metadata final VaultServiceImpl vaultService = new VaultServiceImpl(session); final UvfMetadataPayload vaultMetadata = vaultService.getVaultMetadataJWE(vaultDto.getId(), session.getUserKeys()); - final StorageProfileDtoWrapper storageProfile = new VaultServiceImpl(session).getVaultStorageProfile(vaultMetadata); - log.debug("Read storage profile {}", storageProfile); - switch(storageProfile.getProtocol()) { - case S3: - case S3_STS: - final ProtocolFactory protocols = ProtocolFactory.get(); - final Profile profile = new HubAwareProfile(session, protocols.forType(protocols.find(ProtocolFactory.BUNDLED_PROFILE_PREDICATE), Protocol.Type.s3), - session.getConfig(), storageProfile); - log.debug("Register profile {}", profile); - protocols.register(profile); - break; - default: - throw new VaultException(String.format("Unsupported storage configuration %s", storageProfile.getProtocol().name())); - } final HubUVFVault vault = new HubUVFVault(vaultDto.getId(), new Path(vaultMetadata.storage().getDefaultPath(), EnumSet.of(Path.Type.directory, Path.Type.volume), new PathAttributes().setDisplayname(vaultMetadata.storage().getNickname()))); From a6dba375168d8b19c75a31085f27f12166b20ba8 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sun, 12 Oct 2025 21:14:38 +0200 Subject: [PATCH 078/133] Rename variable. --- .../main/java/cloud/katta/protocols/hub/HubUVFVault.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 629fb01f..61e8ce11 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -126,10 +126,10 @@ public synchronized void close() { public Path create(final Session session, final String region, final VaultCredentials credentials) throws BackgroundException { try { final HubStorageLocationService.StorageLocation location = HubStorageLocationService.StorageLocation.fromIdentifier(region); - final String storageProfile = location.getProfile(); + final String storageProfileId = location.getProfile(); final UvfMetadataPayload metadataPayload = UvfMetadataPayload.create() .withStorage(new VaultMetadataJWEBackendDto() - .provider(storageProfile) + .provider(storageProfileId) .defaultPath(session.getFeature(PathContainerService.class).getContainer(home).getName()) .region(location.getRegion()) .nickname(null != home.attributes().getDisplayname() ? home.attributes().getDisplayname() : "Vault")) @@ -163,7 +163,7 @@ public Path create(final Session session, final String region, final VaultCre vaultResourceApi.apiVaultsVaultIdAccessTokensPost(vaultDto.getId(), Collections.singletonMap(userDto.getId(), jwks.toOwnerAccessToken().encryptForUser(userKeys.ecdhKeyPair().getPublic()))); // Upload vault template to storage - final Protocol profile = ProtocolFactory.get().forName(storageProfile); + final Protocol profile = ProtocolFactory.get().forName(storageProfileId); log.debug("Loaded profile {} for vault {}", profile, this); final Host bookmark = new Host(profile, session.getFeature(CredentialsConfigurator.class).reload().configure(session.getHost())); From 4d9657eac9d682cef98323bbc3fff93f191ccaed Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sun, 12 Oct 2025 21:15:09 +0200 Subject: [PATCH 079/133] Revert "Fail with exception when storage does not support feature." This reverts commit 5bec1524 --- hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 61e8ce11..926defd5 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -99,13 +99,13 @@ public Session getStorage() { } @Override - public T getFeature(final Session hub, final Class type, final T delegate) throws UnsupportedException { + public T getFeature(final Session hub, final Class type, final T delegate) { log.debug("Delegate to {} for feature {}", storage, type); // Ignore feature implementation but delegate to storage backend final T feature = storage._getFeature(type); if(null == feature) { log.warn("No feature {} available for {}", type, storage); - throw new UnsupportedException(); + return null; } return super.getFeature(storage, type, feature); } From ff2ff778917932f0270e6c6a02fd3292825eb815 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sun, 12 Oct 2025 22:27:24 +0200 Subject: [PATCH 080/133] Adopt reusable authentication with temporeary credentials assuming role from STS. --- .../protocols/s3/S3AssumeRoleSession.java | 55 ++++--------------- 1 file changed, 10 insertions(+), 45 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleSession.java b/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleSession.java index fa5a63cf..31d3778b 100644 --- a/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleSession.java +++ b/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleSession.java @@ -7,33 +7,20 @@ import ch.cyberduck.core.Host; import ch.cyberduck.core.LoginCallback; import ch.cyberduck.core.OAuthTokens; -import ch.cyberduck.core.aws.CustomClientConfiguration; import ch.cyberduck.core.exception.LoginCanceledException; -import ch.cyberduck.core.http.CustomServiceUnavailableRetryStrategy; -import ch.cyberduck.core.http.ExecutionCountServiceUnavailableRetryStrategy; import ch.cyberduck.core.oauth.OAuth2AuthorizationService; import ch.cyberduck.core.oauth.OAuth2RequestInterceptor; import ch.cyberduck.core.preferences.HostPreferencesFactory; -import ch.cyberduck.core.proxy.ProxyFinder; -import ch.cyberduck.core.s3.S3AuthenticationResponseInterceptor; import ch.cyberduck.core.s3.S3CredentialsStrategy; import ch.cyberduck.core.s3.S3Session; -import ch.cyberduck.core.ssl.ThreadLocalHostnameDelegatingTrustManager; import ch.cyberduck.core.ssl.X509KeyManager; import ch.cyberduck.core.ssl.X509TrustManager; -import ch.cyberduck.core.sts.STSAssumeRoleCredentialsRequestInterceptor; +import ch.cyberduck.core.sts.STSRequestInterceptor; -import org.apache.commons.lang3.StringUtils; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.AnonymousAWSCredentials; -import com.amazonaws.client.builder.AwsClientBuilder; -import com.amazonaws.services.securitytoken.AWSSecurityTokenService; -import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder; - import static cloud.katta.protocols.s3.S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE; public class S3AssumeRoleSession extends S3Session { @@ -45,15 +32,14 @@ public S3AssumeRoleSession(final Host host, final X509TrustManager trust, final /** * Configured by default with credentials strategy using assume role with web identity followed by - * exchaing the retrieved OIDC token with scoped OAuth tokens to obtain temporary credentials from security + * exchanging the retrieved OIDC token with scoped OAuth tokens to obtain temporary credentials from security * token server (STS) * * @see S3AssumeRoleProtocol#OAUTH_TOKENEXCHANGE * @see S3AssumeRoleProtocol#S3_ASSUMEROLE_ROLEARN_TAG */ @Override - protected S3CredentialsStrategy configureCredentialsStrategy(final ProxyFinder proxy, final HttpClientBuilder configuration, - final LoginCallback prompt) throws LoginCanceledException { + protected S3CredentialsStrategy configureCredentialsStrategy(final HttpClientBuilder configuration, final LoginCallback prompt) throws LoginCanceledException { if(host.getProtocol().isOAuthConfigurable()) { final OAuth2RequestInterceptor oauth; if(HostPreferencesFactory.get(host).getBoolean(OAUTH_TOKENEXCHANGE)) { @@ -68,37 +54,16 @@ protected S3CredentialsStrategy configureCredentialsStrategy(final ProxyFinder p } log.debug("Register interceptor {}", oauth); configuration.addInterceptorLast(oauth); - final AWSSecurityTokenService tokenService = AWSSecurityTokenServiceClientBuilder - .standard() - .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(host.getProtocol().getSTSEndpoint(), null)) - .withCredentials(new AWSStaticCredentialsProvider(new AnonymousAWSCredentials())) - .withClientConfiguration(new CustomClientConfiguration(host, - new ThreadLocalHostnameDelegatingTrustManager(trust, host.getProtocol().getSTSEndpoint()), key)) - .build(); - final STSAssumeRoleCredentialsRequestInterceptor sts; - if(StringUtils.isNotBlank(host.getProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_2))) { - sts = new STSChainedAssumeRoleRequestInterceptor(oauth, this, tokenService, prompt) { - @Override - protected String getWebIdentityToken(final OAuthTokens oauth) { - return oauth.getAccessToken(); - } - }; - } - else { - sts = new STSAssumeRoleCredentialsRequestInterceptor(oauth, this, tokenService, prompt) { - @Override - protected String getWebIdentityToken(final OAuthTokens oauth) { - return oauth.getAccessToken(); - } - }; - } + final STSRequestInterceptor sts = new STSChainedAssumeRoleRequestInterceptor(oauth, host, trust, key, prompt) { + @Override + protected String getWebIdentityToken(final OAuthTokens oauth) { + return oauth.getAccessToken(); + } + }; log.debug("Register interceptor {}", sts); configuration.addInterceptorLast(sts); - final S3AuthenticationResponseInterceptor interceptor = new S3AuthenticationResponseInterceptor(this, sts); - configuration.setServiceUnavailableRetryStrategy(new CustomServiceUnavailableRetryStrategy(host, - new ExecutionCountServiceUnavailableRetryStrategy(interceptor))); return sts; } - return super.configureCredentialsStrategy(proxy, configuration, prompt); + return super.configureCredentialsStrategy(configuration, prompt); } } From ba602dbac7d88ec0d7429b54bef29452142a8f2d Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 13 Oct 2025 16:05:40 +0200 Subject: [PATCH 081/133] Add new parameters. --- .../main/java/cloud/katta/protocols/hub/HubUVFVault.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 926defd5..9fd356d9 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -9,13 +9,12 @@ import ch.cyberduck.core.cryptomator.UVFVault; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.exception.InteroperabilityException; -import ch.cyberduck.core.exception.UnsupportedException; -import ch.cyberduck.core.features.Write; import ch.cyberduck.core.features.AttributesFinder; import ch.cyberduck.core.features.Directory; import ch.cyberduck.core.features.Write; import ch.cyberduck.core.preferences.PreferencesFactory; import ch.cyberduck.core.proxy.ProxyFactory; +import ch.cyberduck.core.s3.S3Session; import ch.cyberduck.core.ssl.X509KeyManager; import ch.cyberduck.core.ssl.X509TrustManager; import ch.cyberduck.core.transfer.TransferStatus; @@ -154,7 +153,8 @@ public Path create(final Session session, final String region, final VaultCre // Create vault in Hub final VaultResourceApi vaultResourceApi = new VaultResourceApi(hub.getClient()); log.debug("Create vault {}", vaultDto); - vaultResourceApi.apiVaultsVaultIdPut(vaultDto.getId(), vaultDto); + vaultResourceApi.apiVaultsVaultIdPut(vaultDto.getId(), vaultDto, + !S3Session.isAwsHostname(session.getHost().getHostname()), S3Session.isAwsHostname(session.getHost().getHostname())); // Upload JWE log.debug("Grant access to vault {}", vaultDto); final UserDto userDto = new UsersResourceApi(hub.getClient()).apiUsersMeGet(false, false); From 056b171048b9b70a1834d29643ae906228baa81c Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 13 Oct 2025 20:18:49 +0200 Subject: [PATCH 082/133] Require test to create bucket for vault. --- .../test/resources/docker-compose-minio-localhost-hub.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/hub/src/test/resources/docker-compose-minio-localhost-hub.yml b/hub/src/test/resources/docker-compose-minio-localhost-hub.yml index 1178a74a..0030717d 100644 --- a/hub/src/test/resources/docker-compose-minio-localhost-hub.yml +++ b/hub/src/test/resources/docker-compose-minio-localhost-hub.yml @@ -226,11 +226,10 @@ services: /mc idp openid ls myminio /mc idp openid info myminio - # if container is restarted, the bucket already exists... - /mc mb myminio/handmade --with-versioning || true - /mc rm --recursive --force myminio/handmade + # if container is restarted, the bucket already exists + /mc rb myminio/handmade || true - echo "createbuckets successful" + echo "Completed MinIO Setup" hub: build: From 56459f9af5b0c11175b96e8ec07fd468922ebd45 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 13 Oct 2025 22:05:17 +0200 Subject: [PATCH 083/133] Assign bucket. --- hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 9fd356d9..d5db1098 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -65,7 +65,7 @@ public class HubUVFVault extends UVFVault { * Storage connection only available after loading vault */ private Session storage; - private Path home; + private final Path home; public HubUVFVault(final Path home) { this(home, null, null, null); @@ -91,6 +91,7 @@ public HubUVFVault(final Path home, final String masterkey, final String config, public HubUVFVault(final UUID vaultId, final Path bucket) { super(bucket); this.vaultId = vaultId; + this.home = bucket; } public Session getStorage() { From 7fca9af7c34583a542581ced4ca64f0f41287e42 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 13 Oct 2025 22:06:03 +0200 Subject: [PATCH 084/133] No preloading of buckets. --- .../java/cloud/katta/protocols/hub/HubSession.java | 12 +----------- .../katta/protocols/hub/HubVaultListService.java | 8 +++----- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 3b1a638c..623c80b5 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -90,7 +90,6 @@ public class HubSession extends HttpSession { private UserDto me; private ConfigDto config; private UserKeys userKeys; - private AttributedList vaults; public HubSession(final Host host, final X509TrustManager trust, final X509KeyManager key) { super(host, trust, key); @@ -184,15 +183,6 @@ public void login(final LoginCallback prompt, final CancelCallback cancel) throw throw new InteroperabilityException(String.format("Unsupported storage configuration %s", storageProfile.getProtocol().name())); } } - // Ensure vaults are registered - try { - vaults = new HubVaultListService(this, prompt).list(Home.root(), new DisabledListProgressListener()); - } - finally { - log.debug("Destroyed user keys {}", userKeys); - // Short-lived - userKeys.destroy(); - } } catch(ApiException e) { throw new HubExceptionMappingService().map(e); @@ -253,7 +243,7 @@ public UserKeys getUserKeys() { @SuppressWarnings("unchecked") public T _getFeature(final Class type) { if(type == ListService.class) { - return (T) (ListService) (directory, listener) -> vaults; + return (T) new HubVaultListService(this); } if(type == Scheduler.class) { return (T) access; diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java index 953c8039..128be24a 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java @@ -5,10 +5,10 @@ package cloud.katta.protocols.hub; import ch.cyberduck.core.AttributedList; +import ch.cyberduck.core.DisabledPasswordCallback; import ch.cyberduck.core.ListProgressListener; import ch.cyberduck.core.ListService; import ch.cyberduck.core.LocaleFactory; -import ch.cyberduck.core.PasswordCallback; import ch.cyberduck.core.Path; import ch.cyberduck.core.PathAttributes; import ch.cyberduck.core.exception.AccessDeniedException; @@ -36,11 +36,9 @@ public class HubVaultListService implements ListService { private static final Logger log = LogManager.getLogger(HubVaultListService.class); private final HubSession session; - private final PasswordCallback prompt; - public HubVaultListService(final HubSession session, final PasswordCallback prompt) { + public HubVaultListService(final HubSession session) { this.session = session; - this.prompt = prompt; } @Override @@ -62,7 +60,7 @@ public AttributedList list(final Path directory, final ListProgressListene new Path(vaultMetadata.storage().getDefaultPath(), EnumSet.of(Path.Type.directory, Path.Type.volume), new PathAttributes().setDisplayname(vaultMetadata.storage().getNickname()))); try { - registry.add(vault.load(session, prompt)); + registry.add(vault.load(session, new DisabledPasswordCallback())); vaults.add(vault.getHome()); listener.chunk(directory, vaults); } From 0c67e00d2451d4a1a3e9e95c0d855a059994eeda Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 13 Oct 2025 22:32:56 +0200 Subject: [PATCH 085/133] Unused. --- .../test/java/cloud/katta/core/AbstractHubSynchronizeTest.java | 2 +- .../java/cloud/katta/workflows/AbstractHubWorkflowTest.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java index ae1ed74d..22464cdc 100644 --- a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java +++ b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java @@ -235,7 +235,7 @@ public void test03AddVault(final HubTestConfig config) throws Exception { final HubUVFVault cryptomator = new HubUVFVault(bucket); cryptomator.create(hubSession, new HubStorageLocationService.StorageLocation(storageProfileWrapper.getId().toString(), storageProfileWrapper.getRegion(), - storageProfileWrapper.getName()).getIdentifier(), new VaultCredentials("test")); + storageProfileWrapper.getName()).getIdentifier(), new VaultCredentials(StringUtils.EMPTY)); final AttributedList vaults = hubSession.getFeature(ListService.class).list(Home.root(), new DisabledListProgressListener()); assertFalse(vaults.isEmpty()); diff --git a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java index eb53dc84..cc323ed4 100644 --- a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java +++ b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java @@ -7,6 +7,7 @@ import ch.cyberduck.core.Path; import ch.cyberduck.core.vault.VaultCredentials; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.params.ParameterizedTest; @@ -66,7 +67,7 @@ public void testHubWorkflow(final HubTestConfig config) throws Exception { EnumSet.of(Path.Type.volume, Path.Type.directory)); final HubUVFVault cryptomator = new HubUVFVault(bucket); cryptomator.create(hubSession, new HubStorageLocationService.StorageLocation(storageProfileWrapper.getId().toString(), storageProfileWrapper.getRegion(), - storageProfileWrapper.getName()).getIdentifier(), new VaultCredentials("test")); + storageProfileWrapper.getName()).getIdentifier(), new VaultCredentials(StringUtils.EMPTY)); checkNumberOfVaults(hubSession, config, vaultId, 0, 0, 1, 0, 0); From 24ce8d4033b2d5c7234debfbcce7c2977731787c Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 13 Oct 2025 23:08:54 +0200 Subject: [PATCH 086/133] Require storage credentials. --- .../katta/protocols/hub/HubUVFVault.java | 54 ++++++++++--------- .../protocols/hub/HubVaultListService.java | 8 ++- .../core/AbstractHubSynchronizeTest.java | 9 ++-- .../workflows/AbstractHubWorkflowTest.java | 5 +- 4 files changed, 42 insertions(+), 34 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index d5db1098..2da81314 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -4,7 +4,20 @@ package cloud.katta.protocols.hub; -import ch.cyberduck.core.*; +import ch.cyberduck.core.Credentials; +import ch.cyberduck.core.CredentialsConfigurator; +import ch.cyberduck.core.DisabledCancelCallback; +import ch.cyberduck.core.DisabledHostKeyCallback; +import ch.cyberduck.core.DisabledLoginCallback; +import ch.cyberduck.core.Host; +import ch.cyberduck.core.HostUrlProvider; +import ch.cyberduck.core.PasswordCallback; +import ch.cyberduck.core.Path; +import ch.cyberduck.core.PathAttributes; +import ch.cyberduck.core.Protocol; +import ch.cyberduck.core.ProtocolFactory; +import ch.cyberduck.core.Session; +import ch.cyberduck.core.SessionFactory; import ch.cyberduck.core.cryptomator.ContentWriter; import ch.cyberduck.core.cryptomator.UVFVault; import ch.cyberduck.core.exception.BackgroundException; @@ -66,32 +79,20 @@ public class HubUVFVault extends UVFVault { */ private Session storage; private final Path home; - - public HubUVFVault(final Path home) { - this(home, null, null, null); - } - - /** - * Constructor for factory creating new vault - * - * @param home Bucket - */ - public HubUVFVault(final Path home, final String masterkey, final String config, final byte[] pepper) { - super(home); - this.home = home; - this.vaultId = UUID.fromString(new UUIDRandomStringService().random()); - } + private final Credentials credentials; /** * Open from existing metadata * - * @param vaultId Vault ID Used to lookup profile - * @param bucket Bucket name + * @param vaultId Vault ID Used to lookup profile + * @param bucket Bucket name + * @param credentials Storage access credentials */ - public HubUVFVault(final UUID vaultId, final Path bucket) { + public HubUVFVault(final UUID vaultId, final Path bucket, final Credentials credentials) { super(bucket); this.vaultId = vaultId; this.home = bucket; + this.credentials = credentials; } public Session getStorage() { @@ -123,14 +124,14 @@ public synchronized void close() { } @Override - public Path create(final Session session, final String region, final VaultCredentials credentials) throws BackgroundException { + public Path create(final Session session, final String region, final VaultCredentials noop) throws BackgroundException { try { final HubStorageLocationService.StorageLocation location = HubStorageLocationService.StorageLocation.fromIdentifier(region); final String storageProfileId = location.getProfile(); final UvfMetadataPayload metadataPayload = UvfMetadataPayload.create() .withStorage(new VaultMetadataJWEBackendDto() .provider(storageProfileId) - .defaultPath(session.getFeature(PathContainerService.class).getContainer(home).getName()) + .defaultPath(home.getAbsolute()) .region(location.getRegion()) .nickname(null != home.attributes().getDisplayname() ? home.attributes().getDisplayname() : "Vault")) .withAutomaticAccessGrant(new VaultMetadataJWEAutomaticAccessGrantDto() @@ -166,10 +167,10 @@ public Path create(final Session session, final String region, final VaultCre // Upload vault template to storage final Protocol profile = ProtocolFactory.get().forName(storageProfileId); log.debug("Loaded profile {} for vault {}", profile, this); - final Host bookmark = new Host(profile, - session.getFeature(CredentialsConfigurator.class).reload().configure(session.getHost())); + final Host bookmark = new Host(profile, credentials); bookmark.setProperty(OAUTH_TOKENEXCHANGE_VAULT, vaultId.toString()); bookmark.setRegion(location.getRegion()); + bookmark.setDefaultPath(home.getAbsolute()); log.debug("Configured {} for vault {}", bookmark, this); storage = SessionFactory.create(bookmark, session.getFeature(X509TrustManager.class), session.getFeature(X509KeyManager.class)); log.debug("Connect to {}", storage); @@ -178,7 +179,7 @@ public Path create(final Session session, final String region, final VaultCre log.debug("Upload vault template to {}", storage); final Path vault; if(false) { - return super.create(storage, region, credentials); + return super.create(storage, region, noop); } else { // Obsolete when implemented in super final Directory directory = (Directory) storage._getFeature(Directory.class); @@ -277,8 +278,9 @@ public HubUVFVault load(final Session session, final PasswordCallback prompt) @Override public String toString() { final StringBuilder sb = new StringBuilder("HubUVFVault{"); - sb.append("storage=").append(storage); - sb.append(", vaultId=").append(vaultId); + sb.append("vaultId=").append(vaultId); + sb.append(", home=").append(home); + sb.append(", credentials=").append(credentials); sb.append('}'); return sb.toString(); } diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java index 128be24a..3b4720a1 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java @@ -5,6 +5,7 @@ package cloud.katta.protocols.hub; import ch.cyberduck.core.AttributedList; +import ch.cyberduck.core.Credentials; import ch.cyberduck.core.DisabledPasswordCallback; import ch.cyberduck.core.ListProgressListener; import ch.cyberduck.core.ListService; @@ -27,6 +28,7 @@ import cloud.katta.client.api.VaultResourceApi; import cloud.katta.client.model.VaultDto; import cloud.katta.crypto.uvf.UvfMetadataPayload; +import cloud.katta.crypto.uvf.VaultMetadataJWEBackendDto; import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService; import cloud.katta.workflows.VaultServiceImpl; import cloud.katta.workflows.exceptions.AccessException; @@ -56,9 +58,11 @@ public AttributedList list(final Path directory, final ListProgressListene // Find storage configuration in vault metadata final VaultServiceImpl vaultService = new VaultServiceImpl(session); final UvfMetadataPayload vaultMetadata = vaultService.getVaultMetadataJWE(vaultDto.getId(), session.getUserKeys()); + final VaultMetadataJWEBackendDto storage = vaultMetadata.storage(); final HubUVFVault vault = new HubUVFVault(vaultDto.getId(), - new Path(vaultMetadata.storage().getDefaultPath(), EnumSet.of(Path.Type.directory, Path.Type.volume), - new PathAttributes().setDisplayname(vaultMetadata.storage().getNickname()))); + new Path(storage.getDefaultPath(), EnumSet.of(Path.Type.directory, Path.Type.volume), + new PathAttributes().setDisplayname(storage.getNickname())), + new Credentials(storage.getUsername(), storage.getPassword())); try { registry.add(vault.load(session, new DisabledPasswordCallback())); vaults.add(vault.getHome()); diff --git a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java index 22464cdc..646384e1 100644 --- a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java +++ b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java @@ -6,6 +6,7 @@ import ch.cyberduck.core.AlphanumericRandomStringService; import ch.cyberduck.core.AttributedList; +import ch.cyberduck.core.Credentials; import ch.cyberduck.core.DisabledConnectionCallback; import ch.cyberduck.core.DisabledListProgressListener; import ch.cyberduck.core.ListService; @@ -13,6 +14,7 @@ import ch.cyberduck.core.Path; import ch.cyberduck.core.Session; import ch.cyberduck.core.SimplePathPredicate; +import ch.cyberduck.core.UUIDRandomStringService; import ch.cyberduck.core.exception.AccessDeniedException; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.exception.NotfoundException; @@ -232,8 +234,8 @@ public void test03AddVault(final HubTestConfig config) throws Exception { final Path bucket = new Path(storageProfileWrapper.getProtocol() == Protocol.S3_STS ? storageProfileWrapper.getBucketPrefix() + vaultId : config.vault.bucketName, EnumSet.of(Path.Type.volume, Path.Type.directory)); - final HubUVFVault cryptomator = new HubUVFVault(bucket); - + final HubUVFVault cryptomator = new HubUVFVault(UUID.fromString(new UUIDRandomStringService().random()), bucket, + new Credentials(config.vault.username, config.vault.password)); cryptomator.create(hubSession, new HubStorageLocationService.StorageLocation(storageProfileWrapper.getId().toString(), storageProfileWrapper.getRegion(), storageProfileWrapper.getName()).getIdentifier(), new VaultCredentials(StringUtils.EMPTY)); @@ -248,9 +250,6 @@ public void test03AddVault(final HubTestConfig config) throws Exception { assertEquals(config.vault.region, hubSession.getFeature(AttributesFinder.class).find(bucket).getRegion()); assertNotSame(Vault.DISABLED, vaultRegistry.find(hubSession, bucket)); - - // listing decrypted file names - assertNotSame(Vault.DISABLED, vaultRegistry.find(hubSession, bucket)); } final Path vault = vaults.find(new SimplePathPredicate(bucket)); diff --git a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java index cc323ed4..a970541d 100644 --- a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java +++ b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java @@ -4,7 +4,9 @@ package cloud.katta.workflows; +import ch.cyberduck.core.Credentials; import ch.cyberduck.core.Path; +import ch.cyberduck.core.UUIDRandomStringService; import ch.cyberduck.core.vault.VaultCredentials; import org.apache.commons.lang3.StringUtils; @@ -65,7 +67,8 @@ public void testHubWorkflow(final HubTestConfig config) throws Exception { final Path bucket = new Path(storageProfileWrapper.getProtocol() == Protocol.S3_STS ? storageProfileWrapper.getBucketPrefix() + vaultId : config.vault.bucketName, EnumSet.of(Path.Type.volume, Path.Type.directory)); - final HubUVFVault cryptomator = new HubUVFVault(bucket); + final HubUVFVault cryptomator = new HubUVFVault(UUID.fromString(new UUIDRandomStringService().random()), bucket, + new Credentials(config.vault.username, config.vault.password)); cryptomator.create(hubSession, new HubStorageLocationService.StorageLocation(storageProfileWrapper.getId().toString(), storageProfileWrapper.getRegion(), storageProfileWrapper.getName()).getIdentifier(), new VaultCredentials(StringUtils.EMPTY)); From 31f7cae035981977114e72280844ffc744ee08ac Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 14 Oct 2025 12:34:26 +0200 Subject: [PATCH 087/133] Extract region name. --- hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 2da81314..43ed4207 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -179,12 +179,13 @@ public Path create(final Session session, final String region, final VaultCre log.debug("Upload vault template to {}", storage); final Path vault; if(false) { - return super.create(storage, region, noop); + return super.create(storage, + HubStorageLocationService.StorageLocation.fromIdentifier(region).getRegion(), noop); } else { // Obsolete when implemented in super final Directory directory = (Directory) storage._getFeature(Directory.class); log.debug("Create vault root directory at {}", home); - final TransferStatus status = (new TransferStatus()).setRegion(region); + final TransferStatus status = (new TransferStatus()).setRegion(HubStorageLocationService.StorageLocation.fromIdentifier(region).getRegion()); vault = directory.mkdir(storage._getFeature(Write.class), home, status); final String hashedRootDirId = metadataPayload.computeRootDirIdHash(); From 56ff305786a1a5c0d189ad3659ce9485602c4465 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 14 Oct 2025 12:34:54 +0200 Subject: [PATCH 088/133] Use cached keys. --- hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 43ed4207..f28bea28 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -159,9 +159,8 @@ public Path create(final Session session, final String region, final VaultCre !S3Session.isAwsHostname(session.getHost().getHostname()), S3Session.isAwsHostname(session.getHost().getHostname())); // Upload JWE log.debug("Grant access to vault {}", vaultDto); - final UserDto userDto = new UsersResourceApi(hub.getClient()).apiUsersMeGet(false, false); - final DeviceKeys deviceKeys = new DeviceKeysServiceImpl().getDeviceKeys(session.getHost()); - final UserKeys userKeys = new UserKeysServiceImpl(hub).getUserKeys(session.getHost(), hub.getMe(), deviceKeys); + final UserDto userDto = hub.getMe(); + final UserKeys userKeys = hub.getUserKeys(); vaultResourceApi.apiVaultsVaultIdAccessTokensPost(vaultDto.getId(), Collections.singletonMap(userDto.getId(), jwks.toOwnerAccessToken().encryptForUser(userKeys.ecdhKeyPair().getPublic()))); // Upload vault template to storage From 7b9e6529449ac460f4a7681e004c729f76888942 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 14 Oct 2025 14:31:40 +0200 Subject: [PATCH 089/133] Empty initialization. --- .../hub/HubOAuthTokensCredentialsConfigurator.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubOAuthTokensCredentialsConfigurator.java b/hub/src/main/java/cloud/katta/protocols/hub/HubOAuthTokensCredentialsConfigurator.java index 76c0ae18..f6df3cca 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubOAuthTokensCredentialsConfigurator.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubOAuthTokensCredentialsConfigurator.java @@ -19,18 +19,11 @@ public class HubOAuthTokensCredentialsConfigurator implements CredentialsConfigu private final HostPasswordStore keychain; private final Host host; - private OAuthTokens tokens; + private OAuthTokens tokens = OAuthTokens.EMPTY; public HubOAuthTokensCredentialsConfigurator(final HostPasswordStore keychain, final Host host) { this.keychain = keychain; this.host = host; - final Credentials credentials = host.getCredentials(); - // Copy prior reset of credentials after login - this.tokens = new OAuthTokens(credentials.getOauth().getAccessToken(), - credentials.getOauth().getRefreshToken(), - credentials.getOauth().getExpiryInMilliseconds(), - credentials.getOauth().getIdToken()); - log.debug("Initialized tokens {}", tokens); } @Override From a749fe21a4c69d4f2118ea2f4d52d561dfbb1ed2 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 14 Oct 2025 14:33:26 +0200 Subject: [PATCH 090/133] Throw failure when attempting to list non-root. --- .../java/cloud/katta/protocols/hub/HubVaultListService.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java index 3b4720a1..d43accb9 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java @@ -14,6 +14,7 @@ import ch.cyberduck.core.PathAttributes; import ch.cyberduck.core.exception.AccessDeniedException; import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.exception.NotfoundException; import ch.cyberduck.core.vault.VaultRegistry; import ch.cyberduck.core.vault.VaultUnlockCancelException; @@ -45,6 +46,7 @@ public HubVaultListService(final HubSession session) { @Override public AttributedList list(final Path directory, final ListProgressListener listener) throws BackgroundException { + if(directory.isRoot()) { try { final VaultRegistry registry = session.getRegistry(); final AttributedList vaults = new AttributedList<>(); @@ -89,6 +91,8 @@ public AttributedList list(final Path directory, final ListProgressListene catch(ApiException e) { throw new HubExceptionMappingService().map("Listing directory {0} failed", e, directory); } + } + throw new NotfoundException(directory.getAbsolute()); } @Override From bd9da57a2a86408e7a6bc835b3aa610d6f8b94d7 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 14 Oct 2025 14:34:38 +0200 Subject: [PATCH 091/133] Determine access grant options depeding on storage profile. --- hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index f28bea28..ba6953a2 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -156,7 +156,7 @@ public Path create(final Session session, final String region, final VaultCre final VaultResourceApi vaultResourceApi = new VaultResourceApi(hub.getClient()); log.debug("Create vault {}", vaultDto); vaultResourceApi.apiVaultsVaultIdPut(vaultDto.getId(), vaultDto, - !S3Session.isAwsHostname(session.getHost().getHostname()), S3Session.isAwsHostname(session.getHost().getHostname())); + !S3Session.isAwsHostname(storage.getHost().getHostname()), S3Session.isAwsHostname(storage.getHost().getHostname())); // Upload JWE log.debug("Grant access to vault {}", vaultDto); final UserDto userDto = hub.getMe(); From 19ff1641f553c241d88c401b7428df141ae08cbc Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 14 Oct 2025 15:36:03 +0200 Subject: [PATCH 092/133] Revert "Resolve deprecated usages." This reverts commit 4aeb19ae7bc32a4bea009bddd7c9bbd5c9e29980. --- .../main/java/cloud/katta/workflows/UserKeysServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hub/src/main/java/cloud/katta/workflows/UserKeysServiceImpl.java b/hub/src/main/java/cloud/katta/workflows/UserKeysServiceImpl.java index df01dc8a..5fc09c71 100644 --- a/hub/src/main/java/cloud/katta/workflows/UserKeysServiceImpl.java +++ b/hub/src/main/java/cloud/katta/workflows/UserKeysServiceImpl.java @@ -147,7 +147,7 @@ private UserKeys uploadUserKeys(final UserDto me, final UserKeys userKeys, final try { usersResourceApi.apiUsersMePut(me.ecdhPublicKey(userKeys.encodedEcdhPublicKey()) .ecdsaPublicKey(userKeys.encodedEcdsaPublicKey()) - .privateKeys(userKeys.encryptWithSetupCode(setupCode)) + .privateKey(userKeys.encryptWithSetupCode(setupCode)) .setupCode(new SetupCodeJWE(setupCode).encryptForUser(userKeys.ecdhKeyPair().getPublic()))); } catch(JOSEException | JsonProcessingException e) { @@ -175,6 +175,6 @@ private UserKeys uploadDeviceKeys(final String deviceName, final UserKeys userKe } private static boolean validate(final UserDto me) { - return me.getEcdhPublicKey() != null && me.getPrivateKeys() != null; + return me.getEcdhPublicKey() != null && me.getPrivateKey() != null; } } From a6d0fe3700e31f4f84b7ea536f9a3650a155f1ed Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 14 Oct 2025 18:47:56 +0200 Subject: [PATCH 093/133] Set role configurable key. --- hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java | 3 ++- .../hub/serializer/StorageProfileDtoWrapperDeserializer.java | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index ba6953a2..79cb7b32 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -156,7 +156,8 @@ public Path create(final Session session, final String region, final VaultCre final VaultResourceApi vaultResourceApi = new VaultResourceApi(hub.getClient()); log.debug("Create vault {}", vaultDto); vaultResourceApi.apiVaultsVaultIdPut(vaultDto.getId(), vaultDto, - !S3Session.isAwsHostname(storage.getHost().getHostname()), S3Session.isAwsHostname(storage.getHost().getHostname())); + storage.getHost().getProtocol().isRoleConfigurable() && !S3Session.isAwsHostname(storage.getHost().getHostname()), + storage.getHost().getProtocol().isRoleConfigurable() && S3Session.isAwsHostname(storage.getHost().getHostname())); // Upload JWE log.debug("Grant access to vault {}", vaultDto); final UserDto userDto = hub.getMe(); diff --git a/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java b/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java index 7f8ebb83..c6e79aee 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java @@ -112,6 +112,8 @@ public Boolean booleanForKey(final String key) { return true; } break; + case ROLE_KEY_CONFIGURABLE_KEY: + return dto.getStsRoleArn() != null; } return super.booleanForKey(key); } @@ -146,6 +148,9 @@ public List keys() { if(dto.getRegions() != null) { keys.add(REGIONS_KEY); } + if(dto.getStsRoleArn() != null) { + keys.add(ROLE_KEY_CONFIGURABLE_KEY); + } return keys; } } From 4a06d84348daaaf6b111dafe2f1fc309dcbb52ec Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 14 Oct 2025 18:51:18 +0200 Subject: [PATCH 094/133] Fix region name. --- hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java b/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java index d0632f42..bce083e8 100644 --- a/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java +++ b/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java @@ -46,7 +46,7 @@ public abstract class AbstractHubTest extends VaultTest { } private static final HubTestConfig.VaultSpec minioSTSVaultConfig = new HubTestConfig.VaultSpec("MinIO STS", "732D43FA-3716-46C4-B931-66EA5405EF1C", - null, null, null, "eu-west-1"); + null, null, null, "eu-central-1"); private static final HubTestConfig.VaultSpec minioStaticVaultConfig = new HubTestConfig.VaultSpec("MinIO static", "71B910E0-2ECC-46DE-A871-8DB28549677E", "handmade", "minioadmin", "minioadmin", "us-east-1"); private static final HubTestConfig.VaultSpec awsSTSVaultConfig = new HubTestConfig.VaultSpec("AWS STS", "844BD517-96D4-4787-BCFA-238E103149F6", From 378bf995787628533961c5dc298e313b486b4e8f Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 14 Oct 2025 18:51:43 +0200 Subject: [PATCH 095/133] Review ARN property names. --- .../StorageProfileDtoWrapperDeserializer.java | 2 +- .../protocols/s3/S3AssumeRoleProtocol.java | 2 +- ...TSChainedAssumeRoleRequestInterceptor.java | 63 ++++--------------- 3 files changed, 14 insertions(+), 53 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java b/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java index c6e79aee..62f0452e 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java @@ -53,7 +53,7 @@ public List listForKey(final String key) { } if(dto.getProtocol() == Protocol.S3_STS) { properties.add(String.format("%s=%s", S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE, true)); - properties.add(String.format("%s=%s", S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN, dto.getStsRoleArn())); + properties.add(String.format("%s=%s", S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_WEBIDENTITY, dto.getStsRoleArn())); if(dto.getStsRoleArn2() != null) { properties.add(String.format("%s=%s", S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_TAG, dto.getStsRoleArn2())); } diff --git a/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleProtocol.java b/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleProtocol.java index efbc6fa8..ca19cea8 100644 --- a/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleProtocol.java +++ b/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleProtocol.java @@ -19,7 +19,7 @@ public class S3AssumeRoleProtocol extends S3Protocol { public static final String OAUTH_TOKENEXCHANGE_VAULT = "oauth.tokenexchange.vault"; // STS assume role with web identity resource name - public static final String S3_ASSUMEROLE_ROLEARN = Profile.STS_ROLE_ARN_PROPERTY_KEY; + public static final String S3_ASSUMEROLE_ROLEARN_WEBIDENTITY = Profile.STS_ROLE_ARN_PROPERTY_KEY; public static final String S3_ASSUMEROLE_DURATIONSECONDS = Profile.STS_DURATION_SECONDS_PROPERTY_KEY; // STS assume role chaining (AWS only) public static final String S3_ASSUMEROLE_ROLEARN_TAG = "s3.assumerole.rolearn.tag"; diff --git a/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java b/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java index 7b26d910..1d3b0ed0 100644 --- a/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java +++ b/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java @@ -4,15 +4,16 @@ package cloud.katta.protocols.s3; +import ch.cyberduck.core.Credentials; import ch.cyberduck.core.Host; import ch.cyberduck.core.LoginCallback; -import ch.cyberduck.core.OAuthTokens; import ch.cyberduck.core.Profile; import ch.cyberduck.core.TemporaryAccessTokens; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.oauth.OAuth2RequestInterceptor; import ch.cyberduck.core.preferences.HostPreferencesFactory; import ch.cyberduck.core.preferences.PreferencesReader; +import ch.cyberduck.core.preferences.ProxyPreferencesReader; import ch.cyberduck.core.ssl.X509KeyManager; import ch.cyberduck.core.ssl.X509TrustManager; import ch.cyberduck.core.sts.STSAssumeRoleWithWebIdentityRequestInterceptor; @@ -21,24 +22,12 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import cloud.katta.client.ApiException; -import cloud.katta.client.api.StorageResourceApi; -import cloud.katta.client.model.AccessTokenResponse; -import cloud.katta.protocols.hub.HubSession; -import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService; - /** * Assume role with temporary credentials obtained using OIDC token from security token service (STS) */ public class STSChainedAssumeRoleRequestInterceptor extends STSAssumeRoleWithWebIdentityRequestInterceptor { private static final Logger log = LogManager.getLogger(STSChainedAssumeRoleRequestInterceptor.class); - /** - * The party to which the ID Token was issued - * ... - */ - private static final String OIDC_AUTHORIZED_PARTY = "azp"; - private final Host bookmark; public STSChainedAssumeRoleRequestInterceptor(final OAuth2RequestInterceptor oauth, final Host host, @@ -51,7 +40,7 @@ public STSChainedAssumeRoleRequestInterceptor(final OAuth2RequestInterceptor oau /** * Assume role with previously obtained temporary access token * - * @param oauth OIDC tokens + * @param credentials Session credentials * @return Temporary scoped access tokens * @throws ch.cyberduck.core.exception.ExpiredTokenException Expired identity * @throws ch.cyberduck.core.exception.LoginFailureException Authorization failure @@ -59,46 +48,18 @@ public STSChainedAssumeRoleRequestInterceptor(final OAuth2RequestInterceptor oau * @see S3AssumeRoleProtocol#S3_ASSUMEROLE_ROLEARN_CREATE_BUCKET */ @Override - public TemporaryAccessTokens assumeRoleWithWebIdentity(final OAuthTokens oauth, final String roleArn) throws BackgroundException { - final PreferencesReader settings = HostPreferencesFactory.get(bookmark); - final TemporaryAccessTokens tokens = super.assumeRoleWithWebIdentity(this.tokenExchange(oauth), settings.getProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN)); + public TemporaryAccessTokens assumeRoleWithWebIdentity(final Credentials credentials) throws BackgroundException { + final PreferencesReader settings = new ProxyPreferencesReader(bookmark, credentials); + final TemporaryAccessTokens tokens = super.assumeRoleWithWebIdentity(credentials + .withProperty(Profile.STS_ROLE_ARN_PROPERTY_KEY, settings.getProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_WEBIDENTITY))); if(StringUtils.isNotBlank(settings.getProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_TAG))) { log.debug("Assume role with temporary credentials {}", tokens); // Assume role with previously obtained temporary access token - return super.assumeRole(credentials.setTokens(tokens) - .setProperty(Profile.STS_TAGS_PROPERTY_KEY, String.format("%s=%s", HostPreferencesFactory.get(bookmark).getProperty("s3.assumerole.rolearn.tag.vaultid.key"), - settings.getProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_VAULT))), - settings.getProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_TAG)); - } - log.warn("No vault tag set. Skip assuming role with temporary credentials {} for {}", tokens, bookmark); - return tokens; - } - - /** - * Perform OAuth 2.0 Token Exchange - * - * @return New tokens - * @see S3AssumeRoleProtocol#OAUTH_TOKENEXCHANGE_VAULT - */ - private OAuthTokens tokenExchange(final OAuthTokens tokens) throws BackgroundException { - final PreferencesReader settings = HostPreferencesFactory.get(bookmark); - if(settings.getBoolean(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE)) { - log.info("Exchange tokens for {}", bookmark); - final HubSession hub = bookmark.getProtocol().getFeature(HubSession.class); - log.debug("Exchange token with hub {}", hub); - final StorageResourceApi api = new StorageResourceApi(hub.getClient()); - try { - final AccessTokenResponse tokenExchangeResponse = api.apiStorageS3TokenPost(settings.getProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_VAULT)); - // N.B. token exchange with Id token does not work! - final OAuthTokens exchanged = new OAuthTokens(tokenExchangeResponse.getAccessToken(), - tokenExchangeResponse.getRefreshToken(), - tokenExchangeResponse.getExpiresIn() != null ? System.currentTimeMillis() + tokenExchangeResponse.getExpiresIn() * 1000 : null); - log.debug("Received exchanged token {} for {}", exchanged, bookmark); - return exchanged; - } - catch(ApiException e) { - throw new HubExceptionMappingService().map(e); - } + return super.assumeRole(credentials.withTokens(tokens) + .withProperty(Profile.STS_ROLE_ARN_PROPERTY_KEY, settings.getProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_TAG)) + .withProperty(Profile.STS_TAGS_PROPERTY_KEY, String.format("%s=%s", HostPreferencesFactory.get(bookmark).getProperty("s3.assumerole.rolearn.tag.vaultid.key"), + settings.getProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_VAULT))) + ); } return tokens; } From f3d5159b8713efcb0f81876f32c6b1c331bcc678 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 14 Oct 2025 19:26:41 +0200 Subject: [PATCH 096/133] Add support to create vault from client. --- .../katta/protocols/hub/HubUVFVault.java | 168 ++++++++---------- .../protocols/hub/HubVaultListService.java | 76 ++++---- .../core/AbstractHubSynchronizeTest.java | 25 ++- .../workflows/AbstractHubWorkflowTest.java | 28 ++- 4 files changed, 149 insertions(+), 148 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 79cb7b32..d6cf3f58 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -4,28 +4,17 @@ package cloud.katta.protocols.hub; -import ch.cyberduck.core.Credentials; -import ch.cyberduck.core.CredentialsConfigurator; -import ch.cyberduck.core.DisabledCancelCallback; -import ch.cyberduck.core.DisabledHostKeyCallback; -import ch.cyberduck.core.DisabledLoginCallback; -import ch.cyberduck.core.Host; -import ch.cyberduck.core.HostUrlProvider; -import ch.cyberduck.core.PasswordCallback; -import ch.cyberduck.core.Path; -import ch.cyberduck.core.PathAttributes; -import ch.cyberduck.core.Protocol; -import ch.cyberduck.core.ProtocolFactory; -import ch.cyberduck.core.Session; -import ch.cyberduck.core.SessionFactory; +import ch.cyberduck.core.*; import ch.cyberduck.core.cryptomator.ContentWriter; import ch.cyberduck.core.cryptomator.UVFVault; import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.exception.ConnectionCanceledException; import ch.cyberduck.core.exception.InteroperabilityException; import ch.cyberduck.core.features.AttributesFinder; import ch.cyberduck.core.features.Directory; import ch.cyberduck.core.features.Write; import ch.cyberduck.core.preferences.PreferencesFactory; +import ch.cyberduck.core.preferences.ProxyPreferencesReader; import ch.cyberduck.core.proxy.ProxyFactory; import ch.cyberduck.core.s3.S3Session; import ch.cyberduck.core.ssl.X509KeyManager; @@ -34,7 +23,6 @@ import ch.cyberduck.core.vault.VaultCredentials; import ch.cyberduck.core.vault.VaultUnlockCancelException; -import org.apache.http.HttpStatus; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.joda.time.DateTime; @@ -45,22 +33,16 @@ import java.util.UUID; import cloud.katta.client.ApiException; -import cloud.katta.client.api.UsersResourceApi; import cloud.katta.client.api.VaultResourceApi; import cloud.katta.client.model.UserDto; import cloud.katta.client.model.VaultDto; -import cloud.katta.crypto.DeviceKeys; import cloud.katta.crypto.UserKeys; import cloud.katta.crypto.uvf.UvfMetadataPayload; import cloud.katta.crypto.uvf.UvfMetadataPayloadPasswordCallback; import cloud.katta.crypto.uvf.VaultMetadataJWEAutomaticAccessGrantDto; import cloud.katta.crypto.uvf.VaultMetadataJWEBackendDto; import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService; -import cloud.katta.workflows.DeviceKeysServiceImpl; -import cloud.katta.workflows.UserKeysServiceImpl; -import cloud.katta.workflows.VaultServiceImpl; -import cloud.katta.workflows.exceptions.AccessException; -import cloud.katta.workflows.exceptions.SecurityFailure; +import cloud.katta.protocols.s3.S3AssumeRoleProtocol; import com.fasterxml.jackson.core.JsonProcessingException; import com.nimbusds.jose.JOSEException; @@ -73,28 +55,76 @@ public class HubUVFVault extends UVFVault { private static final Logger log = LogManager.getLogger(HubUVFVault.class); private final UUID vaultId; + private final UvfMetadataPayload vaultMetadata; /** * Storage connection only available after loading vault */ - private Session storage; + private final Session storage; + private final Path home; - private final Credentials credentials; + + public HubUVFVault(final HubSession hub, final Path bucket, final HubStorageLocationService.StorageLocation location) throws ConnectionCanceledException { + this(hub, UUID.fromString(new UUIDRandomStringService().random()), bucket, location); + } + + /** + * Constructor for factory creating new vault + * + * @param bucket Bucket + */ + public HubUVFVault(final HubSession hub, final UUID vaultId, final Path bucket, final HubStorageLocationService.StorageLocation location) throws ConnectionCanceledException { + this(hub, vaultId, bucket, + UvfMetadataPayload.create() + .withStorage(new VaultMetadataJWEBackendDto() + .provider(location.getProfile()) + .defaultPath(bucket.getAbsolute()) + .region(location.getRegion()) + .nickname(null != bucket.attributes().getDisplayname() ? bucket.attributes().getDisplayname() : "Vault")) + .withAutomaticAccessGrant(new VaultMetadataJWEAutomaticAccessGrantDto() + .enabled(true) + .maxWotDepth(null))); + } /** * Open from existing metadata * - * @param vaultId Vault ID Used to lookup profile - * @param bucket Bucket name - * @param credentials Storage access credentials + * @param vaultId Vault ID Used to lookup profile */ - public HubUVFVault(final UUID vaultId, final Path bucket, final Credentials credentials) { + public HubUVFVault(final HubSession hub, final UUID vaultId, final UvfMetadataPayload vaultMetadata) throws ConnectionCanceledException { + this(hub, vaultId, new Path(vaultMetadata.storage().getDefaultPath(), EnumSet.of(Path.Type.directory, Path.Type.volume), + new PathAttributes().setDisplayname(vaultMetadata.storage().getNickname())), vaultMetadata); + } + + public HubUVFVault(final HubSession hub, final UUID vaultId, final Path bucket, final UvfMetadataPayload vaultMetadata) throws ConnectionCanceledException { super(bucket); this.vaultId = vaultId; + this.vaultMetadata = vaultMetadata; this.home = bucket; - this.credentials = credentials; + + final VaultMetadataJWEBackendDto vaultStorageMetadata = vaultMetadata.storage(); + final Protocol profile = ProtocolFactory.get().forName(vaultStorageMetadata.getProvider()); + log.debug("Loaded profile {} for UVF metadata {}", profile, vaultMetadata); + final Credentials credentials = + hub.getFeature(CredentialsConfigurator.class).reload().configure(hub.getHost()); + log.debug("Copy credentials {}", credentials); + if(vaultStorageMetadata.getUsername() != null) { + credentials.setUsername(vaultStorageMetadata.getUsername()); + } + if(vaultStorageMetadata.getPassword() != null) { + credentials.setPassword(vaultStorageMetadata.getPassword()); + } + final Host storageProvider = new Host(profile, credentials); + storageProvider.setProperty(OAUTH_TOKENEXCHANGE_VAULT, vaultId.toString()); + storageProvider.setRegion(vaultStorageMetadata.getRegion()); + log.debug("Configured {} for vault {}", storageProvider, this); + this.storage = SessionFactory.create(storageProvider, hub.getFeature(X509TrustManager.class), hub.getFeature(X509KeyManager.class)); } + /** + * + * @return Storage provider configuration + */ public Session getStorage() { return storage; } @@ -126,32 +156,21 @@ public synchronized void close() { @Override public Path create(final Session session, final String region, final VaultCredentials noop) throws BackgroundException { try { - final HubStorageLocationService.StorageLocation location = HubStorageLocationService.StorageLocation.fromIdentifier(region); - final String storageProfileId = location.getProfile(); - final UvfMetadataPayload metadataPayload = UvfMetadataPayload.create() - .withStorage(new VaultMetadataJWEBackendDto() - .provider(storageProfileId) - .defaultPath(home.getAbsolute()) - .region(location.getRegion()) - .nickname(null != home.attributes().getDisplayname() ? home.attributes().getDisplayname() : "Vault")) - .withAutomaticAccessGrant(new VaultMetadataJWEAutomaticAccessGrantDto() - .enabled(true) - .maxWotDepth(null)); - log.debug("Created metadata JWE {}", metadataPayload); + final HubSession hub = HubSession.coerce(session); + log.debug("Created metadata JWE {}", vaultMetadata); final UvfMetadataPayload.UniversalVaultFormatJWKS jwks = UvfMetadataPayload.createKeys(); final VaultDto vaultDto = new VaultDto() .id(vaultId) - .name(metadataPayload.storage().getNickname()) + .name(vaultMetadata.storage().getNickname()) .description(null) .archived(false) .creationTime(DateTime.now()) - .uvfMetadataFile(metadataPayload.encrypt( + .uvfMetadataFile(vaultMetadata.encrypt( String.format("%s/api", new HostUrlProvider(false, true).get(session.getHost())), vaultId, jwks.toJWKSet() )) .uvfKeySet(jwks.serializePublicRecoverykey()); - final HubSession hub = (HubSession) session; // Create vault in Hub final VaultResourceApi vaultResourceApi = new VaultResourceApi(hub.getClient()); log.debug("Create vault {}", vaultDto); @@ -165,20 +184,19 @@ public Path create(final Session session, final String region, final VaultCre vaultResourceApi.apiVaultsVaultIdAccessTokensPost(vaultDto.getId(), Collections.singletonMap(userDto.getId(), jwks.toOwnerAccessToken().encryptForUser(userKeys.ecdhKeyPair().getPublic()))); // Upload vault template to storage - final Protocol profile = ProtocolFactory.get().forName(storageProfileId); - log.debug("Loaded profile {} for vault {}", profile, this); - final Host bookmark = new Host(profile, credentials); - bookmark.setProperty(OAUTH_TOKENEXCHANGE_VAULT, vaultId.toString()); - bookmark.setRegion(location.getRegion()); - bookmark.setDefaultPath(home.getAbsolute()); - log.debug("Configured {} for vault {}", bookmark, this); - storage = SessionFactory.create(bookmark, session.getFeature(X509TrustManager.class), session.getFeature(X509KeyManager.class)); log.debug("Connect to {}", storage); + final Host configuration = storage.getHost(); + // No token exchange with Katta Server + configuration.setProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE, null); + // Assume role with policy attached to create vault + configuration.setProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_WEBIDENTITY, + new ProxyPreferencesReader(storage.getHost()).getProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_CREATE_BUCKET)); + // No role chaining when creating vault + configuration.setProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_TAG, null); storage.open(ProxyFactory.get(), new DisabledHostKeyCallback(), new DisabledLoginCallback(), new DisabledCancelCallback()); - storage.login(new DisabledLoginCallback(), new DisabledCancelCallback()); - log.debug("Upload vault template to {}", storage); final Path vault; if(false) { + log.debug("Upload vault template to {}", storage); return super.create(storage, HubStorageLocationService.StorageLocation.fromIdentifier(region).getRegion(), noop); } @@ -188,7 +206,7 @@ public Path create(final Session session, final String region, final VaultCre final TransferStatus status = (new TransferStatus()).setRegion(HubStorageLocationService.StorageLocation.fromIdentifier(region).getRegion()); vault = directory.mkdir(storage._getFeature(Write.class), home, status); - final String hashedRootDirId = metadataPayload.computeRootDirIdHash(); + final String hashedRootDirId = vaultMetadata.computeRootDirIdHash(); final Path dataDir = new Path(vault, "d", EnumSet.of(Path.Type.directory)); final Path firstLevel = new Path(dataDir, hashedRootDirId.substring(0, 2), EnumSet.of(Path.Type.directory)); final Path secondLevel = new Path(firstLevel, hashedRootDirId.substring(2), EnumSet.of(Path.Type.directory)); @@ -202,11 +220,11 @@ public Path create(final Session session, final String region, final VaultCre EnumSet.of(Path.Type.file, Path.Type.vault)), vaultDto.getUvfMetadataFile().getBytes(StandardCharsets.US_ASCII)); // dir.uvf new ContentWriter(storage).write(new Path(secondLevel, "dir.uvf", EnumSet.of(Path.Type.file)), - metadataPayload.computeRootDirUvf()); + vaultMetadata.computeRootDirUvf()); } return vault; } - catch(JOSEException | JsonProcessingException | AccessException | SecurityFailure e) { + catch(JOSEException | JsonProcessingException e) { throw new InteroperabilityException(e.getMessage(), e); } catch(ApiException e) { @@ -223,35 +241,9 @@ public Path create(final Session session, final String region, final VaultCre @Override public HubUVFVault load(final Session session, final PasswordCallback prompt) throws BackgroundException { try { - final HubSession hub = HubSession.coerce(session); - // Find storage configuration in vault metadata - final VaultServiceImpl vaultService = new VaultServiceImpl(hub); - final UvfMetadataPayload vaultMetadata = vaultService.getVaultMetadataJWE(vaultId, hub.getUserKeys()); - final Protocol profile = ProtocolFactory.get().forName(vaultMetadata.storage().getProvider()); - log.debug("Loaded profile {} for vault {}", profile, this); - final Credentials credentials = - session.getFeature(CredentialsConfigurator.class).reload().configure(session.getHost()); - log.debug("Copy credentials {}", credentials); - final VaultMetadataJWEBackendDto vaultStorageMetadata = vaultMetadata.storage(); - if(vaultStorageMetadata.getUsername() != null) { - credentials.setUsername(vaultStorageMetadata.getUsername()); - } - if(vaultStorageMetadata.getPassword() != null) { - credentials.setPassword(vaultStorageMetadata.getPassword()); - } - final Host bookmark = new Host(profile, credentials); - log.debug("Configure bookmark for vault {}", vaultStorageMetadata); - bookmark.setNickname(vaultStorageMetadata.getNickname()); - bookmark.setDefaultPath(vaultStorageMetadata.getDefaultPath()); - bookmark.setProperty(OAUTH_TOKENEXCHANGE_VAULT, vaultId.toString()); - // region as chosen by user upon vault creation (STS) or as retrieved from bucket (permanent) - bookmark.setRegion(vaultStorageMetadata.getRegion()); - log.debug("Configured {} for vault {}", bookmark, this); - storage = SessionFactory.create(bookmark, session.getFeature(X509TrustManager.class), session.getFeature(X509KeyManager.class)); log.debug("Connect to {}", storage); try { storage.open(ProxyFactory.get(), new DisabledHostKeyCallback(), new DisabledLoginCallback(), new DisabledCancelCallback()); - storage.login(new DisabledLoginCallback(), new DisabledCancelCallback()); } catch(BackgroundException e) { log.warn("Skip loading vault with failure {} connecting to storage", e.toString()); @@ -265,13 +257,7 @@ public HubUVFVault load(final Session session, final PasswordCallback prompt) super.load(storage, new UvfMetadataPayloadPasswordCallback(vaultMetadata.toJSON())); return this; } - catch(ApiException e) { - if(HttpStatus.SC_FORBIDDEN == e.getCode()) { - throw new VaultUnlockCancelException(this, e); - } - throw new HubExceptionMappingService().map(e); - } - catch(JsonProcessingException | SecurityFailure | AccessException e) { + catch(JsonProcessingException e) { throw new InteroperabilityException(e.getMessage(), e); } } @@ -280,8 +266,8 @@ public HubUVFVault load(final Session session, final PasswordCallback prompt) public String toString() { final StringBuilder sb = new StringBuilder("HubUVFVault{"); sb.append("vaultId=").append(vaultId); + sb.append(", vaultMetadata=").append(vaultMetadata); sb.append(", home=").append(home); - sb.append(", credentials=").append(credentials); sb.append('}'); return sb.toString(); } diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java index d43accb9..73a149cf 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java @@ -5,13 +5,11 @@ package cloud.katta.protocols.hub; import ch.cyberduck.core.AttributedList; -import ch.cyberduck.core.Credentials; import ch.cyberduck.core.DisabledPasswordCallback; import ch.cyberduck.core.ListProgressListener; import ch.cyberduck.core.ListService; import ch.cyberduck.core.LocaleFactory; import ch.cyberduck.core.Path; -import ch.cyberduck.core.PathAttributes; import ch.cyberduck.core.exception.AccessDeniedException; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.exception.NotfoundException; @@ -23,13 +21,11 @@ import org.apache.logging.log4j.Logger; import java.text.MessageFormat; -import java.util.EnumSet; import cloud.katta.client.ApiException; import cloud.katta.client.api.VaultResourceApi; import cloud.katta.client.model.VaultDto; import cloud.katta.crypto.uvf.UvfMetadataPayload; -import cloud.katta.crypto.uvf.VaultMetadataJWEBackendDto; import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService; import cloud.katta.workflows.VaultServiceImpl; import cloud.katta.workflows.exceptions.AccessException; @@ -47,50 +43,46 @@ public HubVaultListService(final HubSession session) { @Override public AttributedList list(final Path directory, final ListProgressListener listener) throws BackgroundException { if(directory.isRoot()) { - try { - final VaultRegistry registry = session.getRegistry(); - final AttributedList vaults = new AttributedList<>(); - for(final VaultDto vaultDto : new VaultResourceApi(session.getClient()).apiVaultsAccessibleGet(null)) { - if(Boolean.TRUE.equals(vaultDto.getArchived())) { - log.debug("Skip archived vault {}", vaultDto); - continue; - } - log.debug("Load vault {}", vaultDto); - try { - // Find storage configuration in vault metadata - final VaultServiceImpl vaultService = new VaultServiceImpl(session); - final UvfMetadataPayload vaultMetadata = vaultService.getVaultMetadataJWE(vaultDto.getId(), session.getUserKeys()); - final VaultMetadataJWEBackendDto storage = vaultMetadata.storage(); - final HubUVFVault vault = new HubUVFVault(vaultDto.getId(), - new Path(storage.getDefaultPath(), EnumSet.of(Path.Type.directory, Path.Type.volume), - new PathAttributes().setDisplayname(storage.getNickname())), - new Credentials(storage.getUsername(), storage.getPassword())); + try { + final VaultRegistry registry = session.getRegistry(); + final AttributedList vaults = new AttributedList<>(); + for(final VaultDto vaultDto : new VaultResourceApi(session.getClient()).apiVaultsAccessibleGet(null)) { + if(Boolean.TRUE.equals(vaultDto.getArchived())) { + log.debug("Skip archived vault {}", vaultDto); + continue; + } + log.debug("Load vault {}", vaultDto); try { - registry.add(vault.load(session, new DisabledPasswordCallback())); - vaults.add(vault.getHome()); - listener.chunk(directory, vaults); + // Find storage configuration in vault metadata + final VaultServiceImpl vaultService = new VaultServiceImpl(session); + final UvfMetadataPayload vaultMetadata = vaultService.getVaultMetadataJWE(vaultDto.getId(), session.getUserKeys()); + final HubUVFVault vault = new HubUVFVault(session, vaultDto.getId(), vaultMetadata); + try { + registry.add(vault.load(session, new DisabledPasswordCallback())); + vaults.add(vault.getHome()); + listener.chunk(directory, vaults); + } + catch(VaultUnlockCancelException e) { + log.warn("Skip vault {} with failure {} loading", vaultDto, e); + continue; + } } - catch(VaultUnlockCancelException e) { - log.warn("Skip vault {} with failure {} loading", vaultDto, e); - continue; + catch(ApiException e) { + if(HttpStatus.SC_FORBIDDEN == e.getCode()) { + log.warn("Skip vault {} with insufficient permissions {}", vaultDto, e); + continue; + } + throw e; } - } - catch(ApiException e) { - if(HttpStatus.SC_FORBIDDEN == e.getCode()) { - log.warn("Skip vault {} with insufficient permissions {}", vaultDto, e); - continue; + catch(AccessException | SecurityFailure e) { + throw new AccessDeniedException(e.getMessage(), e); } - throw e; - } - catch(AccessException | SecurityFailure e) { - throw new AccessDeniedException(e.getMessage(), e); } + return vaults; + } + catch(ApiException e) { + throw new HubExceptionMappingService().map("Listing directory {0} failed", e, directory); } - return vaults; - } - catch(ApiException e) { - throw new HubExceptionMappingService().map("Listing directory {0} failed", e, directory); - } } throw new NotfoundException(directory.getAbsolute()); } diff --git a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java index 646384e1..d71f9aa8 100644 --- a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java +++ b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java @@ -6,7 +6,6 @@ import ch.cyberduck.core.AlphanumericRandomStringService; import ch.cyberduck.core.AttributedList; -import ch.cyberduck.core.Credentials; import ch.cyberduck.core.DisabledConnectionCallback; import ch.cyberduck.core.DisabledListProgressListener; import ch.cyberduck.core.ListService; @@ -68,6 +67,9 @@ import cloud.katta.client.model.StorageProfileDto; import cloud.katta.client.model.StorageProfileS3Dto; import cloud.katta.client.model.StorageProfileS3STSDto; +import cloud.katta.crypto.uvf.UvfMetadataPayload; +import cloud.katta.crypto.uvf.VaultMetadataJWEAutomaticAccessGrantDto; +import cloud.katta.crypto.uvf.VaultMetadataJWEBackendDto; import cloud.katta.model.StorageProfileDtoWrapper; import cloud.katta.protocols.hub.HubSession; import cloud.katta.protocols.hub.HubStorageLocationService; @@ -230,14 +232,25 @@ public void test03AddVault(final HubTestConfig config) throws Exception { .filter(p -> p.getId().toString().equals(config.vault.storageProfileId.toLowerCase())).findFirst().get(); log.info("Creating vault in {}", hubSession); - final UUID vaultId = UUID.randomUUID(); + final UUID vaultId = UUID.fromString(new UUIDRandomStringService().random()); final Path bucket = new Path(storageProfileWrapper.getProtocol() == Protocol.S3_STS ? storageProfileWrapper.getBucketPrefix() + vaultId : config.vault.bucketName, EnumSet.of(Path.Type.volume, Path.Type.directory)); - final HubUVFVault cryptomator = new HubUVFVault(UUID.fromString(new UUIDRandomStringService().random()), bucket, - new Credentials(config.vault.username, config.vault.password)); - cryptomator.create(hubSession, new HubStorageLocationService.StorageLocation(storageProfileWrapper.getId().toString(), storageProfileWrapper.getRegion(), - storageProfileWrapper.getName()).getIdentifier(), new VaultCredentials(StringUtils.EMPTY)); + final HubStorageLocationService.StorageLocation location = new HubStorageLocationService.StorageLocation(storageProfileWrapper.getId().toString(), storageProfileWrapper.getRegion(), + storageProfileWrapper.getName()); + final UvfMetadataPayload vaultMetadata = UvfMetadataPayload.create() + .withStorage(new VaultMetadataJWEBackendDto() + .username(config.vault.username) + .password(config.vault.password) + .provider(location.getProfile()) + .defaultPath(bucket.getAbsolute()) + .region(location.getRegion()) + .nickname(null != bucket.attributes().getDisplayname() ? bucket.attributes().getDisplayname() : "Vault")) + .withAutomaticAccessGrant(new VaultMetadataJWEAutomaticAccessGrantDto() + .enabled(true) + .maxWotDepth(null)); + final HubUVFVault cryptomator = new HubUVFVault(hubSession, vaultId, bucket, vaultMetadata); + cryptomator.create(hubSession, location.getIdentifier(), new VaultCredentials(StringUtils.EMPTY)); final AttributedList vaults = hubSession.getFeature(ListService.class).list(Home.root(), new DisabledListProgressListener()); assertFalse(vaults.isEmpty()); diff --git a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java index a970541d..19b2500c 100644 --- a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java +++ b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java @@ -4,7 +4,6 @@ package cloud.katta.workflows; -import ch.cyberduck.core.Credentials; import ch.cyberduck.core.Path; import ch.cyberduck.core.UUIDRandomStringService; import ch.cyberduck.core.vault.VaultCredentials; @@ -31,6 +30,9 @@ import cloud.katta.client.model.UserDto; import cloud.katta.client.model.VaultDto; import cloud.katta.crypto.UserKeys; +import cloud.katta.crypto.uvf.UvfMetadataPayload; +import cloud.katta.crypto.uvf.VaultMetadataJWEAutomaticAccessGrantDto; +import cloud.katta.crypto.uvf.VaultMetadataJWEBackendDto; import cloud.katta.model.SetupCodeJWE; import cloud.katta.model.StorageProfileDtoWrapper; import cloud.katta.protocols.hub.HubSession; @@ -61,16 +63,24 @@ public void testHubWorkflow(final HubTestConfig config) throws Exception { .map(StorageProfileDtoWrapper::coerce) .filter(p -> p.getId().toString().equals(config.vault.storageProfileId.toLowerCase())).findFirst().get(); - final UUID vaultId = UUID.randomUUID(); - // upload template (STS: create bucket first, static: existing bucket) - // TODO test with multiple wot levels? - + final UUID vaultId = UUID.fromString(new UUIDRandomStringService().random()); final Path bucket = new Path(storageProfileWrapper.getProtocol() == Protocol.S3_STS ? storageProfileWrapper.getBucketPrefix() + vaultId : config.vault.bucketName, EnumSet.of(Path.Type.volume, Path.Type.directory)); - final HubUVFVault cryptomator = new HubUVFVault(UUID.fromString(new UUIDRandomStringService().random()), bucket, - new Credentials(config.vault.username, config.vault.password)); - cryptomator.create(hubSession, new HubStorageLocationService.StorageLocation(storageProfileWrapper.getId().toString(), storageProfileWrapper.getRegion(), - storageProfileWrapper.getName()).getIdentifier(), new VaultCredentials(StringUtils.EMPTY)); + final HubStorageLocationService.StorageLocation location = new HubStorageLocationService.StorageLocation(storageProfileWrapper.getId().toString(), storageProfileWrapper.getRegion(), + storageProfileWrapper.getName()); + final UvfMetadataPayload vaultMetadata = UvfMetadataPayload.create() + .withStorage(new VaultMetadataJWEBackendDto() + .username(config.vault.username) + .password(config.vault.password) + .provider(location.getProfile()) + .defaultPath(bucket.getAbsolute()) + .region(location.getRegion()) + .nickname(null != bucket.attributes().getDisplayname() ? bucket.attributes().getDisplayname() : "Vault")) + .withAutomaticAccessGrant(new VaultMetadataJWEAutomaticAccessGrantDto() + .enabled(true) + .maxWotDepth(3)); + final HubUVFVault cryptomator = new HubUVFVault(hubSession, vaultId, bucket, vaultMetadata); + cryptomator.create(hubSession, location.getIdentifier(), new VaultCredentials(StringUtils.EMPTY)); checkNumberOfVaults(hubSession, config, vaultId, 0, 0, 1, 0, 0); From 35ae8ee3bd5395ca4c0ad77734f74549f7494ec3 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 14 Oct 2025 21:39:19 +0200 Subject: [PATCH 097/133] Use provided bucket name when available. --- .../test/java/cloud/katta/core/AbstractHubSynchronizeTest.java | 3 +-- .../java/cloud/katta/workflows/AbstractHubWorkflowTest.java | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java index d71f9aa8..671c1a4f 100644 --- a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java +++ b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java @@ -61,7 +61,6 @@ import cloud.katta.client.api.StorageProfileResourceApi; import cloud.katta.client.api.UsersResourceApi; import cloud.katta.client.model.ConfigDto; -import cloud.katta.client.model.Protocol; import cloud.katta.client.model.S3SERVERSIDEENCRYPTION; import cloud.katta.client.model.S3STORAGECLASSES; import cloud.katta.client.model.StorageProfileDto; @@ -234,7 +233,7 @@ public void test03AddVault(final HubTestConfig config) throws Exception { log.info("Creating vault in {}", hubSession); final UUID vaultId = UUID.fromString(new UUIDRandomStringService().random()); - final Path bucket = new Path(storageProfileWrapper.getProtocol() == Protocol.S3_STS ? storageProfileWrapper.getBucketPrefix() + vaultId : config.vault.bucketName, + final Path bucket = new Path(null == config.vault.bucketName ? storageProfileWrapper.getBucketPrefix() + vaultId : config.vault.bucketName, EnumSet.of(Path.Type.volume, Path.Type.directory)); final HubStorageLocationService.StorageLocation location = new HubStorageLocationService.StorageLocation(storageProfileWrapper.getId().toString(), storageProfileWrapper.getRegion(), storageProfileWrapper.getName()); diff --git a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java index 19b2500c..31c9caac 100644 --- a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java +++ b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java @@ -24,7 +24,6 @@ import cloud.katta.client.api.UsersResourceApi; import cloud.katta.client.api.VaultResourceApi; import cloud.katta.client.model.MemberDto; -import cloud.katta.client.model.Protocol; import cloud.katta.client.model.Role; import cloud.katta.client.model.StorageProfileDto; import cloud.katta.client.model.UserDto; @@ -64,7 +63,7 @@ public void testHubWorkflow(final HubTestConfig config) throws Exception { .filter(p -> p.getId().toString().equals(config.vault.storageProfileId.toLowerCase())).findFirst().get(); final UUID vaultId = UUID.fromString(new UUIDRandomStringService().random()); - final Path bucket = new Path(storageProfileWrapper.getProtocol() == Protocol.S3_STS ? storageProfileWrapper.getBucketPrefix() + vaultId : config.vault.bucketName, + final Path bucket = new Path(null == config.vault.bucketName ? storageProfileWrapper.getBucketPrefix() + vaultId : config.vault.bucketName, EnumSet.of(Path.Type.volume, Path.Type.directory)); final HubStorageLocationService.StorageLocation location = new HubStorageLocationService.StorageLocation(storageProfileWrapper.getId().toString(), storageProfileWrapper.getRegion(), storageProfileWrapper.getName()); From 44c93270d929c7e0716829804e8bb83c51039813 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 14 Oct 2025 22:06:04 +0200 Subject: [PATCH 098/133] Handle null. --- .../cloud/katta/protocols/hub/HubStorageLocationService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java index aff6d191..f1478f16 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java @@ -7,6 +7,7 @@ import ch.cyberduck.core.Path; import ch.cyberduck.core.features.Location; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -74,7 +75,7 @@ public static final class StorageLocation extends Name { * @param storageProfileName Description */ public StorageLocation(final String storageProfileId, final String region, final String storageProfileName) { - super(String.format("%s,%s", storageProfileId, region)); + super(String.format("%s,%s", storageProfileId, null == region ? StringUtils.EMPTY : region)); this.storageProfileId = storageProfileId; this.region = region; this.storageProfileName = storageProfileName; @@ -108,7 +109,7 @@ public static StorageLocation fromIdentifier(final String identifier) { if(parts.length != 2) { return new StorageLocation(identifier, null, null); } - return new StorageLocation(parts[0], parts[1], null); + return new StorageLocation(StringUtils.isBlank(parts[0]) ? null : parts[0], StringUtils.isBlank(parts[1]) ? null : parts[1], null); } } } From 6a2ba6284de4eb50100367c8e4278e85fa26f9cd Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 14 Oct 2025 22:37:17 +0200 Subject: [PATCH 099/133] Create random bucket in tests. --- .../java/cloud/katta/core/AbstractHubSynchronizeTest.java | 5 +---- hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java index 671c1a4f..fcb6f0f2 100644 --- a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java +++ b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java @@ -57,10 +57,8 @@ import cloud.katta.client.ApiClient; import cloud.katta.client.ApiException; -import cloud.katta.client.api.ConfigResourceApi; import cloud.katta.client.api.StorageProfileResourceApi; import cloud.katta.client.api.UsersResourceApi; -import cloud.katta.client.model.ConfigDto; import cloud.katta.client.model.S3SERVERSIDEENCRYPTION; import cloud.katta.client.model.S3STORAGECLASSES; import cloud.katta.client.model.StorageProfileDto; @@ -337,8 +335,7 @@ public void test04SetupCode(final HubTestConfig config) throws Exception { assertEquals(StringUtils.EMPTY, hubSession.getHost().getCredentials().getPassword()); final ListService feature = hubSession.getFeature(ListService.class); final AttributedList vaults = feature.list(Home.root(), new DisabledListProgressListener()); - final ConfigDto configDto = new ConfigResourceApi(hubSession.getClient()).apiConfigGet(); - final int expectedNumberOfVaults = configDto.getKeycloakTokenEndpoint().contains("localhost") ? 2 : 4; + final int expectedNumberOfVaults = 4; assertEquals(expectedNumberOfVaults, vaults.size()); assertEquals(vaults, feature.list(Home.root(), new DisabledListProgressListener())); for(final Path vault : vaults) { diff --git a/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java b/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java index bce083e8..74a62160 100644 --- a/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java +++ b/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java @@ -48,11 +48,11 @@ public abstract class AbstractHubTest extends VaultTest { private static final HubTestConfig.VaultSpec minioSTSVaultConfig = new HubTestConfig.VaultSpec("MinIO STS", "732D43FA-3716-46C4-B931-66EA5405EF1C", null, null, null, "eu-central-1"); private static final HubTestConfig.VaultSpec minioStaticVaultConfig = new HubTestConfig.VaultSpec("MinIO static", "71B910E0-2ECC-46DE-A871-8DB28549677E", - "handmade", "minioadmin", "minioadmin", "us-east-1"); + null, "minioadmin", "minioadmin", "us-east-1"); private static final HubTestConfig.VaultSpec awsSTSVaultConfig = new HubTestConfig.VaultSpec("AWS STS", "844BD517-96D4-4787-BCFA-238E103149F6", null, null, null, "eu-west-1"); private static final HubTestConfig.VaultSpec awsStaticVaultConfig = new HubTestConfig.VaultSpec("AWS static", "72736C19-283C-49D3-80A5-AB74B5202543", - "handmade2", PROPERTIES.get("handmade2.s3.amazonaws.com.username"), PROPERTIES.get("handmade2.s3.amazonaws.com.password"), "eu-north-1" + null, PROPERTIES.get("handmade2.s3.amazonaws.com.username"), PROPERTIES.get("handmade2.s3.amazonaws.com.password"), "us-east-1" ); /** From e5db877739c4429435721411671b0553f6a7cbe5 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 14 Oct 2025 22:37:38 +0200 Subject: [PATCH 100/133] Workaround #189. --- .../test/java/cloud/katta/core/AbstractHubSynchronizeTest.java | 2 +- .../java/cloud/katta/workflows/AbstractHubWorkflowTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java index fcb6f0f2..0a915afa 100644 --- a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java +++ b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java @@ -231,7 +231,7 @@ public void test03AddVault(final HubTestConfig config) throws Exception { log.info("Creating vault in {}", hubSession); final UUID vaultId = UUID.fromString(new UUIDRandomStringService().random()); - final Path bucket = new Path(null == config.vault.bucketName ? storageProfileWrapper.getBucketPrefix() + vaultId : config.vault.bucketName, + final Path bucket = new Path(null == config.vault.bucketName ? null == storageProfileWrapper.getBucketPrefix() ? "katta-test-" + vaultId : storageProfileWrapper.getBucketPrefix() + vaultId : config.vault.bucketName, EnumSet.of(Path.Type.volume, Path.Type.directory)); final HubStorageLocationService.StorageLocation location = new HubStorageLocationService.StorageLocation(storageProfileWrapper.getId().toString(), storageProfileWrapper.getRegion(), storageProfileWrapper.getName()); diff --git a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java index 31c9caac..bfaa31b6 100644 --- a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java +++ b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java @@ -63,7 +63,7 @@ public void testHubWorkflow(final HubTestConfig config) throws Exception { .filter(p -> p.getId().toString().equals(config.vault.storageProfileId.toLowerCase())).findFirst().get(); final UUID vaultId = UUID.fromString(new UUIDRandomStringService().random()); - final Path bucket = new Path(null == config.vault.bucketName ? storageProfileWrapper.getBucketPrefix() + vaultId : config.vault.bucketName, + final Path bucket = new Path(null == config.vault.bucketName ? null == storageProfileWrapper.getBucketPrefix() ? "katta-test-" + vaultId : storageProfileWrapper.getBucketPrefix() + vaultId : config.vault.bucketName, EnumSet.of(Path.Type.volume, Path.Type.directory)); final HubStorageLocationService.StorageLocation location = new HubStorageLocationService.StorageLocation(storageProfileWrapper.getId().toString(), storageProfileWrapper.getRegion(), storageProfileWrapper.getName()); From 92cffe21975926aafd80599b0ca677e4f44b0cd7 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 14 Oct 2025 23:03:16 +0200 Subject: [PATCH 101/133] Delete unused. --- .../workflows/CachingUserKeysService.java | 47 -------------- .../katta/workflows/CachingWoTService.java | 51 ---------------- .../workflows/CachingUserKeysServiceTest.java | 32 ---------- .../workflows/CachingWoTServiceTest.java | 61 ------------------- 4 files changed, 191 deletions(-) delete mode 100644 hub/src/main/java/cloud/katta/workflows/CachingUserKeysService.java delete mode 100644 hub/src/main/java/cloud/katta/workflows/CachingWoTService.java delete mode 100644 hub/src/test/java/cloud/katta/workflows/CachingUserKeysServiceTest.java delete mode 100644 hub/src/test/java/cloud/katta/workflows/CachingWoTServiceTest.java diff --git a/hub/src/main/java/cloud/katta/workflows/CachingUserKeysService.java b/hub/src/main/java/cloud/katta/workflows/CachingUserKeysService.java deleted file mode 100644 index 0d149427..00000000 --- a/hub/src/main/java/cloud/katta/workflows/CachingUserKeysService.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2025 shift7 GmbH. All rights reserved. - */ - -package cloud.katta.workflows; - -import ch.cyberduck.core.Host; - -import cloud.katta.client.ApiException; -import cloud.katta.client.model.UserDto; -import cloud.katta.core.DeviceSetupCallback; -import cloud.katta.crypto.DeviceKeys; -import cloud.katta.crypto.UserKeys; -import cloud.katta.workflows.exceptions.AccessException; -import cloud.katta.workflows.exceptions.SecurityFailure; - -/** - * Retrieve user keys from hub upon first access and cache in memory during service's lifetime. - */ -public class CachingUserKeysService implements UserKeysService { - - private final UserKeysService proxy; - private UserKeys userKeys; - - public CachingUserKeysService(final UserKeysService proxy) { - this.proxy = proxy; - } - - /** - * Get user key from hub and decrypt with device-keys - */ - public UserKeys getUserKeys(final Host hub, final UserDto me, final DeviceKeys deviceKeyPair) throws ApiException, AccessException, SecurityFailure { - // Get user key from hub and decrypt with device-keys - if(userKeys == null) { - userKeys = proxy.getUserKeys(hub, me, deviceKeyPair); - } - return userKeys; - } - - @Override - public UserKeys getOrCreateUserKeys(final Host hub, final UserDto me, final DeviceKeys deviceKeyPair, final DeviceSetupCallback prompt) throws ApiException, AccessException, SecurityFailure { - if(userKeys == null) { - userKeys = proxy.getOrCreateUserKeys(hub, me, deviceKeyPair, prompt); - } - return userKeys; - } -} diff --git a/hub/src/main/java/cloud/katta/workflows/CachingWoTService.java b/hub/src/main/java/cloud/katta/workflows/CachingWoTService.java deleted file mode 100644 index cf901f90..00000000 --- a/hub/src/main/java/cloud/katta/workflows/CachingWoTService.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2025 shift7 GmbH. All rights reserved. - */ - -package cloud.katta.workflows; - -import java.text.ParseException; -import java.util.List; -import java.util.Map; - -import cloud.katta.client.ApiException; -import cloud.katta.client.model.TrustedUserDto; -import cloud.katta.client.model.UserDto; -import cloud.katta.crypto.UserKeys; -import cloud.katta.crypto.wot.SignedKeys; -import cloud.katta.workflows.exceptions.AccessException; -import cloud.katta.workflows.exceptions.SecurityFailure; -import com.nimbusds.jose.JOSEException; - -/** - * Retrieve verified trusted user from hub upon first access and cache afterwards. - * Counterpart of @see wot.ts. - */ -public class CachingWoTService implements WoTService { - - private final WoTService proxy; - - private Map trustLevels; - - public CachingWoTService(final WoTService proxy) { - this.proxy = proxy; - } - - @Override - public Map getTrustLevelsPerUserId(final UserKeys userKeys) throws ApiException, AccessException, SecurityFailure { - if(trustLevels == null) { - trustLevels = proxy.getTrustLevelsPerUserId(userKeys); - } - return trustLevels; - } - - @Override - public void verify(final UserKeys userKeys, final List signatureChain, final SignedKeys allegedSignedKey) throws ApiException, AccessException, SecurityFailure { - proxy.verify(userKeys, signatureChain, allegedSignedKey); - } - - @Override - public TrustedUserDto sign(final UserKeys userKeys, final UserDto user) throws ApiException, ParseException, JOSEException, AccessException, SecurityFailure { - return proxy.sign(userKeys, user); - } -} diff --git a/hub/src/test/java/cloud/katta/workflows/CachingUserKeysServiceTest.java b/hub/src/test/java/cloud/katta/workflows/CachingUserKeysServiceTest.java deleted file mode 100644 index bba88d19..00000000 --- a/hub/src/test/java/cloud/katta/workflows/CachingUserKeysServiceTest.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2025 shift7 GmbH. All rights reserved. - */ - -package cloud.katta.workflows; - -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import cloud.katta.client.ApiException; -import cloud.katta.crypto.UserKeys; -import cloud.katta.workflows.exceptions.AccessException; -import cloud.katta.workflows.exceptions.SecurityFailure; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.times; - -class CachingUserKeysServiceTest { - - @Test - void testGetUserKeys() throws AccessException, SecurityFailure, ApiException { - final UserKeysService proxyMock = Mockito.mock(UserKeysService.class); - final UserKeys userKeys = UserKeys.create(); - Mockito.when(proxyMock.getUserKeys(any(), any(), any())).thenReturn(userKeys); - final CachingUserKeysService service = new CachingUserKeysService(proxyMock); - assertEquals(userKeys, service.getUserKeys(null, null, null)); - Mockito.verify(proxyMock, times(1)).getUserKeys(any(), any(), any()); - assertEquals(userKeys, service.getUserKeys(null, null, null)); - Mockito.verify(proxyMock, times(1)).getUserKeys(any(), any(), any()); - } -} diff --git a/hub/src/test/java/cloud/katta/workflows/CachingWoTServiceTest.java b/hub/src/test/java/cloud/katta/workflows/CachingWoTServiceTest.java deleted file mode 100644 index 34d522f6..00000000 --- a/hub/src/test/java/cloud/katta/workflows/CachingWoTServiceTest.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2025 shift7 GmbH. All rights reserved. - */ - -package cloud.katta.workflows; - -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import java.text.ParseException; -import java.util.HashMap; - -import cloud.katta.client.ApiException; -import cloud.katta.client.model.TrustedUserDto; -import cloud.katta.workflows.exceptions.AccessException; -import cloud.katta.workflows.exceptions.SecurityFailure; -import com.nimbusds.jose.JOSEException; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.times; - -class CachingWoTServiceTest { - - @Test - void testGetTrustLevelsPerUserId() throws AccessException, SecurityFailure, ApiException { - final WoTService proxyMock = Mockito.mock(WoTService.class); - final HashMap trustLevels = new HashMap() {{ - put("alkdajf", 5); - put("lakdjfa", 42); - }}; - Mockito.when(proxyMock.getTrustLevelsPerUserId(any())).thenReturn(trustLevels); - final CachingWoTService service = new CachingWoTService(proxyMock); - assertEquals(trustLevels, service.getTrustLevelsPerUserId(null)); - Mockito.verify(proxyMock, times(1)).getTrustLevelsPerUserId(any()); - assertEquals(trustLevels, service.getTrustLevelsPerUserId(null)); - Mockito.verify(proxyMock, times(1)).getTrustLevelsPerUserId(any()); - } - - @Test - void testVerify() throws AccessException, SecurityFailure, ApiException { - final WoTService proxyMock = Mockito.mock(WoTService.class); - final CachingWoTService service = new CachingWoTService(proxyMock); - service.verify(null, null, null); - Mockito.verify(proxyMock, times(1)).verify(any(), any(), any()); - service.verify(null, null, null); - Mockito.verify(proxyMock, times(2)).verify(any(), any(), any()); - } - - @Test - void testSign() throws AccessException, SecurityFailure, ParseException, JOSEException, ApiException { - final WoTService proxyMock = Mockito.mock(WoTService.class); - final TrustedUserDto trustedUser = new TrustedUserDto(); - Mockito.when(proxyMock.sign(any(), any())).thenReturn(trustedUser); - final CachingWoTService service = new CachingWoTService(proxyMock); - assertEquals(trustedUser, service.sign(null, null)); - Mockito.verify(proxyMock, times(1)).sign(any(), any()); - assertEquals(trustedUser, service.sign(null, null)); - Mockito.verify(proxyMock, times(2)).sign(any(), any()); - } -} From 852e7b513d2ec896547006e3242744770d28d631 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 15 Oct 2025 08:10:02 +0200 Subject: [PATCH 102/133] No duplicate transcript. --- hub/src/test/resources/log4j-test.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/hub/src/test/resources/log4j-test.xml b/hub/src/test/resources/log4j-test.xml index fc4fb139..36948bf7 100644 --- a/hub/src/test/resources/log4j-test.xml +++ b/hub/src/test/resources/log4j-test.xml @@ -32,7 +32,6 @@ - From 0dae67fba60853bb22bebb6d171fa0a778265f78 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 15 Oct 2025 09:45:00 +0200 Subject: [PATCH 103/133] Delete test. --- .../test/java/cloud/katta/core/AbstractHubSynchronizeTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java index 0a915afa..99ce7ae5 100644 --- a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java +++ b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java @@ -335,8 +335,6 @@ public void test04SetupCode(final HubTestConfig config) throws Exception { assertEquals(StringUtils.EMPTY, hubSession.getHost().getCredentials().getPassword()); final ListService feature = hubSession.getFeature(ListService.class); final AttributedList vaults = feature.list(Home.root(), new DisabledListProgressListener()); - final int expectedNumberOfVaults = 4; - assertEquals(expectedNumberOfVaults, vaults.size()); assertEquals(vaults, feature.list(Home.root(), new DisabledListProgressListener())); for(final Path vault : vaults) { assertTrue(hubSession.getFeature(Find.class).find(vault)); From a5cd29b1639231dcaffa45c882f2847a8a55bb47 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 15 Oct 2025 16:53:05 +0200 Subject: [PATCH 104/133] Revert "No preloading of buckets." This reverts commit 87452aa4 --- .../cloud/katta/protocols/hub/HubSession.java | 12 ++++++++- .../katta/protocols/hub/HubUVFVault.java | 26 ++++++++++--------- .../protocols/hub/HubVaultListService.java | 10 ++++--- .../core/AbstractHubSynchronizeTest.java | 3 ++- .../workflows/AbstractHubWorkflowTest.java | 3 ++- 5 files changed, 35 insertions(+), 19 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 623c80b5..3b1a638c 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -90,6 +90,7 @@ public class HubSession extends HttpSession { private UserDto me; private ConfigDto config; private UserKeys userKeys; + private AttributedList vaults; public HubSession(final Host host, final X509TrustManager trust, final X509KeyManager key) { super(host, trust, key); @@ -183,6 +184,15 @@ public void login(final LoginCallback prompt, final CancelCallback cancel) throw throw new InteroperabilityException(String.format("Unsupported storage configuration %s", storageProfile.getProtocol().name())); } } + // Ensure vaults are registered + try { + vaults = new HubVaultListService(this, prompt).list(Home.root(), new DisabledListProgressListener()); + } + finally { + log.debug("Destroyed user keys {}", userKeys); + // Short-lived + userKeys.destroy(); + } } catch(ApiException e) { throw new HubExceptionMappingService().map(e); @@ -243,7 +253,7 @@ public UserKeys getUserKeys() { @SuppressWarnings("unchecked") public T _getFeature(final Class type) { if(type == ListService.class) { - return (T) new HubVaultListService(this); + return (T) (ListService) (directory, listener) -> vaults; } if(type == Scheduler.class) { return (T) access; diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index d6cf3f58..3348528f 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -10,6 +10,7 @@ import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.exception.ConnectionCanceledException; import ch.cyberduck.core.exception.InteroperabilityException; +import ch.cyberduck.core.exception.UnsupportedException; import ch.cyberduck.core.features.AttributesFinder; import ch.cyberduck.core.features.Directory; import ch.cyberduck.core.features.Write; @@ -63,9 +64,10 @@ public class HubUVFVault extends UVFVault { private final Session storage; private final Path home; + private final LoginCallback prompt; - public HubUVFVault(final HubSession hub, final Path bucket, final HubStorageLocationService.StorageLocation location) throws ConnectionCanceledException { - this(hub, UUID.fromString(new UUIDRandomStringService().random()), bucket, location); + public HubUVFVault(final HubSession hub, final Path bucket, final HubStorageLocationService.StorageLocation location, final LoginCallback prompt) throws ConnectionCanceledException { + this(hub, UUID.fromString(new UUIDRandomStringService().random()), bucket, location, prompt); } /** @@ -73,7 +75,7 @@ public HubUVFVault(final HubSession hub, final Path bucket, final HubStorageLoca * * @param bucket Bucket */ - public HubUVFVault(final HubSession hub, final UUID vaultId, final Path bucket, final HubStorageLocationService.StorageLocation location) throws ConnectionCanceledException { + public HubUVFVault(final HubSession hub, final UUID vaultId, final Path bucket, final HubStorageLocationService.StorageLocation location, final LoginCallback prompt) throws ConnectionCanceledException { this(hub, vaultId, bucket, UvfMetadataPayload.create() .withStorage(new VaultMetadataJWEBackendDto() @@ -83,7 +85,7 @@ public HubUVFVault(final HubSession hub, final UUID vaultId, final Path bucket, .nickname(null != bucket.attributes().getDisplayname() ? bucket.attributes().getDisplayname() : "Vault")) .withAutomaticAccessGrant(new VaultMetadataJWEAutomaticAccessGrantDto() .enabled(true) - .maxWotDepth(null))); + .maxWotDepth(null)), prompt); } /** @@ -91,17 +93,17 @@ public HubUVFVault(final HubSession hub, final UUID vaultId, final Path bucket, * * @param vaultId Vault ID Used to lookup profile */ - public HubUVFVault(final HubSession hub, final UUID vaultId, final UvfMetadataPayload vaultMetadata) throws ConnectionCanceledException { + public HubUVFVault(final HubSession hub, final UUID vaultId, final UvfMetadataPayload vaultMetadata, final LoginCallback prompt) throws ConnectionCanceledException { this(hub, vaultId, new Path(vaultMetadata.storage().getDefaultPath(), EnumSet.of(Path.Type.directory, Path.Type.volume), - new PathAttributes().setDisplayname(vaultMetadata.storage().getNickname())), vaultMetadata); + new PathAttributes().setDisplayname(vaultMetadata.storage().getNickname())), vaultMetadata, prompt); } - public HubUVFVault(final HubSession hub, final UUID vaultId, final Path bucket, final UvfMetadataPayload vaultMetadata) throws ConnectionCanceledException { + public HubUVFVault(final HubSession hub, final UUID vaultId, final Path bucket, final UvfMetadataPayload vaultMetadata, final LoginCallback prompt) throws ConnectionCanceledException { super(bucket); this.vaultId = vaultId; this.vaultMetadata = vaultMetadata; this.home = bucket; - + this.prompt = prompt; final VaultMetadataJWEBackendDto vaultStorageMetadata = vaultMetadata.storage(); final Protocol profile = ProtocolFactory.get().forName(vaultStorageMetadata.getProvider()); log.debug("Loaded profile {} for UVF metadata {}", profile, vaultMetadata); @@ -130,13 +132,13 @@ public Session getStorage() { } @Override - public T getFeature(final Session hub, final Class type, final T delegate) { + public T getFeature(final Session hub, final Class type, final T delegate) throws UnsupportedException { log.debug("Delegate to {} for feature {}", storage, type); // Ignore feature implementation but delegate to storage backend final T feature = storage._getFeature(type); if(null == feature) { log.warn("No feature {} available for {}", type, storage); - return null; + throw new UnsupportedException(); } return super.getFeature(storage, type, feature); } @@ -193,7 +195,7 @@ public Path create(final Session session, final String region, final VaultCre new ProxyPreferencesReader(storage.getHost()).getProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_CREATE_BUCKET)); // No role chaining when creating vault configuration.setProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_TAG, null); - storage.open(ProxyFactory.get(), new DisabledHostKeyCallback(), new DisabledLoginCallback(), new DisabledCancelCallback()); + storage.open(ProxyFactory.get(), new DisabledHostKeyCallback(), prompt, new DisabledCancelCallback()); final Path vault; if(false) { log.debug("Upload vault template to {}", storage); @@ -243,7 +245,7 @@ public HubUVFVault load(final Session session, final PasswordCallback prompt) try { log.debug("Connect to {}", storage); try { - storage.open(ProxyFactory.get(), new DisabledHostKeyCallback(), new DisabledLoginCallback(), new DisabledCancelCallback()); + storage.open(ProxyFactory.get(), new DisabledHostKeyCallback(), this.prompt, new DisabledCancelCallback()); } catch(BackgroundException e) { log.warn("Skip loading vault with failure {} connecting to storage", e.toString()); diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java index 73a149cf..09757fe7 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java @@ -5,10 +5,10 @@ package cloud.katta.protocols.hub; import ch.cyberduck.core.AttributedList; -import ch.cyberduck.core.DisabledPasswordCallback; import ch.cyberduck.core.ListProgressListener; import ch.cyberduck.core.ListService; import ch.cyberduck.core.LocaleFactory; +import ch.cyberduck.core.LoginCallback; import ch.cyberduck.core.Path; import ch.cyberduck.core.exception.AccessDeniedException; import ch.cyberduck.core.exception.BackgroundException; @@ -35,9 +35,11 @@ public class HubVaultListService implements ListService { private static final Logger log = LogManager.getLogger(HubVaultListService.class); private final HubSession session; + private final LoginCallback prompt; - public HubVaultListService(final HubSession session) { + public HubVaultListService(final HubSession session, final LoginCallback prompt) { this.session = session; + this.prompt = prompt; } @Override @@ -56,9 +58,9 @@ public AttributedList list(final Path directory, final ListProgressListene // Find storage configuration in vault metadata final VaultServiceImpl vaultService = new VaultServiceImpl(session); final UvfMetadataPayload vaultMetadata = vaultService.getVaultMetadataJWE(vaultDto.getId(), session.getUserKeys()); - final HubUVFVault vault = new HubUVFVault(session, vaultDto.getId(), vaultMetadata); + final HubUVFVault vault = new HubUVFVault(session, vaultDto.getId(), vaultMetadata, prompt); try { - registry.add(vault.load(session, new DisabledPasswordCallback())); + registry.add(vault.load(session, prompt)); vaults.add(vault.getHome()); listener.chunk(directory, vaults); } diff --git a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java index 99ce7ae5..654d4b3a 100644 --- a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java +++ b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java @@ -8,6 +8,7 @@ import ch.cyberduck.core.AttributedList; import ch.cyberduck.core.DisabledConnectionCallback; import ch.cyberduck.core.DisabledListProgressListener; +import ch.cyberduck.core.DisabledLoginCallback; import ch.cyberduck.core.ListService; import ch.cyberduck.core.OAuthTokens; import ch.cyberduck.core.Path; @@ -246,7 +247,7 @@ public void test03AddVault(final HubTestConfig config) throws Exception { .withAutomaticAccessGrant(new VaultMetadataJWEAutomaticAccessGrantDto() .enabled(true) .maxWotDepth(null)); - final HubUVFVault cryptomator = new HubUVFVault(hubSession, vaultId, bucket, vaultMetadata); + final HubUVFVault cryptomator = new HubUVFVault(hubSession, vaultId, bucket, vaultMetadata, new DisabledLoginCallback()); cryptomator.create(hubSession, location.getIdentifier(), new VaultCredentials(StringUtils.EMPTY)); final AttributedList vaults = hubSession.getFeature(ListService.class).list(Home.root(), new DisabledListProgressListener()); diff --git a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java index bfaa31b6..b6df402b 100644 --- a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java +++ b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java @@ -4,6 +4,7 @@ package cloud.katta.workflows; +import ch.cyberduck.core.DisabledLoginCallback; import ch.cyberduck.core.Path; import ch.cyberduck.core.UUIDRandomStringService; import ch.cyberduck.core.vault.VaultCredentials; @@ -78,7 +79,7 @@ public void testHubWorkflow(final HubTestConfig config) throws Exception { .withAutomaticAccessGrant(new VaultMetadataJWEAutomaticAccessGrantDto() .enabled(true) .maxWotDepth(3)); - final HubUVFVault cryptomator = new HubUVFVault(hubSession, vaultId, bucket, vaultMetadata); + final HubUVFVault cryptomator = new HubUVFVault(hubSession, vaultId, bucket, vaultMetadata, new DisabledLoginCallback()); cryptomator.create(hubSession, location.getIdentifier(), new VaultCredentials(StringUtils.EMPTY)); checkNumberOfVaults(hubSession, config, vaultId, 0, 0, 1, 0, 0); From aeef089c66f1807ff5bba036eef5ea08841a6c93 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 15 Oct 2025 17:41:24 +0200 Subject: [PATCH 105/133] Ensure listener notification. --- hub/src/main/java/cloud/katta/protocols/hub/HubSession.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 3b1a638c..565edd4d 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -253,7 +253,10 @@ public UserKeys getUserKeys() { @SuppressWarnings("unchecked") public T _getFeature(final Class type) { if(type == ListService.class) { - return (T) (ListService) (directory, listener) -> vaults; + return (T) (ListService) (Path directory, ListProgressListener listener) -> { + listener.chunk(directory, vaults); + return vaults; + }; } if(type == Scheduler.class) { return (T) access; From 5ae6d5590f5459aa7aaa0bebaef58eac9d0a7d3c Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 15 Oct 2025 18:09:09 +0200 Subject: [PATCH 106/133] Ignore vaults that fail to load. --- .../hub/HubVaultStorageAwareComparisonService.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultStorageAwareComparisonService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultStorageAwareComparisonService.java index c26521c8..703ec312 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultStorageAwareComparisonService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultStorageAwareComparisonService.java @@ -6,6 +6,7 @@ import ch.cyberduck.core.Path; import ch.cyberduck.core.PathAttributes; +import ch.cyberduck.core.features.Vault; import ch.cyberduck.core.synchronization.Comparison; import ch.cyberduck.core.synchronization.ComparisonService; import ch.cyberduck.core.vault.VaultUnlockCancelException; @@ -44,7 +45,12 @@ private ComparisonService getFeature(final Path vault) throws VaultUnlockCancelE if(null == vault) { return ComparisonService.disabled; } - final HubUVFVault cryptomator = (HubUVFVault) session.getRegistry().find(session, vault); - return cryptomator.getStorage().getFeature(ComparisonService.class); + final Vault impl = session.getRegistry().find(session, vault); + if(impl instanceof HubUVFVault) { + final HubUVFVault cryptomator = (HubUVFVault) impl; + return cryptomator.getStorage().getFeature(ComparisonService.class); + } + // Disabled + return ComparisonService.disabled; } } From 91e6bccc76ae60fea4b333dd99b7f4390d4e5b9c Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 16 Oct 2025 10:31:06 +0200 Subject: [PATCH 107/133] Delete unused accessor. --- .../main/java/cloud/katta/protocols/hub/HubSession.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 565edd4d..42b0df74 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -233,14 +233,6 @@ public UserDto getMe() { return me; } - /** - * - * @return Null when not connected - */ - public ConfigDto getConfig() { - return config; - } - /** * * @return Destroyed keys after login From 682d8128ad6c299dc213192c4896551b27bed0fb Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 16 Oct 2025 10:44:21 +0200 Subject: [PATCH 108/133] Keep user keys alive. --- .../main/java/cloud/katta/protocols/hub/HubSession.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 42b0df74..7b7897e4 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -185,14 +185,7 @@ public void login(final LoginCallback prompt, final CancelCallback cancel) throw } } // Ensure vaults are registered - try { - vaults = new HubVaultListService(this, prompt).list(Home.root(), new DisabledListProgressListener()); - } - finally { - log.debug("Destroyed user keys {}", userKeys); - // Short-lived - userKeys.destroy(); - } + vaults = new HubVaultListService(this, prompt).list(Home.root(), new DisabledListProgressListener()); } catch(ApiException e) { throw new HubExceptionMappingService().map(e); From 98c3095928fcdcd263c50314230dcdd90cd68128 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 16 Oct 2025 11:38:19 +0200 Subject: [PATCH 109/133] Make user keys short-lived. --- .../cloud/katta/protocols/hub/HubSession.java | 24 ++++++++++--------- .../katta/protocols/hub/HubUVFVault.java | 4 +++- .../protocols/hub/HubVaultListService.java | 4 +++- .../resources/Katta Server.cyberduckprofile | 5 ++++ 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 7b7897e4..e4dfb7f9 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -89,8 +89,11 @@ public class HubSession extends HttpSession { private UserDto me; private ConfigDto config; - private UserKeys userKeys; - private AttributedList vaults; + + private final ExpiringObjectHolder userKeys + = new ExpiringObjectHolder<>(preferences.getLong("katta.userkeys.ttl")); + + private HubVaultListService vaults; public HubSession(final Host host, final X509TrustManager trust, final X509KeyManager key) { super(host, trust, key); @@ -166,7 +169,7 @@ public void login(final LoginCallback prompt, final CancelCallback cancel) throw // Ensure device key is available final DeviceSetupCallback setup = prompt.getFeature(DeviceSetupCallback.class); log.debug("Configured with setup prompt {}", setup); - userKeys = this.pair(setup); + userKeys.set(this.pair(setup)); final List storageProfileDtos = new StorageProfileResourceApi(client).apiStorageprofileGet(false); for(StorageProfileDto storageProfileDto : storageProfileDtos) { final StorageProfileDtoWrapper storageProfile = StorageProfileDtoWrapper.coerce(storageProfileDto); @@ -184,8 +187,7 @@ public void login(final LoginCallback prompt, final CancelCallback cancel) throw throw new InteroperabilityException(String.format("Unsupported storage configuration %s", storageProfile.getProtocol().name())); } } - // Ensure vaults are registered - vaults = new HubVaultListService(this, prompt).list(Home.root(), new DisabledListProgressListener()); + vaults = new HubVaultListService(this, prompt); } catch(ApiException e) { throw new HubExceptionMappingService().map(e); @@ -230,18 +232,18 @@ public UserDto getMe() { * * @return Destroyed keys after login */ - public UserKeys getUserKeys() { - return userKeys; + public UserKeys getUserKeys(final DeviceSetupCallback setup) throws BackgroundException { + if(userKeys.get() == null) { + userKeys.set(this.pair(setup)); + } + return userKeys.get(); } @Override @SuppressWarnings("unchecked") public T _getFeature(final Class type) { if(type == ListService.class) { - return (T) (ListService) (Path directory, ListProgressListener listener) -> { - listener.chunk(directory, vaults); - return vaults; - }; + return (T) vaults; } if(type == Scheduler.class) { return (T) access; diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 3348528f..4750f04a 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -37,6 +37,7 @@ import cloud.katta.client.api.VaultResourceApi; import cloud.katta.client.model.UserDto; import cloud.katta.client.model.VaultDto; +import cloud.katta.core.DeviceSetupCallback; import cloud.katta.crypto.UserKeys; import cloud.katta.crypto.uvf.UvfMetadataPayload; import cloud.katta.crypto.uvf.UvfMetadataPayloadPasswordCallback; @@ -182,7 +183,8 @@ public Path create(final Session session, final String region, final VaultCre // Upload JWE log.debug("Grant access to vault {}", vaultDto); final UserDto userDto = hub.getMe(); - final UserKeys userKeys = hub.getUserKeys(); + final DeviceSetupCallback setup = prompt.getFeature(DeviceSetupCallback.class); + final UserKeys userKeys = hub.getUserKeys(setup); vaultResourceApi.apiVaultsVaultIdAccessTokensPost(vaultDto.getId(), Collections.singletonMap(userDto.getId(), jwks.toOwnerAccessToken().encryptForUser(userKeys.ecdhKeyPair().getPublic()))); // Upload vault template to storage diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java index 09757fe7..3569a9ab 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java @@ -25,6 +25,7 @@ import cloud.katta.client.ApiException; import cloud.katta.client.api.VaultResourceApi; import cloud.katta.client.model.VaultDto; +import cloud.katta.core.DeviceSetupCallback; import cloud.katta.crypto.uvf.UvfMetadataPayload; import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService; import cloud.katta.workflows.VaultServiceImpl; @@ -57,7 +58,8 @@ public AttributedList list(final Path directory, final ListProgressListene try { // Find storage configuration in vault metadata final VaultServiceImpl vaultService = new VaultServiceImpl(session); - final UvfMetadataPayload vaultMetadata = vaultService.getVaultMetadataJWE(vaultDto.getId(), session.getUserKeys()); + final DeviceSetupCallback setup = prompt.getFeature(DeviceSetupCallback.class); + final UvfMetadataPayload vaultMetadata = vaultService.getVaultMetadataJWE(vaultDto.getId(), session.getUserKeys(setup)); final HubUVFVault vault = new HubUVFVault(session, vaultDto.getId(), vaultMetadata, prompt); try { registry.add(vault.load(session, prompt)); diff --git a/hub/src/test/resources/Katta Server.cyberduckprofile b/hub/src/test/resources/Katta Server.cyberduckprofile index a3210e9e..0aec7655 100644 --- a/hub/src/test/resources/Katta Server.cyberduckprofile +++ b/hub/src/test/resources/Katta Server.cyberduckprofile @@ -39,5 +39,10 @@ Username Configurable + Properties + + katta.userkeys.ttl + 3600 + From 2e36d8901c45940fbe317e86afde356959f6ddcf Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 16 Oct 2025 12:02:36 +0200 Subject: [PATCH 110/133] Inline configuration. --- .../katta/protocols/hub/HubUVFVault.java | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 4750f04a..4a76b095 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -4,7 +4,20 @@ package cloud.katta.protocols.hub; -import ch.cyberduck.core.*; +import ch.cyberduck.core.CredentialsConfigurator; +import ch.cyberduck.core.DisabledCancelCallback; +import ch.cyberduck.core.DisabledHostKeyCallback; +import ch.cyberduck.core.Host; +import ch.cyberduck.core.HostUrlProvider; +import ch.cyberduck.core.LoginCallback; +import ch.cyberduck.core.PasswordCallback; +import ch.cyberduck.core.Path; +import ch.cyberduck.core.PathAttributes; +import ch.cyberduck.core.Protocol; +import ch.cyberduck.core.ProtocolFactory; +import ch.cyberduck.core.Session; +import ch.cyberduck.core.SessionFactory; +import ch.cyberduck.core.UUIDRandomStringService; import ch.cyberduck.core.cryptomator.ContentWriter; import ch.cyberduck.core.cryptomator.UVFVault; import ch.cyberduck.core.exception.BackgroundException; @@ -99,7 +112,7 @@ public HubUVFVault(final HubSession hub, final UUID vaultId, final UvfMetadataPa new PathAttributes().setDisplayname(vaultMetadata.storage().getNickname())), vaultMetadata, prompt); } - public HubUVFVault(final HubSession hub, final UUID vaultId, final Path bucket, final UvfMetadataPayload vaultMetadata, final LoginCallback prompt) throws ConnectionCanceledException { + public HubUVFVault(final HubSession session, final UUID vaultId, final Path bucket, final UvfMetadataPayload vaultMetadata, final LoginCallback prompt) throws ConnectionCanceledException { super(bucket); this.vaultId = vaultId; this.vaultMetadata = vaultMetadata; @@ -108,20 +121,12 @@ public HubUVFVault(final HubSession hub, final UUID vaultId, final Path bucket, final VaultMetadataJWEBackendDto vaultStorageMetadata = vaultMetadata.storage(); final Protocol profile = ProtocolFactory.get().forName(vaultStorageMetadata.getProvider()); log.debug("Loaded profile {} for UVF metadata {}", profile, vaultMetadata); - final Credentials credentials = - hub.getFeature(CredentialsConfigurator.class).reload().configure(hub.getHost()); - log.debug("Copy credentials {}", credentials); - if(vaultStorageMetadata.getUsername() != null) { - credentials.setUsername(vaultStorageMetadata.getUsername()); - } - if(vaultStorageMetadata.getPassword() != null) { - credentials.setPassword(vaultStorageMetadata.getPassword()); - } - final Host storageProvider = new Host(profile, credentials); + final Host storageProvider = new Host(profile, session.getFeature(CredentialsConfigurator.class).reload().configure(session.getHost()) + .withUsername(vaultStorageMetadata.getUsername()).withPassword(vaultStorageMetadata.getPassword())); storageProvider.setProperty(OAUTH_TOKENEXCHANGE_VAULT, vaultId.toString()); storageProvider.setRegion(vaultStorageMetadata.getRegion()); log.debug("Configured {} for vault {}", storageProvider, this); - this.storage = SessionFactory.create(storageProvider, hub.getFeature(X509TrustManager.class), hub.getFeature(X509KeyManager.class)); + this.storage = SessionFactory.create(storageProvider, session.getFeature(X509TrustManager.class), session.getFeature(X509KeyManager.class)); } /** From 717850de1efa0f9be06f555822aab764775009a5 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 16 Oct 2025 12:13:15 +0200 Subject: [PATCH 111/133] Inline configuration. --- .../cloud/katta/protocols/hub/HubUVFVault.java | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 4a76b095..3af77d98 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -78,7 +78,7 @@ public class HubUVFVault extends UVFVault { private final Session storage; private final Path home; - private final LoginCallback prompt; + private final LoginCallback login; public HubUVFVault(final HubSession hub, final Path bucket, final HubStorageLocationService.StorageLocation location, final LoginCallback prompt) throws ConnectionCanceledException { this(hub, UUID.fromString(new UUIDRandomStringService().random()), bucket, location, prompt); @@ -117,14 +117,13 @@ public HubUVFVault(final HubSession session, final UUID vaultId, final Path buck this.vaultId = vaultId; this.vaultMetadata = vaultMetadata; this.home = bucket; - this.prompt = prompt; + this.login = prompt; final VaultMetadataJWEBackendDto vaultStorageMetadata = vaultMetadata.storage(); final Protocol profile = ProtocolFactory.get().forName(vaultStorageMetadata.getProvider()); log.debug("Loaded profile {} for UVF metadata {}", profile, vaultMetadata); final Host storageProvider = new Host(profile, session.getFeature(CredentialsConfigurator.class).reload().configure(session.getHost()) - .withUsername(vaultStorageMetadata.getUsername()).withPassword(vaultStorageMetadata.getPassword())); + .withUsername(vaultStorageMetadata.getUsername()).withPassword(vaultStorageMetadata.getPassword())).withRegion(vaultStorageMetadata.getRegion()); storageProvider.setProperty(OAUTH_TOKENEXCHANGE_VAULT, vaultId.toString()); - storageProvider.setRegion(vaultStorageMetadata.getRegion()); log.debug("Configured {} for vault {}", storageProvider, this); this.storage = SessionFactory.create(storageProvider, session.getFeature(X509TrustManager.class), session.getFeature(X509KeyManager.class)); } @@ -188,7 +187,7 @@ public Path create(final Session session, final String region, final VaultCre // Upload JWE log.debug("Grant access to vault {}", vaultDto); final UserDto userDto = hub.getMe(); - final DeviceSetupCallback setup = prompt.getFeature(DeviceSetupCallback.class); + final DeviceSetupCallback setup = login.getFeature(DeviceSetupCallback.class); final UserKeys userKeys = hub.getUserKeys(setup); vaultResourceApi.apiVaultsVaultIdAccessTokensPost(vaultDto.getId(), Collections.singletonMap(userDto.getId(), jwks.toOwnerAccessToken().encryptForUser(userKeys.ecdhKeyPair().getPublic()))); @@ -202,7 +201,7 @@ public Path create(final Session session, final String region, final VaultCre new ProxyPreferencesReader(storage.getHost()).getProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_CREATE_BUCKET)); // No role chaining when creating vault configuration.setProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_TAG, null); - storage.open(ProxyFactory.get(), new DisabledHostKeyCallback(), prompt, new DisabledCancelCallback()); + storage.open(ProxyFactory.get(), new DisabledHostKeyCallback(), login, new DisabledCancelCallback()); final Path vault; if(false) { log.debug("Upload vault template to {}", storage); @@ -252,15 +251,14 @@ public HubUVFVault load(final Session session, final PasswordCallback prompt) try { log.debug("Connect to {}", storage); try { - storage.open(ProxyFactory.get(), new DisabledHostKeyCallback(), this.prompt, new DisabledCancelCallback()); + storage.open(ProxyFactory.get(), new DisabledHostKeyCallback(), login, new DisabledCancelCallback()); } catch(BackgroundException e) { log.warn("Skip loading vault with failure {} connecting to storage", e.toString()); throw new VaultUnlockCancelException(this, e); } - final PathAttributes attr = storage.getFeature(AttributesFinder.class).find(home); - attr.setDisplayname(vaultMetadata.storage().getNickname()); - home.setAttributes(attr); + home.setAttributes(storage.getFeature(AttributesFinder.class).find(home) + .setDisplayname(vaultMetadata.storage().getNickname())); log.debug("Initialize vault {} with metadata {}", this, vaultMetadata); // Initialize cryptors super.load(storage, new UvfMetadataPayloadPasswordCallback(vaultMetadata.toJSON())); From 3108aacf209408dba7ad98067850ddfd88d4cae5 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 16 Oct 2025 13:33:45 +0200 Subject: [PATCH 112/133] Wrap with expiring holders. --- .../cloud/katta/protocols/hub/HubSession.java | 34 +++++++++++++------ .../resources/Katta Server.cyberduckprofile | 4 ++- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index e4dfb7f9..9a1a230c 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -87,11 +87,13 @@ public class HubSession extends HttpSession { */ private OAuth2RequestInterceptor authorizationService; - private UserDto me; private ConfigDto config; - private final ExpiringObjectHolder userKeys - = new ExpiringObjectHolder<>(preferences.getLong("katta.userkeys.ttl")); + private final ExpiringObjectHolder userDtoHolder + = new ExpiringObjectHolder<>(-1L == preferences.getLong("katta.user.ttl") ? 60000 : preferences.getLong("katta.user.ttl")); + + private final ExpiringObjectHolder userKeysHolder + = new ExpiringObjectHolder<>(-1L == preferences.getLong("katta.userkeys.ttl") ? 60000 : preferences.getLong("katta.userkeys.ttl")); private HubVaultListService vaults; @@ -164,12 +166,13 @@ public void login(final LoginCallback prompt, final CancelCallback cancel) throw throw new LoginCanceledException(e); } try { - me = new UsersResourceApi(client).apiUsersMeGet(true, false); + final UserDto me = this.getMe(); log.debug("Retrieved user {}", me); // Ensure device key is available final DeviceSetupCallback setup = prompt.getFeature(DeviceSetupCallback.class); log.debug("Configured with setup prompt {}", setup); - userKeys.set(this.pair(setup)); + final UserKeys userKeys = this.getUserKeys(setup); + log.debug("Retrieved user keys {}", userKeys); final List storageProfileDtos = new StorageProfileResourceApi(client).apiStorageprofileGet(false); for(StorageProfileDto storageProfileDto : storageProfileDtos) { final StorageProfileDtoWrapper storageProfile = StorageProfileDtoWrapper.coerce(storageProfileDto); @@ -198,7 +201,8 @@ private UserKeys pair(final DeviceSetupCallback setup) throws BackgroundExceptio try { final DeviceKeys deviceKeys = new DeviceKeysServiceImpl(keychain).getOrCreateDeviceKeys(host, setup); log.debug("Retrieved device keys {}", deviceKeys); - final UserKeys userKeys = new UserKeysServiceImpl(this, keychain).getOrCreateUserKeys(host, me, deviceKeys, setup); + final UserKeys userKeys = new UserKeysServiceImpl(this, keychain).getOrCreateUserKeys(host, + this.getMe(), deviceKeys, setup); log.debug("Retrieved user keys {}", userKeys); return userKeys; } @@ -224,8 +228,16 @@ protected void logout() { * * @return Null prior login */ - public UserDto getMe() { - return me; + public UserDto getMe() throws BackgroundException { + try { + if(userDtoHolder.get() == null) { + userDtoHolder.set(new UsersResourceApi(client).apiUsersMeGet(true, false)); + } + return userDtoHolder.get(); + } + catch(ApiException e) { + throw new HubExceptionMappingService().map(e); + } } /** @@ -233,10 +245,10 @@ public UserDto getMe() { * @return Destroyed keys after login */ public UserKeys getUserKeys(final DeviceSetupCallback setup) throws BackgroundException { - if(userKeys.get() == null) { - userKeys.set(this.pair(setup)); + if(userKeysHolder.get() == null) { + userKeysHolder.set(this.pair(setup)); } - return userKeys.get(); + return userKeysHolder.get(); } @Override diff --git a/hub/src/test/resources/Katta Server.cyberduckprofile b/hub/src/test/resources/Katta Server.cyberduckprofile index 0aec7655..89e8807f 100644 --- a/hub/src/test/resources/Katta Server.cyberduckprofile +++ b/hub/src/test/resources/Katta Server.cyberduckprofile @@ -42,7 +42,9 @@ Properties katta.userkeys.ttl - 3600 + 60000 + katta.user.ttl + 60000 From 6526cf8a46bae5a1d615795cc633cee6ae56f8b5 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 16 Oct 2025 17:41:46 +0200 Subject: [PATCH 113/133] Obtain hub session as feature. --- .../katta/protocols/hub/HubUVFVault.java | 19 ++++++++++--------- .../protocols/hub/HubVaultListService.java | 2 +- .../core/AbstractHubSynchronizeTest.java | 2 +- .../workflows/AbstractHubWorkflowTest.java | 2 +- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 3af77d98..af77ce76 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -80,8 +80,8 @@ public class HubUVFVault extends UVFVault { private final Path home; private final LoginCallback login; - public HubUVFVault(final HubSession hub, final Path bucket, final HubStorageLocationService.StorageLocation location, final LoginCallback prompt) throws ConnectionCanceledException { - this(hub, UUID.fromString(new UUIDRandomStringService().random()), bucket, location, prompt); + public HubUVFVault(final Path bucket, final HubStorageLocationService.StorageLocation location, final LoginCallback prompt) throws ConnectionCanceledException { + this(UUID.fromString(new UUIDRandomStringService().random()), bucket, location, prompt); } /** @@ -89,8 +89,8 @@ public HubUVFVault(final HubSession hub, final Path bucket, final HubStorageLoca * * @param bucket Bucket */ - public HubUVFVault(final HubSession hub, final UUID vaultId, final Path bucket, final HubStorageLocationService.StorageLocation location, final LoginCallback prompt) throws ConnectionCanceledException { - this(hub, vaultId, bucket, + public HubUVFVault(final UUID vaultId, final Path bucket, final HubStorageLocationService.StorageLocation location, final LoginCallback prompt) throws ConnectionCanceledException { + this(vaultId, bucket, UvfMetadataPayload.create() .withStorage(new VaultMetadataJWEBackendDto() .provider(location.getProfile()) @@ -107,12 +107,12 @@ public HubUVFVault(final HubSession hub, final UUID vaultId, final Path bucket, * * @param vaultId Vault ID Used to lookup profile */ - public HubUVFVault(final HubSession hub, final UUID vaultId, final UvfMetadataPayload vaultMetadata, final LoginCallback prompt) throws ConnectionCanceledException { - this(hub, vaultId, new Path(vaultMetadata.storage().getDefaultPath(), EnumSet.of(Path.Type.directory, Path.Type.volume), + public HubUVFVault(final UUID vaultId, final UvfMetadataPayload vaultMetadata, final LoginCallback prompt) throws ConnectionCanceledException { + this(vaultId, new Path(vaultMetadata.storage().getDefaultPath(), EnumSet.of(Path.Type.directory, Path.Type.volume), new PathAttributes().setDisplayname(vaultMetadata.storage().getNickname())), vaultMetadata, prompt); } - public HubUVFVault(final HubSession session, final UUID vaultId, final Path bucket, final UvfMetadataPayload vaultMetadata, final LoginCallback prompt) throws ConnectionCanceledException { + public HubUVFVault(final UUID vaultId, final Path bucket, final UvfMetadataPayload vaultMetadata, final LoginCallback prompt) throws ConnectionCanceledException { super(bucket); this.vaultId = vaultId; this.vaultMetadata = vaultMetadata; @@ -120,12 +120,13 @@ public HubUVFVault(final HubSession session, final UUID vaultId, final Path buck this.login = prompt; final VaultMetadataJWEBackendDto vaultStorageMetadata = vaultMetadata.storage(); final Protocol profile = ProtocolFactory.get().forName(vaultStorageMetadata.getProvider()); + final HubSession hub = profile.getFeature(HubSession.class); log.debug("Loaded profile {} for UVF metadata {}", profile, vaultMetadata); - final Host storageProvider = new Host(profile, session.getFeature(CredentialsConfigurator.class).reload().configure(session.getHost()) + final Host storageProvider = new Host(profile, hub.getFeature(CredentialsConfigurator.class).reload().configure(hub.getHost()) .withUsername(vaultStorageMetadata.getUsername()).withPassword(vaultStorageMetadata.getPassword())).withRegion(vaultStorageMetadata.getRegion()); storageProvider.setProperty(OAUTH_TOKENEXCHANGE_VAULT, vaultId.toString()); log.debug("Configured {} for vault {}", storageProvider, this); - this.storage = SessionFactory.create(storageProvider, session.getFeature(X509TrustManager.class), session.getFeature(X509KeyManager.class)); + this.storage = SessionFactory.create(storageProvider, hub.getFeature(X509TrustManager.class), hub.getFeature(X509KeyManager.class)); } /** diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java index 3569a9ab..4e5b5895 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java @@ -60,7 +60,7 @@ public AttributedList list(final Path directory, final ListProgressListene final VaultServiceImpl vaultService = new VaultServiceImpl(session); final DeviceSetupCallback setup = prompt.getFeature(DeviceSetupCallback.class); final UvfMetadataPayload vaultMetadata = vaultService.getVaultMetadataJWE(vaultDto.getId(), session.getUserKeys(setup)); - final HubUVFVault vault = new HubUVFVault(session, vaultDto.getId(), vaultMetadata, prompt); + final HubUVFVault vault = new HubUVFVault(vaultDto.getId(), vaultMetadata, prompt); try { registry.add(vault.load(session, prompt)); vaults.add(vault.getHome()); diff --git a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java index 654d4b3a..2bb14862 100644 --- a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java +++ b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java @@ -247,7 +247,7 @@ public void test03AddVault(final HubTestConfig config) throws Exception { .withAutomaticAccessGrant(new VaultMetadataJWEAutomaticAccessGrantDto() .enabled(true) .maxWotDepth(null)); - final HubUVFVault cryptomator = new HubUVFVault(hubSession, vaultId, bucket, vaultMetadata, new DisabledLoginCallback()); + final HubUVFVault cryptomator = new HubUVFVault(vaultId, bucket, vaultMetadata, new DisabledLoginCallback()); cryptomator.create(hubSession, location.getIdentifier(), new VaultCredentials(StringUtils.EMPTY)); final AttributedList vaults = hubSession.getFeature(ListService.class).list(Home.root(), new DisabledListProgressListener()); diff --git a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java index b6df402b..263bb184 100644 --- a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java +++ b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java @@ -79,7 +79,7 @@ public void testHubWorkflow(final HubTestConfig config) throws Exception { .withAutomaticAccessGrant(new VaultMetadataJWEAutomaticAccessGrantDto() .enabled(true) .maxWotDepth(3)); - final HubUVFVault cryptomator = new HubUVFVault(hubSession, vaultId, bucket, vaultMetadata, new DisabledLoginCallback()); + final HubUVFVault cryptomator = new HubUVFVault(vaultId, bucket, vaultMetadata, new DisabledLoginCallback()); cryptomator.create(hubSession, location.getIdentifier(), new VaultCredentials(StringUtils.EMPTY)); checkNumberOfVaults(hubSession, config, vaultId, 0, 0, 1, 0, 0); From f13cc407ccee9c75025a6b3e71c1bb90eaad28ac Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 17 Oct 2025 12:05:19 +0200 Subject: [PATCH 114/133] Logging. --- hub/src/test/resources/log4j-test.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/hub/src/test/resources/log4j-test.xml b/hub/src/test/resources/log4j-test.xml index 36948bf7..b1770a03 100644 --- a/hub/src/test/resources/log4j-test.xml +++ b/hub/src/test/resources/log4j-test.xml @@ -31,6 +31,7 @@ + From 1d2801dc4f6e63187ba791d1f76bc0ee6f82311e Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 17 Oct 2025 16:58:39 +0200 Subject: [PATCH 115/133] Adopt interface changes. --- .../protocols/hub/HubOAuthTokensCredentialsConfigurator.java | 2 +- hub/src/main/java/cloud/katta/protocols/hub/HubSession.java | 3 ++- hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubOAuthTokensCredentialsConfigurator.java b/hub/src/main/java/cloud/katta/protocols/hub/HubOAuthTokensCredentialsConfigurator.java index f6df3cca..894345fe 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubOAuthTokensCredentialsConfigurator.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubOAuthTokensCredentialsConfigurator.java @@ -28,7 +28,7 @@ public HubOAuthTokensCredentialsConfigurator(final HostPasswordStore keychain, f @Override public Credentials configure(final Host host) { - return new Credentials(host.getCredentials()).withOauth(tokens); + return new Credentials(host.getCredentials()).setOauth(tokens); } @Override diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 9a1a230c..9ecc0f09 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -156,7 +156,8 @@ protected HubApiClient connect(final ProxyFinder proxy, final HostKeyCallback ke @Override public void login(final LoginCallback prompt, final CancelCallback cancel) throws BackgroundException { - final Credentials credentials = authorizationService.validate(); + final Credentials credentials = host.getCredentials(); + credentials.setOauth(authorizationService.validate(credentials.getOauth())); try { // Set username from OAuth ID Token for saving in keychain credentials.setUsername(JWT.decode(credentials.getOauth().getIdToken()).getSubject()); diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index af77ce76..d4865b5a 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -123,7 +123,7 @@ public HubUVFVault(final UUID vaultId, final Path bucket, final UvfMetadataPaylo final HubSession hub = profile.getFeature(HubSession.class); log.debug("Loaded profile {} for UVF metadata {}", profile, vaultMetadata); final Host storageProvider = new Host(profile, hub.getFeature(CredentialsConfigurator.class).reload().configure(hub.getHost()) - .withUsername(vaultStorageMetadata.getUsername()).withPassword(vaultStorageMetadata.getPassword())).withRegion(vaultStorageMetadata.getRegion()); + .setUsername(vaultStorageMetadata.getUsername()).setPassword(vaultStorageMetadata.getPassword())).withRegion(vaultStorageMetadata.getRegion()); storageProvider.setProperty(OAUTH_TOKENEXCHANGE_VAULT, vaultId.toString()); log.debug("Configured {} for vault {}", storageProvider, this); this.storage = SessionFactory.create(storageProvider, hub.getFeature(X509TrustManager.class), hub.getFeature(X509KeyManager.class)); From 19a500545665372f39bc145514eaa2a402d84c22 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 17 Oct 2025 17:02:46 +0200 Subject: [PATCH 116/133] Merge implementations. --- .../protocols/s3/S3AssumeRoleSession.java | 12 +- ...TSChainedAssumeRoleRequestInterceptor.java | 92 ++++++++++++-- .../s3/TokenExchangeRequestInterceptor.java | 113 ------------------ 3 files changed, 82 insertions(+), 135 deletions(-) delete mode 100644 hub/src/main/java/cloud/katta/protocols/s3/TokenExchangeRequestInterceptor.java diff --git a/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleSession.java b/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleSession.java index 31d3778b..9207f7a5 100644 --- a/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleSession.java +++ b/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleSession.java @@ -10,7 +10,6 @@ import ch.cyberduck.core.exception.LoginCanceledException; import ch.cyberduck.core.oauth.OAuth2AuthorizationService; import ch.cyberduck.core.oauth.OAuth2RequestInterceptor; -import ch.cyberduck.core.preferences.HostPreferencesFactory; import ch.cyberduck.core.s3.S3CredentialsStrategy; import ch.cyberduck.core.s3.S3Session; import ch.cyberduck.core.ssl.X509KeyManager; @@ -21,8 +20,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import static cloud.katta.protocols.s3.S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE; - public class S3AssumeRoleSession extends S3Session { private static final Logger log = LogManager.getLogger(S3AssumeRoleSession.class); @@ -41,13 +38,8 @@ public S3AssumeRoleSession(final Host host, final X509TrustManager trust, final @Override protected S3CredentialsStrategy configureCredentialsStrategy(final HttpClientBuilder configuration, final LoginCallback prompt) throws LoginCanceledException { if(host.getProtocol().isOAuthConfigurable()) { - final OAuth2RequestInterceptor oauth; - if(HostPreferencesFactory.get(host).getBoolean(OAUTH_TOKENEXCHANGE)) { - oauth = new TokenExchangeRequestInterceptor(configuration.build(), host, prompt); - } - else { - oauth = new OAuth2RequestInterceptor(configuration.build(), host, prompt); - } + // Shared OAuth tokens + final OAuth2RequestInterceptor oauth = new OAuth2RequestInterceptor(configuration.build(), host, prompt); oauth.withRedirectUri(host.getProtocol().getOAuthRedirectUrl()); if(host.getProtocol().getAuthorization() != null) { oauth.withFlowType(OAuth2AuthorizationService.FlowType.valueOf(host.getProtocol().getAuthorization())); diff --git a/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java b/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java index 1d3b0ed0..d7c89db7 100644 --- a/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java +++ b/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java @@ -4,16 +4,16 @@ package cloud.katta.protocols.s3; -import ch.cyberduck.core.Credentials; import ch.cyberduck.core.Host; import ch.cyberduck.core.LoginCallback; +import ch.cyberduck.core.OAuthTokens; import ch.cyberduck.core.Profile; import ch.cyberduck.core.TemporaryAccessTokens; import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.exception.LoginFailureException; import ch.cyberduck.core.oauth.OAuth2RequestInterceptor; import ch.cyberduck.core.preferences.HostPreferencesFactory; import ch.cyberduck.core.preferences.PreferencesReader; -import ch.cyberduck.core.preferences.ProxyPreferencesReader; import ch.cyberduck.core.ssl.X509KeyManager; import ch.cyberduck.core.ssl.X509TrustManager; import ch.cyberduck.core.sts.STSAssumeRoleWithWebIdentityRequestInterceptor; @@ -22,12 +22,30 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.util.Arrays; +import java.util.List; + +import cloud.katta.client.ApiException; +import cloud.katta.client.api.StorageResourceApi; +import cloud.katta.client.model.AccessTokenResponse; +import cloud.katta.protocols.hub.HubSession; +import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService; +import com.auth0.jwt.JWT; +import com.auth0.jwt.exceptions.JWTDecodeException; +import com.auth0.jwt.interfaces.DecodedJWT; + /** * Assume role with temporary credentials obtained using OIDC token from security token service (STS) */ public class STSChainedAssumeRoleRequestInterceptor extends STSAssumeRoleWithWebIdentityRequestInterceptor { private static final Logger log = LogManager.getLogger(STSChainedAssumeRoleRequestInterceptor.class); + /** + * The party to which the ID Token was issued + * ... + */ + private static final String OIDC_AUTHORIZED_PARTY = "azp"; + private final Host bookmark; public STSChainedAssumeRoleRequestInterceptor(final OAuth2RequestInterceptor oauth, final Host host, @@ -40,7 +58,7 @@ public STSChainedAssumeRoleRequestInterceptor(final OAuth2RequestInterceptor oau /** * Assume role with previously obtained temporary access token * - * @param credentials Session credentials + * @param oauth OIDC tokens * @return Temporary scoped access tokens * @throws ch.cyberduck.core.exception.ExpiredTokenException Expired identity * @throws ch.cyberduck.core.exception.LoginFailureException Authorization failure @@ -48,19 +66,69 @@ public STSChainedAssumeRoleRequestInterceptor(final OAuth2RequestInterceptor oau * @see S3AssumeRoleProtocol#S3_ASSUMEROLE_ROLEARN_CREATE_BUCKET */ @Override - public TemporaryAccessTokens assumeRoleWithWebIdentity(final Credentials credentials) throws BackgroundException { - final PreferencesReader settings = new ProxyPreferencesReader(bookmark, credentials); - final TemporaryAccessTokens tokens = super.assumeRoleWithWebIdentity(credentials - .withProperty(Profile.STS_ROLE_ARN_PROPERTY_KEY, settings.getProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_WEBIDENTITY))); + public TemporaryAccessTokens assumeRoleWithWebIdentity(final OAuthTokens oauth, final String roleArn) throws BackgroundException { + final PreferencesReader settings = HostPreferencesFactory.get(bookmark); + final TemporaryAccessTokens tokens = super.assumeRoleWithWebIdentity(this.tokenExchange(oauth), settings.getProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_WEBIDENTITY)); if(StringUtils.isNotBlank(settings.getProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_TAG))) { log.debug("Assume role with temporary credentials {}", tokens); // Assume role with previously obtained temporary access token - return super.assumeRole(credentials.withTokens(tokens) - .withProperty(Profile.STS_ROLE_ARN_PROPERTY_KEY, settings.getProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_TAG)) - .withProperty(Profile.STS_TAGS_PROPERTY_KEY, String.format("%s=%s", HostPreferencesFactory.get(bookmark).getProperty("s3.assumerole.rolearn.tag.vaultid.key"), - settings.getProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_VAULT))) - ); + return super.assumeRole(credentials.setTokens(tokens) + .setProperty(Profile.STS_TAGS_PROPERTY_KEY, String.format("%s=%s", HostPreferencesFactory.get(bookmark).getProperty("s3.assumerole.rolearn.tag.vaultid.key"), + settings.getProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_VAULT))), + settings.getProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_TAG)); + } + log.warn("No vault tag set. Skip assuming role with temporary credentials {} for {}", tokens, bookmark); + return tokens; + } + + /** + * Perform OAuth 2.0 Token Exchange + * + * @return New tokens + * @see S3AssumeRoleProtocol#OAUTH_TOKENEXCHANGE_VAULT + */ + private OAuthTokens tokenExchange(final OAuthTokens tokens) throws BackgroundException { + final PreferencesReader settings = HostPreferencesFactory.get(bookmark); + if(settings.getBoolean(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE)) { + if(this.isTokenExchangeRequired(tokens)) { + log.info("Exchange tokens for {}", bookmark); + final HubSession hub = bookmark.getProtocol().getFeature(HubSession.class); + log.debug("Exchange token with hub {}", hub); + final StorageResourceApi api = new StorageResourceApi(hub.getClient()); + try { + final AccessTokenResponse tokenExchangeResponse = api.apiStorageS3TokenPost(settings.getProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_VAULT)); + // N.B. token exchange with Id token does not work! + final OAuthTokens exchanged = new OAuthTokens(tokenExchangeResponse.getAccessToken(), + tokenExchangeResponse.getRefreshToken(), + tokenExchangeResponse.getExpiresIn() != null ? System.currentTimeMillis() + tokenExchangeResponse.getExpiresIn() * 1000 : null); + log.debug("Received exchanged token {} for {}", exchanged, bookmark); + return exchanged; + } + catch(ApiException e) { + throw new HubExceptionMappingService().map(e); + } + } } return tokens; } + + private boolean isTokenExchangeRequired(final OAuthTokens tokens) throws BackgroundException { + final String accessToken = tokens.getAccessToken(); + try { + final DecodedJWT jwt = JWT.decode(accessToken); + final List auds = jwt.getAudience(); + final String azp = jwt.getClaim(OIDC_AUTHORIZED_PARTY).asString(); + log.debug("Decoded JWT {} with audience {} and azp {}", jwt, Arrays.toString(auds.toArray()), azp); + final boolean audNotUnique = 1 != auds.size(); // either multiple audiences or none + // do exchange if aud is not unique or azp is not equal to aud + if(audNotUnique || !auds.get(0).equals(azp)) { + log.debug("None or multiple audiences found {} or audience differs from azp {}", Arrays.toString(auds.toArray()), azp); + return true; + } + } + catch(JWTDecodeException e) { + throw new LoginFailureException("Invalid JWT or JSON format in authentication token", e); + } + return false; + } } diff --git a/hub/src/main/java/cloud/katta/protocols/s3/TokenExchangeRequestInterceptor.java b/hub/src/main/java/cloud/katta/protocols/s3/TokenExchangeRequestInterceptor.java deleted file mode 100644 index 78149d01..00000000 --- a/hub/src/main/java/cloud/katta/protocols/s3/TokenExchangeRequestInterceptor.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (c) 2025 shift7 GmbH. All rights reserved. - */ - -package cloud.katta.protocols.s3; - -import ch.cyberduck.core.Credentials; -import ch.cyberduck.core.Host; -import ch.cyberduck.core.LoginCallback; -import ch.cyberduck.core.OAuthTokens; -import ch.cyberduck.core.exception.BackgroundException; -import ch.cyberduck.core.exception.LoginCanceledException; -import ch.cyberduck.core.exception.LoginFailureException; -import ch.cyberduck.core.oauth.OAuth2RequestInterceptor; -import ch.cyberduck.core.preferences.HostPreferencesFactory; -import ch.cyberduck.core.preferences.PreferencesReader; - -import org.apache.http.client.HttpClient; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.util.Arrays; -import java.util.List; - -import cloud.katta.client.ApiException; -import cloud.katta.client.api.StorageResourceApi; -import cloud.katta.client.model.AccessTokenResponse; -import cloud.katta.protocols.hub.HubSession; -import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService; -import com.auth0.jwt.JWT; -import com.auth0.jwt.exceptions.JWTDecodeException; -import com.auth0.jwt.interfaces.DecodedJWT; - -/** - * Exchange OIDC token to scoped token using OAuth 2.0 Token Exchange. Used for S3-STS in Katta. - */ -public class TokenExchangeRequestInterceptor extends OAuth2RequestInterceptor { - private static final Logger log = LogManager.getLogger(TokenExchangeRequestInterceptor.class); - - /** - * The party to which the ID Token was issued - * ... - */ - public static final String OIDC_AUTHORIZED_PARTY = "azp"; - - private final Host bookmark; - - public TokenExchangeRequestInterceptor(final HttpClient client, final Host bookmark, final LoginCallback prompt) throws LoginCanceledException { - super(client, bookmark, prompt); - this.bookmark = bookmark; - } - - @Override - public OAuthTokens authorize() throws BackgroundException { - return this.exchange(super.authorize()); - } - - @Override - public OAuthTokens refresh(final OAuthTokens previous) throws BackgroundException { - return this.exchange(super.refresh(previous)); - } - - /** - * Perform OAuth 2.0 Token Exchange - * - * @param previous Input tokens retrieved to exchange at the token endpoint - * @return New tokens - * @see S3AssumeRoleProtocol#OAUTH_TOKENEXCHANGE_VAULT - */ - public OAuthTokens exchange(final OAuthTokens previous) throws BackgroundException { - log.info("Exchange tokens {} for {}", previous, bookmark); - final PreferencesReader preferences = HostPreferencesFactory.get(bookmark); - final HubSession hub = bookmark.getProtocol().getFeature(HubSession.class); - log.debug("Exchange token with hub {}", hub); - final StorageResourceApi api = new StorageResourceApi(hub.getClient()); - try { - final AccessTokenResponse tokenExchangeResponse = api.apiStorageS3TokenPost(preferences.getProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_VAULT)); - // N.B. token exchange with Id token does not work! - final OAuthTokens tokens = new OAuthTokens(tokenExchangeResponse.getAccessToken(), - tokenExchangeResponse.getRefreshToken(), - tokenExchangeResponse.getExpiresIn() != null ? System.currentTimeMillis() + tokenExchangeResponse.getExpiresIn() * 1000 : null); - log.debug("Received exchanged token {} for {}", tokens, bookmark); - return tokens; - } - catch(ApiException e) { - throw new HubExceptionMappingService().map(e); - } - } - - @Override - public Credentials validate() throws BackgroundException { - final Credentials credentials = super.validate(); - final OAuthTokens tokens = credentials.getOauth(); - final String accessToken = tokens.getAccessToken(); - try { - final DecodedJWT jwt = JWT.decode(accessToken); - - final List auds = jwt.getAudience(); - final String azp = jwt.getClaim(OIDC_AUTHORIZED_PARTY).asString(); - - final boolean audNotUnique = 1 != auds.size(); // either multiple audiences or none - // do exchange if aud is not unique or azp is not equal to aud - if(audNotUnique || !auds.get(0).equals(azp)) { - log.debug("None or multiple audiences found {} or audience differs from azp {}, triggering token-exchange.", Arrays.toString(auds.toArray()), azp); - return credentials.withOauth(this.exchange(tokens)); - } - } - catch(JWTDecodeException e) { - throw new LoginFailureException("Invalid JWT or JSON format in authentication token", e); - } - return credentials; - } -} From 833f56bf76ce8f8aef67588dd39ea70412cd99b6 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 17 Oct 2025 18:02:02 +0200 Subject: [PATCH 117/133] Share OAuth tokens. Resolves #29. --- .../java/cloud/katta/protocols/hub/HubAwareProfile.java | 9 ++++++++- .../main/java/cloud/katta/protocols/hub/HubSession.java | 3 ++- .../cloud/katta/protocols/s3/S3AssumeRoleSession.java | 7 +------ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubAwareProfile.java b/hub/src/main/java/cloud/katta/protocols/hub/HubAwareProfile.java index 149082b1..fb00cf2f 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubAwareProfile.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubAwareProfile.java @@ -6,6 +6,7 @@ import ch.cyberduck.core.Profile; import ch.cyberduck.core.Protocol; +import ch.cyberduck.core.oauth.OAuth2RequestInterceptor; import cloud.katta.client.model.ConfigDto; import cloud.katta.model.StorageProfileDtoWrapper; @@ -13,11 +14,14 @@ import cloud.katta.protocols.hub.serializer.StorageProfileDtoWrapperDeserializer; public final class HubAwareProfile extends Profile { + private final HubSession hub; + private final OAuth2RequestInterceptor oauth; - public HubAwareProfile(final HubSession hub, final Protocol parent, final ConfigDto configDto, final StorageProfileDtoWrapper storageProfile) { + public HubAwareProfile(final HubSession hub, final OAuth2RequestInterceptor oauth, final Protocol parent, final ConfigDto configDto, final StorageProfileDtoWrapper storageProfile) { super(parent, new HubConfigDtoDeserializer(configDto, new StorageProfileDtoWrapperDeserializer(storageProfile))); this.hub = hub; + this.oauth = oauth; } @SuppressWarnings("unchecked") @@ -26,6 +30,9 @@ public T getFeature(final Class type) { if(type == HubSession.class) { return (T) hub; } + if(type == OAuth2RequestInterceptor.class) { + return (T) oauth; + } return super.getFeature(type); } } diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 9ecc0f09..629af7a5 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -182,7 +182,8 @@ public void login(final LoginCallback prompt, final CancelCallback cancel) throw case S3: case S3_STS: final ProtocolFactory protocols = ProtocolFactory.get(); - final Profile profile = new HubAwareProfile(this, protocols.forType(protocols.find(ProtocolFactory.BUNDLED_PROFILE_PREDICATE), Protocol.Type.s3), + final Profile profile = new HubAwareProfile(this, authorizationService, + protocols.forType(protocols.find(ProtocolFactory.BUNDLED_PROFILE_PREDICATE), Protocol.Type.s3), config, storageProfile); log.debug("Register profile {}", profile); protocols.register(profile); diff --git a/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleSession.java b/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleSession.java index 9207f7a5..e00f609a 100644 --- a/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleSession.java +++ b/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleSession.java @@ -8,7 +8,6 @@ import ch.cyberduck.core.LoginCallback; import ch.cyberduck.core.OAuthTokens; import ch.cyberduck.core.exception.LoginCanceledException; -import ch.cyberduck.core.oauth.OAuth2AuthorizationService; import ch.cyberduck.core.oauth.OAuth2RequestInterceptor; import ch.cyberduck.core.s3.S3CredentialsStrategy; import ch.cyberduck.core.s3.S3Session; @@ -39,11 +38,7 @@ public S3AssumeRoleSession(final Host host, final X509TrustManager trust, final protected S3CredentialsStrategy configureCredentialsStrategy(final HttpClientBuilder configuration, final LoginCallback prompt) throws LoginCanceledException { if(host.getProtocol().isOAuthConfigurable()) { // Shared OAuth tokens - final OAuth2RequestInterceptor oauth = new OAuth2RequestInterceptor(configuration.build(), host, prompt); - oauth.withRedirectUri(host.getProtocol().getOAuthRedirectUrl()); - if(host.getProtocol().getAuthorization() != null) { - oauth.withFlowType(OAuth2AuthorizationService.FlowType.valueOf(host.getProtocol().getAuthorization())); - } + final OAuth2RequestInterceptor oauth = host.getProtocol().getFeature(OAuth2RequestInterceptor.class); log.debug("Register interceptor {}", oauth); configuration.addInterceptorLast(oauth); final STSRequestInterceptor sts = new STSChainedAssumeRoleRequestInterceptor(oauth, host, trust, key, prompt) { From f0cbfce5d04e7d4bc30d4bfa9bade05b6bebcb70 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 17 Oct 2025 18:02:52 +0200 Subject: [PATCH 118/133] Read cached setting. --- hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index d4865b5a..b742ecd3 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -27,8 +27,8 @@ import ch.cyberduck.core.features.AttributesFinder; import ch.cyberduck.core.features.Directory; import ch.cyberduck.core.features.Write; +import ch.cyberduck.core.preferences.HostPreferencesFactory; import ch.cyberduck.core.preferences.PreferencesFactory; -import ch.cyberduck.core.preferences.ProxyPreferencesReader; import ch.cyberduck.core.proxy.ProxyFactory; import ch.cyberduck.core.s3.S3Session; import ch.cyberduck.core.ssl.X509KeyManager; @@ -199,7 +199,7 @@ public Path create(final Session session, final String region, final VaultCre configuration.setProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE, null); // Assume role with policy attached to create vault configuration.setProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_WEBIDENTITY, - new ProxyPreferencesReader(storage.getHost()).getProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_CREATE_BUCKET)); + HostPreferencesFactory.get(storage.getHost()).getProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_CREATE_BUCKET)); // No role chaining when creating vault configuration.setProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_TAG, null); storage.open(ProxyFactory.get(), new DisabledHostKeyCallback(), login, new DisabledCancelCallback()); From e098e7c0103750dcd2bb4b9b5d0fa1e6bff76157 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 17 Oct 2025 22:49:00 +0200 Subject: [PATCH 119/133] Extract field. --- .../s3/STSChainedAssumeRoleRequestInterceptor.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java b/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java index d7c89db7..277ecbc0 100644 --- a/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java +++ b/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java @@ -47,12 +47,14 @@ public class STSChainedAssumeRoleRequestInterceptor extends STSAssumeRoleWithWeb private static final String OIDC_AUTHORIZED_PARTY = "azp"; private final Host bookmark; + private final String vaultId; public STSChainedAssumeRoleRequestInterceptor(final OAuth2RequestInterceptor oauth, final Host host, final X509TrustManager trust, final X509KeyManager key, final LoginCallback prompt) { super(oauth, host, trust, key, prompt); this.bookmark = host; + this.vaultId = HostPreferencesFactory.get(host).getProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_VAULT); } /** @@ -73,8 +75,7 @@ public TemporaryAccessTokens assumeRoleWithWebIdentity(final OAuthTokens oauth, log.debug("Assume role with temporary credentials {}", tokens); // Assume role with previously obtained temporary access token return super.assumeRole(credentials.setTokens(tokens) - .setProperty(Profile.STS_TAGS_PROPERTY_KEY, String.format("%s=%s", HostPreferencesFactory.get(bookmark).getProperty("s3.assumerole.rolearn.tag.vaultid.key"), - settings.getProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_VAULT))), + .setProperty(Profile.STS_TAGS_PROPERTY_KEY, String.format("%s=%s", HostPreferencesFactory.get(bookmark).getProperty("s3.assumerole.rolearn.tag.vaultid.key"), vaultId)), settings.getProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_TAG)); } log.warn("No vault tag set. Skip assuming role with temporary credentials {} for {}", tokens, bookmark); @@ -96,7 +97,7 @@ private OAuthTokens tokenExchange(final OAuthTokens tokens) throws BackgroundExc log.debug("Exchange token with hub {}", hub); final StorageResourceApi api = new StorageResourceApi(hub.getClient()); try { - final AccessTokenResponse tokenExchangeResponse = api.apiStorageS3TokenPost(settings.getProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_VAULT)); + final AccessTokenResponse tokenExchangeResponse = api.apiStorageS3TokenPost(vaultId); // N.B. token exchange with Id token does not work! final OAuthTokens exchanged = new OAuthTokens(tokenExchangeResponse.getAccessToken(), tokenExchangeResponse.getRefreshToken(), From 67d76ff893c9563a7ffacb4370b6f4edc92a04cb Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sat, 18 Oct 2025 23:17:42 +0200 Subject: [PATCH 120/133] Isolate storage configurations per hub. --- .../cloud/katta/protocols/hub/HubSession.java | 15 +++++++++++---- .../cloud/katta/protocols/hub/HubUVFVault.java | 16 +++++++--------- .../katta/protocols/hub/HubVaultListService.java | 12 ++++++++++-- .../katta/core/AbstractHubSynchronizeTest.java | 4 +++- .../cloud/katta/testsetup/AbstractHubTest.java | 2 +- .../katta/workflows/AbstractHubWorkflowTest.java | 4 +++- 6 files changed, 35 insertions(+), 18 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index 629af7a5..efe328d8 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -82,6 +82,11 @@ public class HubSession extends HttpSession { */ private final Scheduler access = new HubGrantAccessSchedulerService(this, keychain); + /** + * Registered storage profiles + */ + private final ProtocolFactory storage = new ProtocolFactory(); + /** * Interceptor for OpenID connect flow */ @@ -181,18 +186,17 @@ public void login(final LoginCallback prompt, final CancelCallback cancel) throw switch(storageProfile.getProtocol()) { case S3: case S3_STS: - final ProtocolFactory protocols = ProtocolFactory.get(); final Profile profile = new HubAwareProfile(this, authorizationService, - protocols.forType(protocols.find(ProtocolFactory.BUNDLED_PROFILE_PREDICATE), Protocol.Type.s3), + ProtocolFactory.get().forType(ProtocolFactory.get().find(ProtocolFactory.BUNDLED_PROFILE_PREDICATE), Protocol.Type.s3), config, storageProfile); log.debug("Register profile {}", profile); - protocols.register(profile); + storage.register(profile); break; default: throw new InteroperabilityException(String.format("Unsupported storage configuration %s", storageProfile.getProtocol().name())); } } - vaults = new HubVaultListService(this, prompt); + vaults = new HubVaultListService(storage, this, prompt); } catch(ApiException e) { throw new HubExceptionMappingService().map(e); @@ -383,6 +387,9 @@ public void preflight(final Path file) throws BackgroundException { if(type == CredentialsConfigurator.class) { return (T) new HubOAuthTokensCredentialsConfigurator(keychain, host); } + if(type == ProtocolFactory.class) { + return (T) storage; + } return super._getFeature(type); } } diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index b742ecd3..f3ecd1c5 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -14,7 +14,6 @@ import ch.cyberduck.core.Path; import ch.cyberduck.core.PathAttributes; import ch.cyberduck.core.Protocol; -import ch.cyberduck.core.ProtocolFactory; import ch.cyberduck.core.Session; import ch.cyberduck.core.SessionFactory; import ch.cyberduck.core.UUIDRandomStringService; @@ -80,8 +79,8 @@ public class HubUVFVault extends UVFVault { private final Path home; private final LoginCallback login; - public HubUVFVault(final Path bucket, final HubStorageLocationService.StorageLocation location, final LoginCallback prompt) throws ConnectionCanceledException { - this(UUID.fromString(new UUIDRandomStringService().random()), bucket, location, prompt); + public HubUVFVault(final Protocol profile, final Path bucket, final HubStorageLocationService.StorageLocation location, final LoginCallback prompt) throws ConnectionCanceledException { + this(profile, UUID.fromString(new UUIDRandomStringService().random()), bucket, location, prompt); } /** @@ -89,8 +88,8 @@ public HubUVFVault(final Path bucket, final HubStorageLocationService.StorageLoc * * @param bucket Bucket */ - public HubUVFVault(final UUID vaultId, final Path bucket, final HubStorageLocationService.StorageLocation location, final LoginCallback prompt) throws ConnectionCanceledException { - this(vaultId, bucket, + public HubUVFVault(final Protocol profile, final UUID vaultId, final Path bucket, final HubStorageLocationService.StorageLocation location, final LoginCallback prompt) throws ConnectionCanceledException { + this(profile, vaultId, bucket, UvfMetadataPayload.create() .withStorage(new VaultMetadataJWEBackendDto() .provider(location.getProfile()) @@ -107,19 +106,18 @@ public HubUVFVault(final UUID vaultId, final Path bucket, final HubStorageLocati * * @param vaultId Vault ID Used to lookup profile */ - public HubUVFVault(final UUID vaultId, final UvfMetadataPayload vaultMetadata, final LoginCallback prompt) throws ConnectionCanceledException { - this(vaultId, new Path(vaultMetadata.storage().getDefaultPath(), EnumSet.of(Path.Type.directory, Path.Type.volume), + public HubUVFVault(final Protocol profile, final UUID vaultId, final UvfMetadataPayload vaultMetadata, final LoginCallback prompt) throws ConnectionCanceledException { + this(profile, vaultId, new Path(vaultMetadata.storage().getDefaultPath(), EnumSet.of(Path.Type.directory, Path.Type.volume), new PathAttributes().setDisplayname(vaultMetadata.storage().getNickname())), vaultMetadata, prompt); } - public HubUVFVault(final UUID vaultId, final Path bucket, final UvfMetadataPayload vaultMetadata, final LoginCallback prompt) throws ConnectionCanceledException { + public HubUVFVault(final Protocol profile, final UUID vaultId, final Path bucket, final UvfMetadataPayload vaultMetadata, final LoginCallback prompt) throws ConnectionCanceledException { super(bucket); this.vaultId = vaultId; this.vaultMetadata = vaultMetadata; this.home = bucket; this.login = prompt; final VaultMetadataJWEBackendDto vaultStorageMetadata = vaultMetadata.storage(); - final Protocol profile = ProtocolFactory.get().forName(vaultStorageMetadata.getProvider()); final HubSession hub = profile.getFeature(HubSession.class); log.debug("Loaded profile {} for UVF metadata {}", profile, vaultMetadata); final Host storageProvider = new Host(profile, hub.getFeature(CredentialsConfigurator.class).reload().configure(hub.getHost()) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java index 4e5b5895..dfb13e79 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java @@ -10,6 +10,8 @@ import ch.cyberduck.core.LocaleFactory; import ch.cyberduck.core.LoginCallback; import ch.cyberduck.core.Path; +import ch.cyberduck.core.Protocol; +import ch.cyberduck.core.ProtocolFactory; import ch.cyberduck.core.exception.AccessDeniedException; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.exception.NotfoundException; @@ -35,10 +37,15 @@ public class HubVaultListService implements ListService { private static final Logger log = LogManager.getLogger(HubVaultListService.class); + /** + * Storage profiles + */ + private final ProtocolFactory storage; private final HubSession session; private final LoginCallback prompt; - public HubVaultListService(final HubSession session, final LoginCallback prompt) { + public HubVaultListService(final ProtocolFactory storage, final HubSession session, final LoginCallback prompt) { + this.storage = storage; this.session = session; this.prompt = prompt; } @@ -60,7 +67,8 @@ public AttributedList list(final Path directory, final ListProgressListene final VaultServiceImpl vaultService = new VaultServiceImpl(session); final DeviceSetupCallback setup = prompt.getFeature(DeviceSetupCallback.class); final UvfMetadataPayload vaultMetadata = vaultService.getVaultMetadataJWE(vaultDto.getId(), session.getUserKeys(setup)); - final HubUVFVault vault = new HubUVFVault(vaultDto.getId(), vaultMetadata, prompt); + final Protocol profile = storage.forName(vaultMetadata.storage().getProvider()); + final HubUVFVault vault = new HubUVFVault(profile, vaultDto.getId(), vaultMetadata, prompt); try { registry.add(vault.load(session, prompt)); vaults.add(vault.getHome()); diff --git a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java index 2bb14862..365de03f 100644 --- a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java +++ b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java @@ -12,6 +12,7 @@ import ch.cyberduck.core.ListService; import ch.cyberduck.core.OAuthTokens; import ch.cyberduck.core.Path; +import ch.cyberduck.core.ProtocolFactory; import ch.cyberduck.core.Session; import ch.cyberduck.core.SimplePathPredicate; import ch.cyberduck.core.UUIDRandomStringService; @@ -247,7 +248,8 @@ public void test03AddVault(final HubTestConfig config) throws Exception { .withAutomaticAccessGrant(new VaultMetadataJWEAutomaticAccessGrantDto() .enabled(true) .maxWotDepth(null)); - final HubUVFVault cryptomator = new HubUVFVault(vaultId, bucket, vaultMetadata, new DisabledLoginCallback()); + final HubUVFVault cryptomator = new HubUVFVault(hubSession.getFeature(ProtocolFactory.class).forName(location.getProfile()), + vaultId, bucket, vaultMetadata, new DisabledLoginCallback()); cryptomator.create(hubSession, location.getIdentifier(), new VaultCredentials(StringUtils.EMPTY)); final AttributedList vaults = hubSession.getFeature(ListService.class).list(Home.root(), new DisabledListProgressListener()); diff --git a/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java b/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java index 74a62160..3cb42015 100644 --- a/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java +++ b/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java @@ -166,7 +166,7 @@ private static String staticSetupCode() { protected static HubSession setupConnection(final HubTestConfig.Setup setup) throws Exception { final ProtocolFactory factory = ProtocolFactory.get(); // ProtocolFactory.get() is static, the profiles contains OAuth token URL, leads to invalid grant exceptions when this changes during class loading lifetime (e.g. if the same storage profile ID is deployed to the LOCAL and the HYBRID hub). - for(final Protocol protocol : ProtocolFactory.get().find()) { + for(final Protocol protocol : factory.find()) { if(protocol instanceof Profile) { factory.unregister((Profile) protocol); } diff --git a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java index 263bb184..05760b18 100644 --- a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java +++ b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java @@ -6,6 +6,7 @@ import ch.cyberduck.core.DisabledLoginCallback; import ch.cyberduck.core.Path; +import ch.cyberduck.core.ProtocolFactory; import ch.cyberduck.core.UUIDRandomStringService; import ch.cyberduck.core.vault.VaultCredentials; @@ -79,7 +80,8 @@ public void testHubWorkflow(final HubTestConfig config) throws Exception { .withAutomaticAccessGrant(new VaultMetadataJWEAutomaticAccessGrantDto() .enabled(true) .maxWotDepth(3)); - final HubUVFVault cryptomator = new HubUVFVault(vaultId, bucket, vaultMetadata, new DisabledLoginCallback()); + final HubUVFVault cryptomator = new HubUVFVault(hubSession.getFeature(ProtocolFactory.class).forName(location.getProfile()), + vaultId, bucket, vaultMetadata, new DisabledLoginCallback()); cryptomator.create(hubSession, location.getIdentifier(), new VaultCredentials(StringUtils.EMPTY)); checkNumberOfVaults(hubSession, config, vaultId, 0, 0, 1, 0, 0); From d07f6b4e93b4acf7c7afabd969f0b926d1862ffe Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sun, 19 Oct 2025 20:23:35 +0200 Subject: [PATCH 121/133] Extract builder method. --- .../protocols/hub/HubStorageLocationService.java | 15 +++++++++++++++ .../cloud/katta/protocols/hub/HubUVFVault.java | 12 +----------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java index f1478f16..f91caa17 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubStorageLocationService.java @@ -19,6 +19,9 @@ import cloud.katta.client.ApiException; import cloud.katta.client.api.StorageProfileResourceApi; import cloud.katta.client.model.StorageProfileDto; +import cloud.katta.crypto.uvf.UvfMetadataPayload; +import cloud.katta.crypto.uvf.VaultMetadataJWEAutomaticAccessGrantDto; +import cloud.katta.crypto.uvf.VaultMetadataJWEBackendDto; import cloud.katta.model.StorageProfileDtoWrapper; public class HubStorageLocationService implements Location { @@ -111,5 +114,17 @@ public static StorageLocation fromIdentifier(final String identifier) { } return new StorageLocation(StringUtils.isBlank(parts[0]) ? null : parts[0], StringUtils.isBlank(parts[1]) ? null : parts[1], null); } + + public UvfMetadataPayload toUvfMetadataPayload(final Path bucket) { + return UvfMetadataPayload.create() + .withStorage(new VaultMetadataJWEBackendDto() + .provider(this.getProfile()) + .defaultPath(bucket.getAbsolute()) + .region(this.getRegion()) + .nickname(null != bucket.attributes().getDisplayname() ? bucket.attributes().getDisplayname() : "Vault")) + .withAutomaticAccessGrant(new VaultMetadataJWEAutomaticAccessGrantDto() + .enabled(true) + .maxWotDepth(null)); + } } } diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index f3ecd1c5..45ec3a87 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -53,7 +53,6 @@ import cloud.katta.crypto.UserKeys; import cloud.katta.crypto.uvf.UvfMetadataPayload; import cloud.katta.crypto.uvf.UvfMetadataPayloadPasswordCallback; -import cloud.katta.crypto.uvf.VaultMetadataJWEAutomaticAccessGrantDto; import cloud.katta.crypto.uvf.VaultMetadataJWEBackendDto; import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService; import cloud.katta.protocols.s3.S3AssumeRoleProtocol; @@ -89,16 +88,7 @@ public HubUVFVault(final Protocol profile, final Path bucket, final HubStorageLo * @param bucket Bucket */ public HubUVFVault(final Protocol profile, final UUID vaultId, final Path bucket, final HubStorageLocationService.StorageLocation location, final LoginCallback prompt) throws ConnectionCanceledException { - this(profile, vaultId, bucket, - UvfMetadataPayload.create() - .withStorage(new VaultMetadataJWEBackendDto() - .provider(location.getProfile()) - .defaultPath(bucket.getAbsolute()) - .region(location.getRegion()) - .nickname(null != bucket.attributes().getDisplayname() ? bucket.attributes().getDisplayname() : "Vault")) - .withAutomaticAccessGrant(new VaultMetadataJWEAutomaticAccessGrantDto() - .enabled(true) - .maxWotDepth(null)), prompt); + this(profile, vaultId, bucket, location.toUvfMetadataPayload(bucket), prompt); } /** From 448b3d6e5a0dbcb2d53c93a385776b35e6790276 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sun, 19 Oct 2025 20:24:13 +0200 Subject: [PATCH 122/133] Delete unused. --- .../cloud/katta/protocols/hub/HubUVFVault.java | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 45ec3a87..dbd10fbf 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -16,7 +16,6 @@ import ch.cyberduck.core.Protocol; import ch.cyberduck.core.Session; import ch.cyberduck.core.SessionFactory; -import ch.cyberduck.core.UUIDRandomStringService; import ch.cyberduck.core.cryptomator.ContentWriter; import ch.cyberduck.core.cryptomator.UVFVault; import ch.cyberduck.core.exception.BackgroundException; @@ -78,19 +77,6 @@ public class HubUVFVault extends UVFVault { private final Path home; private final LoginCallback login; - public HubUVFVault(final Protocol profile, final Path bucket, final HubStorageLocationService.StorageLocation location, final LoginCallback prompt) throws ConnectionCanceledException { - this(profile, UUID.fromString(new UUIDRandomStringService().random()), bucket, location, prompt); - } - - /** - * Constructor for factory creating new vault - * - * @param bucket Bucket - */ - public HubUVFVault(final Protocol profile, final UUID vaultId, final Path bucket, final HubStorageLocationService.StorageLocation location, final LoginCallback prompt) throws ConnectionCanceledException { - this(profile, vaultId, bucket, location.toUvfMetadataPayload(bucket), prompt); - } - /** * Open from existing metadata * From 548ee663f95079cf1faaa97375dd060077066da3 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sun, 19 Oct 2025 20:26:16 +0200 Subject: [PATCH 123/133] Remove duplicate field. --- .../main/java/cloud/katta/protocols/hub/HubUVFVault.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index dbd10fbf..222c5110 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -73,8 +73,6 @@ public class HubUVFVault extends UVFVault { * Storage connection only available after loading vault */ private final Session storage; - - private final Path home; private final LoginCallback login; /** @@ -91,7 +89,6 @@ public HubUVFVault(final Protocol profile, final UUID vaultId, final Path bucket super(bucket); this.vaultId = vaultId; this.vaultMetadata = vaultMetadata; - this.home = bucket; this.login = prompt; final VaultMetadataJWEBackendDto vaultStorageMetadata = vaultMetadata.storage(); final HubSession hub = profile.getFeature(HubSession.class); @@ -185,6 +182,7 @@ public Path create(final Session session, final String region, final VaultCre } else { // Obsolete when implemented in super final Directory directory = (Directory) storage._getFeature(Directory.class); + final Path home = this.getHome(); log.debug("Create vault root directory at {}", home); final TransferStatus status = (new TransferStatus()).setRegion(HubStorageLocationService.StorageLocation.fromIdentifier(region).getRegion()); vault = directory.mkdir(storage._getFeature(Write.class), home, status); @@ -232,6 +230,7 @@ public HubUVFVault load(final Session session, final PasswordCallback prompt) log.warn("Skip loading vault with failure {} connecting to storage", e.toString()); throw new VaultUnlockCancelException(this, e); } + final Path home = this.getHome(); home.setAttributes(storage.getFeature(AttributesFinder.class).find(home) .setDisplayname(vaultMetadata.storage().getNickname())); log.debug("Initialize vault {} with metadata {}", this, vaultMetadata); @@ -249,7 +248,7 @@ public String toString() { final StringBuilder sb = new StringBuilder("HubUVFVault{"); sb.append("vaultId=").append(vaultId); sb.append(", vaultMetadata=").append(vaultMetadata); - sb.append(", home=").append(home); + sb.append(", storage=").append(storage); sb.append('}'); return sb.toString(); } From a76a5550276ae4c751f102ef460abd1be9d7f5a6 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sun, 19 Oct 2025 20:32:27 +0200 Subject: [PATCH 124/133] Remove redundant constructor. --- .../cloud/katta/protocols/hub/HubUVFVault.java | 14 ++++++-------- .../katta/core/AbstractHubSynchronizeTest.java | 2 +- .../katta/workflows/AbstractHubWorkflowTest.java | 2 +- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 222c5110..409ad64f 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -76,17 +76,15 @@ public class HubUVFVault extends UVFVault { private final LoginCallback login; /** - * Open from existing metadata * - * @param vaultId Vault ID Used to lookup profile + * @param profile Storage provider configuration + * @param vaultId Vault Id + * @param vaultMetadata Vault UVF metadata + * @param prompt Login prompt to access storage */ public HubUVFVault(final Protocol profile, final UUID vaultId, final UvfMetadataPayload vaultMetadata, final LoginCallback prompt) throws ConnectionCanceledException { - this(profile, vaultId, new Path(vaultMetadata.storage().getDefaultPath(), EnumSet.of(Path.Type.directory, Path.Type.volume), - new PathAttributes().setDisplayname(vaultMetadata.storage().getNickname())), vaultMetadata, prompt); - } - - public HubUVFVault(final Protocol profile, final UUID vaultId, final Path bucket, final UvfMetadataPayload vaultMetadata, final LoginCallback prompt) throws ConnectionCanceledException { - super(bucket); + super(new Path(vaultMetadata.storage().getDefaultPath(), EnumSet.of(Path.Type.directory, Path.Type.volume), + new PathAttributes().setDisplayname(vaultMetadata.storage().getNickname()))); this.vaultId = vaultId; this.vaultMetadata = vaultMetadata; this.login = prompt; diff --git a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java index 365de03f..8e1b6017 100644 --- a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java +++ b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java @@ -249,7 +249,7 @@ public void test03AddVault(final HubTestConfig config) throws Exception { .enabled(true) .maxWotDepth(null)); final HubUVFVault cryptomator = new HubUVFVault(hubSession.getFeature(ProtocolFactory.class).forName(location.getProfile()), - vaultId, bucket, vaultMetadata, new DisabledLoginCallback()); + vaultId, vaultMetadata, new DisabledLoginCallback()); cryptomator.create(hubSession, location.getIdentifier(), new VaultCredentials(StringUtils.EMPTY)); final AttributedList vaults = hubSession.getFeature(ListService.class).list(Home.root(), new DisabledListProgressListener()); diff --git a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java index 05760b18..97099646 100644 --- a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java +++ b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java @@ -81,7 +81,7 @@ public void testHubWorkflow(final HubTestConfig config) throws Exception { .enabled(true) .maxWotDepth(3)); final HubUVFVault cryptomator = new HubUVFVault(hubSession.getFeature(ProtocolFactory.class).forName(location.getProfile()), - vaultId, bucket, vaultMetadata, new DisabledLoginCallback()); + vaultId, vaultMetadata, new DisabledLoginCallback()); cryptomator.create(hubSession, location.getIdentifier(), new VaultCredentials(StringUtils.EMPTY)); checkNumberOfVaults(hubSession, config, vaultId, 0, 0, 1, 0, 0); From a696a75f74f38c438790e233626a9a8cbcd3af35 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 20 Oct 2025 08:34:13 +0200 Subject: [PATCH 125/133] Use service loader. --- .../katta/protocols/s3/S3AssumeRoleProtocol.java | 16 ---------------- .../cloud/katta/testsetup/AbstractHubTest.java | 16 ++++------------ 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleProtocol.java b/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleProtocol.java index ca19cea8..de264e84 100644 --- a/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleProtocol.java +++ b/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleProtocol.java @@ -16,7 +16,6 @@ public class S3AssumeRoleProtocol extends S3Protocol { // Token exchange public static final String OAUTH_TOKENEXCHANGE = "oauth.tokenexchange"; - public static final String OAUTH_TOKENEXCHANGE_VAULT = "oauth.tokenexchange.vault"; // STS assume role with web identity resource name public static final String S3_ASSUMEROLE_ROLEARN_WEBIDENTITY = Profile.STS_ROLE_ARN_PROPERTY_KEY; @@ -25,16 +24,6 @@ public class S3AssumeRoleProtocol extends S3Protocol { public static final String S3_ASSUMEROLE_ROLEARN_TAG = "s3.assumerole.rolearn.tag"; public static final String S3_ASSUMEROLE_ROLEARN_CREATE_BUCKET = "s3.assumerole.rolearn.createbucket"; - private final String authorization; - - public S3AssumeRoleProtocol() { - this("AuthorizationCode"); - } - - public S3AssumeRoleProtocol(final String authorization) { - this.authorization = authorization; - } - @Override public String getIdentifier() { return "s3-assumerole"; @@ -50,11 +39,6 @@ public String getPrefix() { return String.format("%s.%s", S3AssumeRoleProtocol.class.getPackage().getName(), "S3AssumeRole"); } - @Override - public String getAuthorization() { - return authorization; - } - @Override @SuppressWarnings("unchecked") public T getFeature(final Class type) { diff --git a/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java b/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java index 3cb42015..2a67e164 100644 --- a/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java +++ b/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java @@ -9,6 +9,7 @@ import ch.cyberduck.core.preferences.Preferences; import ch.cyberduck.core.preferences.PreferencesFactory; import ch.cyberduck.core.profiles.LocalProfilesFinder; +import ch.cyberduck.core.serviceloader.AnnotationAutoServiceLoader; import ch.cyberduck.core.ssl.DefaultX509KeyManager; import ch.cyberduck.core.ssl.DefaultX509TrustManager; import ch.cyberduck.core.vault.VaultRegistryFactory; @@ -28,11 +29,9 @@ import cloud.katta.core.DeviceSetupCallback; import cloud.katta.model.AccountKeyAndDeviceName; -import cloud.katta.protocols.hub.HubProtocol; import cloud.katta.protocols.hub.HubSession; import cloud.katta.protocols.hub.HubUVFVault; import cloud.katta.protocols.hub.HubVaultRegistry; -import cloud.katta.protocols.s3.S3AssumeRoleProtocol; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -165,17 +164,10 @@ private static String staticSetupCode() { protected static HubSession setupConnection(final HubTestConfig.Setup setup) throws Exception { final ProtocolFactory factory = ProtocolFactory.get(); - // ProtocolFactory.get() is static, the profiles contains OAuth token URL, leads to invalid grant exceptions when this changes during class loading lifetime (e.g. if the same storage profile ID is deployed to the LOCAL and the HYBRID hub). - for(final Protocol protocol : factory.find()) { - if(protocol instanceof Profile) { - factory.unregister((Profile) protocol); - } - } // Register parent protocol definitions - factory.register( - new HubProtocol(), - new S3AssumeRoleProtocol("PasswordGrant") - ); + for(Protocol p : new AnnotationAutoServiceLoader().load(Protocol.class)) { + factory.register(p); + } // Load bundled profiles factory.load(new LocalProfilesFinder(factory, new Local(AbstractHubTest.class.getResource("/").toURI().getPath()))); assertNotNull(factory.forName("hub")); From 746b1cb16109cfdeaea573a89645065681fe4986 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 20 Oct 2025 08:36:20 +0200 Subject: [PATCH 126/133] Fail on missing configuration. --- .../s3/STSChainedAssumeRoleRequestInterceptor.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java b/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java index 277ecbc0..fb268218 100644 --- a/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java +++ b/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java @@ -74,8 +74,12 @@ public TemporaryAccessTokens assumeRoleWithWebIdentity(final OAuthTokens oauth, if(StringUtils.isNotBlank(settings.getProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_TAG))) { log.debug("Assume role with temporary credentials {}", tokens); // Assume role with previously obtained temporary access token + final String key = HostPreferencesFactory.get(bookmark).getProperty("s3.assumerole.rolearn.tag.vaultid.key"); + if(null == key) { + throw new InteroperabilityException("No vault tag key set"); + } return super.assumeRole(credentials.setTokens(tokens) - .setProperty(Profile.STS_TAGS_PROPERTY_KEY, String.format("%s=%s", HostPreferencesFactory.get(bookmark).getProperty("s3.assumerole.rolearn.tag.vaultid.key"), vaultId)), + .setProperty(Profile.STS_TAGS_PROPERTY_KEY, String.format("%s=%s", key, vaultId)), settings.getProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_TAG)); } log.warn("No vault tag set. Skip assuming role with temporary credentials {} for {}", tokens, bookmark); From 7bef746e9430429a76173d30ad610851527469a1 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 20 Oct 2025 08:49:12 +0200 Subject: [PATCH 127/133] Move to class. --- .../protocols/s3/STSChainedAssumeRoleRequestInterceptor.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java b/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java index fb268218..dcb7abe5 100644 --- a/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java +++ b/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java @@ -57,6 +57,11 @@ public STSChainedAssumeRoleRequestInterceptor(final OAuth2RequestInterceptor oau this.vaultId = HostPreferencesFactory.get(host).getProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_VAULT); } + @Override + protected String getWebIdentityToken(final OAuthTokens oauth) { + return oauth.getAccessToken(); + } + /** * Assume role with previously obtained temporary access token * From 16e832bdabffb6d98352f7d51925f4c101aa2145 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 20 Oct 2025 08:49:27 +0200 Subject: [PATCH 128/133] Move to class. --- .../java/cloud/katta/protocols/s3/S3AssumeRoleSession.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleSession.java b/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleSession.java index e00f609a..63885886 100644 --- a/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleSession.java +++ b/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleSession.java @@ -41,12 +41,7 @@ protected S3CredentialsStrategy configureCredentialsStrategy(final HttpClientBui final OAuth2RequestInterceptor oauth = host.getProtocol().getFeature(OAuth2RequestInterceptor.class); log.debug("Register interceptor {}", oauth); configuration.addInterceptorLast(oauth); - final STSRequestInterceptor sts = new STSChainedAssumeRoleRequestInterceptor(oauth, host, trust, key, prompt) { - @Override - protected String getWebIdentityToken(final OAuthTokens oauth) { - return oauth.getAccessToken(); - } - }; + final STSRequestInterceptor sts = new STSChainedAssumeRoleRequestInterceptor(hub, vaultId, host, trust, key, prompt); log.debug("Register interceptor {}", sts); configuration.addInterceptorLast(sts); return sts; From b03cde13fc28cb1b514dc4639e042d1bc6a34e0f Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 20 Oct 2025 08:59:15 +0200 Subject: [PATCH 129/133] Remove profile properties to be configured from storage profile configuration instead. --- .../StorageProfileDtoWrapperDeserializer.java | 1 + ... S3 Storage Configuration.cyberduckprofile | 25 ------------------- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java b/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java index 62f0452e..5f16e974 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java @@ -64,6 +64,7 @@ public List listForKey(final String key) { properties.add(String.format("%s=%s", S3AssumeRoleProtocol.S3_ASSUMEROLE_DURATIONSECONDS, dto.getStsDurationSeconds().toString())); } } + properties.add("s3.assumerole.rolearn.tag.vaultid.key=Vault"); log.debug("Return properties {} from {}", properties, dto); return (List) properties; case REGIONS_KEY: diff --git a/hub/src/test/resources/Katta S3 Storage Configuration.cyberduckprofile b/hub/src/test/resources/Katta S3 Storage Configuration.cyberduckprofile index 8acfb998..594b4b7a 100644 --- a/hub/src/test/resources/Katta S3 Storage Configuration.cyberduckprofile +++ b/hub/src/test/resources/Katta S3 Storage Configuration.cyberduckprofile @@ -14,30 +14,5 @@ Description S3 Storage Configuration using OAuth Credentials from Katta Server - Scheme - http - Default Port - 80 - Scopes - - openid - - OAuth Redirect Url - ${oauth.handler.scheme}:oauth - OAuth Configurable - - OAuth Client Secret - - Username Configurable - - Password Configurable - - Authorization - AuthorizationCode - Properties - - s3.assumerole.rolearn.tag.vaultid.key - Vault - From 8bed1ae0c7da1806ee8a68d4cc4a799de40bc52e Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 20 Oct 2025 10:37:16 +0200 Subject: [PATCH 130/133] Inject reference to Hub connection/Oauth explicitly. Load storage session without factory. --- .../katta/protocols/hub/HubAwareProfile.java | 20 +----- .../cloud/katta/protocols/hub/HubSession.java | 55 ++++----------- .../katta/protocols/hub/HubUVFVault.java | 22 +----- .../protocols/hub/HubVaultListService.java | 22 +++--- .../protocols/s3/S3AssumeRoleSession.java | 23 +++++-- ...TSChainedAssumeRoleRequestInterceptor.java | 67 ++++++------------- .../cloud/katta/workflows/VaultService.java | 14 ++++ .../katta/workflows/VaultServiceImpl.java | 35 ++++++++++ .../core/AbstractHubSynchronizeTest.java | 4 +- .../workflows/AbstractHubWorkflowTest.java | 3 +- 10 files changed, 116 insertions(+), 149 deletions(-) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubAwareProfile.java b/hub/src/main/java/cloud/katta/protocols/hub/HubAwareProfile.java index fb00cf2f..a15bc50d 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubAwareProfile.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubAwareProfile.java @@ -6,7 +6,6 @@ import ch.cyberduck.core.Profile; import ch.cyberduck.core.Protocol; -import ch.cyberduck.core.oauth.OAuth2RequestInterceptor; import cloud.katta.client.model.ConfigDto; import cloud.katta.model.StorageProfileDtoWrapper; @@ -15,24 +14,7 @@ public final class HubAwareProfile extends Profile { - private final HubSession hub; - private final OAuth2RequestInterceptor oauth; - - public HubAwareProfile(final HubSession hub, final OAuth2RequestInterceptor oauth, final Protocol parent, final ConfigDto configDto, final StorageProfileDtoWrapper storageProfile) { + public HubAwareProfile(final Protocol parent, final ConfigDto configDto, final StorageProfileDtoWrapper storageProfile) { super(parent, new HubConfigDtoDeserializer(configDto, new StorageProfileDtoWrapperDeserializer(storageProfile))); - this.hub = hub; - this.oauth = oauth; - } - - @SuppressWarnings("unchecked") - @Override - public T getFeature(final Class type) { - if(type == HubSession.class) { - return (T) hub; - } - if(type == OAuth2RequestInterceptor.class) { - return (T) oauth; - } - return super.getFeature(type); } } diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index efe328d8..ba721907 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -44,22 +44,18 @@ import org.apache.logging.log4j.Logger; import java.io.InputStream; -import java.util.List; import java.util.Map; import java.util.Optional; import cloud.katta.client.ApiException; import cloud.katta.client.HubApiClient; import cloud.katta.client.api.ConfigResourceApi; -import cloud.katta.client.api.StorageProfileResourceApi; import cloud.katta.client.api.UsersResourceApi; import cloud.katta.client.model.ConfigDto; -import cloud.katta.client.model.StorageProfileDto; import cloud.katta.client.model.UserDto; import cloud.katta.core.DeviceSetupCallback; import cloud.katta.crypto.DeviceKeys; import cloud.katta.crypto.UserKeys; -import cloud.katta.model.StorageProfileDtoWrapper; import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService; import cloud.katta.protocols.hub.serializer.HubConfigDtoDeserializer; import cloud.katta.workflows.DeviceKeysServiceImpl; @@ -82,11 +78,6 @@ public class HubSession extends HttpSession { */ private final Scheduler access = new HubGrantAccessSchedulerService(this, keychain); - /** - * Registered storage profiles - */ - private final ProtocolFactory storage = new ProtocolFactory(); - /** * Interceptor for OpenID connect flow */ @@ -171,36 +162,14 @@ public void login(final LoginCallback prompt, final CancelCallback cancel) throw log.warn("Failure {} decoding JWT {}", e, credentials.getOauth().getIdToken()); throw new LoginCanceledException(e); } - try { - final UserDto me = this.getMe(); - log.debug("Retrieved user {}", me); - // Ensure device key is available - final DeviceSetupCallback setup = prompt.getFeature(DeviceSetupCallback.class); - log.debug("Configured with setup prompt {}", setup); - final UserKeys userKeys = this.getUserKeys(setup); - log.debug("Retrieved user keys {}", userKeys); - final List storageProfileDtos = new StorageProfileResourceApi(client).apiStorageprofileGet(false); - for(StorageProfileDto storageProfileDto : storageProfileDtos) { - final StorageProfileDtoWrapper storageProfile = StorageProfileDtoWrapper.coerce(storageProfileDto); - log.debug("Read storage profile {}", storageProfile); - switch(storageProfile.getProtocol()) { - case S3: - case S3_STS: - final Profile profile = new HubAwareProfile(this, authorizationService, - ProtocolFactory.get().forType(ProtocolFactory.get().find(ProtocolFactory.BUNDLED_PROFILE_PREDICATE), Protocol.Type.s3), - config, storageProfile); - log.debug("Register profile {}", profile); - storage.register(profile); - break; - default: - throw new InteroperabilityException(String.format("Unsupported storage configuration %s", storageProfile.getProtocol().name())); - } - } - vaults = new HubVaultListService(storage, this, prompt); - } - catch(ApiException e) { - throw new HubExceptionMappingService().map(e); - } + final UserDto me = this.getMe(); + log.debug("Retrieved user {}", me); + // Ensure device key is available + final DeviceSetupCallback setup = prompt.getFeature(DeviceSetupCallback.class); + log.debug("Configured with setup prompt {}", setup); + final UserKeys userKeys = this.getUserKeys(setup); + log.debug("Retrieved user keys {}", userKeys); + vaults = new HubVaultListService(this, prompt); } private UserKeys pair(final DeviceSetupCallback setup) throws BackgroundException { @@ -246,6 +215,10 @@ public UserDto getMe() throws BackgroundException { } } + public ConfigDto getConfig() { + return config; + } + /** * * @return Destroyed keys after login @@ -387,8 +360,8 @@ public void preflight(final Path file) throws BackgroundException { if(type == CredentialsConfigurator.class) { return (T) new HubOAuthTokensCredentialsConfigurator(keychain, host); } - if(type == ProtocolFactory.class) { - return (T) storage; + if(type == OAuth2RequestInterceptor.class) { + return (T) authorizationService; } return super._getFeature(type); } diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java index 409ad64f..c011f42d 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java @@ -4,7 +4,6 @@ package cloud.katta.protocols.hub; -import ch.cyberduck.core.CredentialsConfigurator; import ch.cyberduck.core.DisabledCancelCallback; import ch.cyberduck.core.DisabledHostKeyCallback; import ch.cyberduck.core.Host; @@ -13,13 +12,10 @@ import ch.cyberduck.core.PasswordCallback; import ch.cyberduck.core.Path; import ch.cyberduck.core.PathAttributes; -import ch.cyberduck.core.Protocol; import ch.cyberduck.core.Session; -import ch.cyberduck.core.SessionFactory; import ch.cyberduck.core.cryptomator.ContentWriter; import ch.cyberduck.core.cryptomator.UVFVault; import ch.cyberduck.core.exception.BackgroundException; -import ch.cyberduck.core.exception.ConnectionCanceledException; import ch.cyberduck.core.exception.InteroperabilityException; import ch.cyberduck.core.exception.UnsupportedException; import ch.cyberduck.core.features.AttributesFinder; @@ -29,8 +25,6 @@ import ch.cyberduck.core.preferences.PreferencesFactory; import ch.cyberduck.core.proxy.ProxyFactory; import ch.cyberduck.core.s3.S3Session; -import ch.cyberduck.core.ssl.X509KeyManager; -import ch.cyberduck.core.ssl.X509TrustManager; import ch.cyberduck.core.transfer.TransferStatus; import ch.cyberduck.core.vault.VaultCredentials; import ch.cyberduck.core.vault.VaultUnlockCancelException; @@ -52,14 +46,11 @@ import cloud.katta.crypto.UserKeys; import cloud.katta.crypto.uvf.UvfMetadataPayload; import cloud.katta.crypto.uvf.UvfMetadataPayloadPasswordCallback; -import cloud.katta.crypto.uvf.VaultMetadataJWEBackendDto; import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService; import cloud.katta.protocols.s3.S3AssumeRoleProtocol; import com.fasterxml.jackson.core.JsonProcessingException; import com.nimbusds.jose.JOSEException; -import static cloud.katta.protocols.s3.S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_VAULT; - /** * Unified vault format (UVF) implementation for Katta */ @@ -77,25 +68,18 @@ public class HubUVFVault extends UVFVault { /** * - * @param profile Storage provider configuration + * @param storage Storage connection * @param vaultId Vault Id * @param vaultMetadata Vault UVF metadata * @param prompt Login prompt to access storage */ - public HubUVFVault(final Protocol profile, final UUID vaultId, final UvfMetadataPayload vaultMetadata, final LoginCallback prompt) throws ConnectionCanceledException { + public HubUVFVault(final Session storage, final UUID vaultId, final UvfMetadataPayload vaultMetadata, final LoginCallback prompt) { super(new Path(vaultMetadata.storage().getDefaultPath(), EnumSet.of(Path.Type.directory, Path.Type.volume), new PathAttributes().setDisplayname(vaultMetadata.storage().getNickname()))); + this.storage = storage; this.vaultId = vaultId; this.vaultMetadata = vaultMetadata; this.login = prompt; - final VaultMetadataJWEBackendDto vaultStorageMetadata = vaultMetadata.storage(); - final HubSession hub = profile.getFeature(HubSession.class); - log.debug("Loaded profile {} for UVF metadata {}", profile, vaultMetadata); - final Host storageProvider = new Host(profile, hub.getFeature(CredentialsConfigurator.class).reload().configure(hub.getHost()) - .setUsername(vaultStorageMetadata.getUsername()).setPassword(vaultStorageMetadata.getPassword())).withRegion(vaultStorageMetadata.getRegion()); - storageProvider.setProperty(OAUTH_TOKENEXCHANGE_VAULT, vaultId.toString()); - log.debug("Configured {} for vault {}", storageProvider, this); - this.storage = SessionFactory.create(storageProvider, hub.getFeature(X509TrustManager.class), hub.getFeature(X509KeyManager.class)); } /** diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java index dfb13e79..c3963ade 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubVaultListService.java @@ -10,8 +10,7 @@ import ch.cyberduck.core.LocaleFactory; import ch.cyberduck.core.LoginCallback; import ch.cyberduck.core.Path; -import ch.cyberduck.core.Protocol; -import ch.cyberduck.core.ProtocolFactory; +import ch.cyberduck.core.Session; import ch.cyberduck.core.exception.AccessDeniedException; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.exception.NotfoundException; @@ -37,15 +36,10 @@ public class HubVaultListService implements ListService { private static final Logger log = LogManager.getLogger(HubVaultListService.class); - /** - * Storage profiles - */ - private final ProtocolFactory storage; private final HubSession session; private final LoginCallback prompt; - public HubVaultListService(final ProtocolFactory storage, final HubSession session, final LoginCallback prompt) { - this.storage = storage; + public HubVaultListService(final HubSession session, final LoginCallback prompt) { this.session = session; this.prompt = prompt; } @@ -54,6 +48,7 @@ public HubVaultListService(final ProtocolFactory storage, final HubSession sessi public AttributedList list(final Path directory, final ListProgressListener listener) throws BackgroundException { if(directory.isRoot()) { try { + final VaultServiceImpl vaultService = new VaultServiceImpl(session); final VaultRegistry registry = session.getRegistry(); final AttributedList vaults = new AttributedList<>(); for(final VaultDto vaultDto : new VaultResourceApi(session.getClient()).apiVaultsAccessibleGet(null)) { @@ -64,11 +59,10 @@ public AttributedList list(final Path directory, final ListProgressListene log.debug("Load vault {}", vaultDto); try { // Find storage configuration in vault metadata - final VaultServiceImpl vaultService = new VaultServiceImpl(session); final DeviceSetupCallback setup = prompt.getFeature(DeviceSetupCallback.class); final UvfMetadataPayload vaultMetadata = vaultService.getVaultMetadataJWE(vaultDto.getId(), session.getUserKeys(setup)); - final Protocol profile = storage.forName(vaultMetadata.storage().getProvider()); - final HubUVFVault vault = new HubUVFVault(profile, vaultDto.getId(), vaultMetadata, prompt); + final Session storage = vaultService.getVaultStorageSession(session, vaultDto.getId(), vaultMetadata); + final HubUVFVault vault = new HubUVFVault(storage, vaultDto.getId(), vaultMetadata, prompt); try { registry.add(vault.load(session, prompt)); vaults.add(vault.getHome()); @@ -76,7 +70,6 @@ public AttributedList list(final Path directory, final ListProgressListene } catch(VaultUnlockCancelException e) { log.warn("Skip vault {} with failure {} loading", vaultDto, e); - continue; } } catch(ApiException e) { @@ -86,7 +79,10 @@ public AttributedList list(final Path directory, final ListProgressListene } throw e; } - catch(AccessException | SecurityFailure e) { + catch(AccessException e) { + log.warn("Skip vault {} with access failure {}", vaultDto, e); + } + catch(SecurityFailure e) { throw new AccessDeniedException(e.getMessage(), e); } } diff --git a/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleSession.java b/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleSession.java index 63885886..e7e4db43 100644 --- a/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleSession.java +++ b/hub/src/main/java/cloud/katta/protocols/s3/S3AssumeRoleSession.java @@ -6,7 +6,6 @@ import ch.cyberduck.core.Host; import ch.cyberduck.core.LoginCallback; -import ch.cyberduck.core.OAuthTokens; import ch.cyberduck.core.exception.LoginCanceledException; import ch.cyberduck.core.oauth.OAuth2RequestInterceptor; import ch.cyberduck.core.s3.S3CredentialsStrategy; @@ -19,11 +18,25 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.util.UUID; + +import cloud.katta.protocols.hub.HubSession; + public class S3AssumeRoleSession extends S3Session { private static final Logger log = LogManager.getLogger(S3AssumeRoleSession.class); - public S3AssumeRoleSession(final Host host, final X509TrustManager trust, final X509KeyManager key) { - super(host, trust, key); + private final HubSession hub; + /** + * Shared OAuth tokens + */ + private final OAuth2RequestInterceptor oauth; + private final UUID vaultId; + + public S3AssumeRoleSession(final HubSession hub, final UUID vaultId, final Host host) { + super(host, hub.getFeature(X509TrustManager.class), hub.getFeature(X509KeyManager.class)); + this.hub = hub; + this.oauth = hub.getFeature(OAuth2RequestInterceptor.class); + this.vaultId = vaultId; } /** @@ -37,11 +50,9 @@ public S3AssumeRoleSession(final Host host, final X509TrustManager trust, final @Override protected S3CredentialsStrategy configureCredentialsStrategy(final HttpClientBuilder configuration, final LoginCallback prompt) throws LoginCanceledException { if(host.getProtocol().isOAuthConfigurable()) { - // Shared OAuth tokens - final OAuth2RequestInterceptor oauth = host.getProtocol().getFeature(OAuth2RequestInterceptor.class); log.debug("Register interceptor {}", oauth); configuration.addInterceptorLast(oauth); - final STSRequestInterceptor sts = new STSChainedAssumeRoleRequestInterceptor(hub, vaultId, host, trust, key, prompt); + final STSRequestInterceptor sts = new STSChainedAssumeRoleRequestInterceptor(hub, oauth, vaultId, host, trust, key, prompt); log.debug("Register interceptor {}", sts); configuration.addInterceptorLast(sts); return sts; diff --git a/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java b/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java index dcb7abe5..f585749e 100644 --- a/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java +++ b/hub/src/main/java/cloud/katta/protocols/s3/STSChainedAssumeRoleRequestInterceptor.java @@ -10,7 +10,7 @@ import ch.cyberduck.core.Profile; import ch.cyberduck.core.TemporaryAccessTokens; import ch.cyberduck.core.exception.BackgroundException; -import ch.cyberduck.core.exception.LoginFailureException; +import ch.cyberduck.core.exception.InteroperabilityException; import ch.cyberduck.core.oauth.OAuth2RequestInterceptor; import ch.cyberduck.core.preferences.HostPreferencesFactory; import ch.cyberduck.core.preferences.PreferencesReader; @@ -22,17 +22,13 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.util.Arrays; -import java.util.List; +import java.util.UUID; import cloud.katta.client.ApiException; import cloud.katta.client.api.StorageResourceApi; import cloud.katta.client.model.AccessTokenResponse; import cloud.katta.protocols.hub.HubSession; import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService; -import com.auth0.jwt.JWT; -import com.auth0.jwt.exceptions.JWTDecodeException; -import com.auth0.jwt.interfaces.DecodedJWT; /** * Assume role with temporary credentials obtained using OIDC token from security token service (STS) @@ -46,15 +42,17 @@ public class STSChainedAssumeRoleRequestInterceptor extends STSAssumeRoleWithWeb */ private static final String OIDC_AUTHORIZED_PARTY = "azp"; + private final HubSession hub; private final Host bookmark; - private final String vaultId; + private final UUID vaultId; - public STSChainedAssumeRoleRequestInterceptor(final OAuth2RequestInterceptor oauth, final Host host, + public STSChainedAssumeRoleRequestInterceptor(final HubSession hub, final OAuth2RequestInterceptor oauth, final UUID vaultId, final Host host, final X509TrustManager trust, final X509KeyManager key, final LoginCallback prompt) { super(oauth, host, trust, key, prompt); + this.hub = hub; this.bookmark = host; - this.vaultId = HostPreferencesFactory.get(host).getProperty(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_VAULT); + this.vaultId = vaultId; } @Override @@ -95,50 +93,25 @@ public TemporaryAccessTokens assumeRoleWithWebIdentity(final OAuthTokens oauth, * Perform OAuth 2.0 Token Exchange * * @return New tokens - * @see S3AssumeRoleProtocol#OAUTH_TOKENEXCHANGE_VAULT */ private OAuthTokens tokenExchange(final OAuthTokens tokens) throws BackgroundException { final PreferencesReader settings = HostPreferencesFactory.get(bookmark); if(settings.getBoolean(S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE)) { - if(this.isTokenExchangeRequired(tokens)) { - log.info("Exchange tokens for {}", bookmark); - final HubSession hub = bookmark.getProtocol().getFeature(HubSession.class); - log.debug("Exchange token with hub {}", hub); - final StorageResourceApi api = new StorageResourceApi(hub.getClient()); - try { - final AccessTokenResponse tokenExchangeResponse = api.apiStorageS3TokenPost(vaultId); - // N.B. token exchange with Id token does not work! - final OAuthTokens exchanged = new OAuthTokens(tokenExchangeResponse.getAccessToken(), - tokenExchangeResponse.getRefreshToken(), - tokenExchangeResponse.getExpiresIn() != null ? System.currentTimeMillis() + tokenExchangeResponse.getExpiresIn() * 1000 : null); - log.debug("Received exchanged token {} for {}", exchanged, bookmark); - return exchanged; - } - catch(ApiException e) { - throw new HubExceptionMappingService().map(e); - } + log.info("Exchange tokens {} for vault {}", tokens, vaultId); + final StorageResourceApi api = new StorageResourceApi(hub.getClient()); + try { + final AccessTokenResponse tokenExchangeResponse = api.apiStorageS3TokenPost(vaultId.toString()); + // N.B. token exchange with Id token does not work! + final OAuthTokens exchanged = new OAuthTokens(tokenExchangeResponse.getAccessToken(), + tokenExchangeResponse.getRefreshToken(), + tokenExchangeResponse.getExpiresIn() != null ? System.currentTimeMillis() + tokenExchangeResponse.getExpiresIn() * 1000 : null); + log.debug("Received exchanged token {} for {}", exchanged, bookmark); + return exchanged; } - } - return tokens; - } - - private boolean isTokenExchangeRequired(final OAuthTokens tokens) throws BackgroundException { - final String accessToken = tokens.getAccessToken(); - try { - final DecodedJWT jwt = JWT.decode(accessToken); - final List auds = jwt.getAudience(); - final String azp = jwt.getClaim(OIDC_AUTHORIZED_PARTY).asString(); - log.debug("Decoded JWT {} with audience {} and azp {}", jwt, Arrays.toString(auds.toArray()), azp); - final boolean audNotUnique = 1 != auds.size(); // either multiple audiences or none - // do exchange if aud is not unique or azp is not equal to aud - if(audNotUnique || !auds.get(0).equals(azp)) { - log.debug("None or multiple audiences found {} or audience differs from azp {}", Arrays.toString(auds.toArray()), azp); - return true; + catch(ApiException e) { + throw new HubExceptionMappingService().map(e); } } - catch(JWTDecodeException e) { - throw new LoginFailureException("Invalid JWT or JSON format in authentication token", e); - } - return false; + return tokens; } } diff --git a/hub/src/main/java/cloud/katta/workflows/VaultService.java b/hub/src/main/java/cloud/katta/workflows/VaultService.java index aa5b12ea..14c560f1 100644 --- a/hub/src/main/java/cloud/katta/workflows/VaultService.java +++ b/hub/src/main/java/cloud/katta/workflows/VaultService.java @@ -4,6 +4,8 @@ package cloud.katta.workflows; +import ch.cyberduck.core.Session; + import java.util.UUID; import cloud.katta.client.ApiException; @@ -11,6 +13,7 @@ import cloud.katta.crypto.uvf.UvfAccessTokenPayload; import cloud.katta.crypto.uvf.UvfMetadataPayload; import cloud.katta.model.StorageProfileDtoWrapper; +import cloud.katta.protocols.hub.HubSession; import cloud.katta.workflows.exceptions.AccessException; import cloud.katta.workflows.exceptions.SecurityFailure; @@ -46,4 +49,15 @@ public interface VaultService { * @return Storage profile */ StorageProfileDtoWrapper getVaultStorageProfile(UvfMetadataPayload metadataPayload) throws ApiException, AccessException, SecurityFailure; + + /** + * Get storage session for vault + * + * @param session Hub Connection + * @param vaultId Vault ID + * @param metadataPayload Vault metadata including storage configuration + * @return Storage Session + * @throws AccessException Unsupported storage configuration found for vault + */ + Session getVaultStorageSession(HubSession session, UUID vaultId, UvfMetadataPayload metadataPayload) throws ApiException, AccessException; } diff --git a/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java b/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java index 82af08b5..1c1daed4 100644 --- a/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java +++ b/hub/src/main/java/cloud/katta/workflows/VaultServiceImpl.java @@ -4,6 +4,14 @@ package cloud.katta.workflows; +import ch.cyberduck.core.CredentialsConfigurator; +import ch.cyberduck.core.Host; +import ch.cyberduck.core.Protocol; +import ch.cyberduck.core.ProtocolFactory; +import ch.cyberduck.core.Session; +import ch.cyberduck.core.exception.InteroperabilityException; +import ch.cyberduck.core.exception.LoginCanceledException; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -18,8 +26,11 @@ import cloud.katta.crypto.UserKeys; import cloud.katta.crypto.uvf.UvfAccessTokenPayload; import cloud.katta.crypto.uvf.UvfMetadataPayload; +import cloud.katta.crypto.uvf.VaultMetadataJWEBackendDto; import cloud.katta.model.StorageProfileDtoWrapper; +import cloud.katta.protocols.hub.HubAwareProfile; import cloud.katta.protocols.hub.HubSession; +import cloud.katta.protocols.s3.S3AssumeRoleSession; import cloud.katta.workflows.exceptions.AccessException; import cloud.katta.workflows.exceptions.SecurityFailure; import com.fasterxml.jackson.core.JsonProcessingException; @@ -77,4 +88,28 @@ public StorageProfileDtoWrapper getVaultStorageProfile(final UvfMetadataPayload return StorageProfileDtoWrapper.coerce(storageProfileResourceApi .apiStorageprofileProfileIdGet(UUID.fromString(metadataPayload.storage().getProvider()))); } + + @Override + public Session getVaultStorageSession(final HubSession session, final UUID vaultId, final UvfMetadataPayload vaultMetadata) throws ApiException, AccessException { + final StorageProfileDtoWrapper vaultStorageProfile = this.getVaultStorageProfile(vaultMetadata); + switch(vaultStorageProfile.getProtocol()) { + case S3: + case S3_STS: + final VaultMetadataJWEBackendDto vaultStorageMetadata = vaultMetadata.storage(); + try { + final S3AssumeRoleSession storage = new S3AssumeRoleSession(session, vaultId, new Host(new HubAwareProfile( + ProtocolFactory.get().forType(ProtocolFactory.get().find(ProtocolFactory.BUNDLED_PROFILE_PREDICATE), Protocol.Type.s3), session.getConfig(), vaultStorageProfile), + session.getFeature(CredentialsConfigurator.class).reload().configure(session.getHost()) + .setUsername(vaultStorageMetadata.getUsername()).setPassword(vaultStorageMetadata.getPassword())).withRegion(vaultStorageMetadata.getRegion())); + log.debug("Configured {} for vault {}", storage, vaultId); + return storage; + } + catch(LoginCanceledException e) { + throw new AccessException(e); + } + default: + log.warn("Unsupported storage configuration {} for vault {}", vaultStorageProfile.getProtocol(), vaultId); + throw new AccessException(new InteroperabilityException()); + } + } } diff --git a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java index 8e1b6017..7101bf1e 100644 --- a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java +++ b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java @@ -12,7 +12,6 @@ import ch.cyberduck.core.ListService; import ch.cyberduck.core.OAuthTokens; import ch.cyberduck.core.Path; -import ch.cyberduck.core.ProtocolFactory; import ch.cyberduck.core.Session; import ch.cyberduck.core.SimplePathPredicate; import ch.cyberduck.core.UUIDRandomStringService; @@ -77,6 +76,7 @@ import cloud.katta.testsetup.AbstractHubTest; import cloud.katta.testsetup.HubTestConfig; import cloud.katta.testsetup.MethodIgnorableSource; +import cloud.katta.workflows.VaultServiceImpl; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; @@ -248,7 +248,7 @@ public void test03AddVault(final HubTestConfig config) throws Exception { .withAutomaticAccessGrant(new VaultMetadataJWEAutomaticAccessGrantDto() .enabled(true) .maxWotDepth(null)); - final HubUVFVault cryptomator = new HubUVFVault(hubSession.getFeature(ProtocolFactory.class).forName(location.getProfile()), + final HubUVFVault cryptomator = new HubUVFVault(new VaultServiceImpl(hubSession).getVaultStorageSession(hubSession, vaultId, vaultMetadata), vaultId, vaultMetadata, new DisabledLoginCallback()); cryptomator.create(hubSession, location.getIdentifier(), new VaultCredentials(StringUtils.EMPTY)); diff --git a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java index 97099646..6b3a9891 100644 --- a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java +++ b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java @@ -6,7 +6,6 @@ import ch.cyberduck.core.DisabledLoginCallback; import ch.cyberduck.core.Path; -import ch.cyberduck.core.ProtocolFactory; import ch.cyberduck.core.UUIDRandomStringService; import ch.cyberduck.core.vault.VaultCredentials; @@ -80,7 +79,7 @@ public void testHubWorkflow(final HubTestConfig config) throws Exception { .withAutomaticAccessGrant(new VaultMetadataJWEAutomaticAccessGrantDto() .enabled(true) .maxWotDepth(3)); - final HubUVFVault cryptomator = new HubUVFVault(hubSession.getFeature(ProtocolFactory.class).forName(location.getProfile()), + final HubUVFVault cryptomator = new HubUVFVault(new VaultServiceImpl(hubSession).getVaultStorageSession(hubSession, vaultId, vaultMetadata), vaultId, vaultMetadata, new DisabledLoginCallback()); cryptomator.create(hubSession, location.getIdentifier(), new VaultCredentials(StringUtils.EMPTY)); From 3dde8a655841596f5cf60578569b207c553e4269 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 24 Oct 2025 10:08:50 +0200 Subject: [PATCH 131/133] Remove option to save account key. --- .../core/DefaultDeviceSetupCallback.java | 6 ++-- .../katta/model/AccountKeyAndDeviceName.java | 10 ------ .../cloud/katta/protocols/hub/HubSession.java | 2 +- .../katta/workflows/UserKeysServiceImpl.java | 35 ++----------------- .../controller/DeviceSetupController.java | 9 +---- .../controller/FirstLoginController.java | 9 +---- 6 files changed, 7 insertions(+), 64 deletions(-) diff --git a/hub/src/main/java/cloud/katta/core/DefaultDeviceSetupCallback.java b/hub/src/main/java/cloud/katta/core/DefaultDeviceSetupCallback.java index 39be9dda..5db04af9 100644 --- a/hub/src/main/java/cloud/katta/core/DefaultDeviceSetupCallback.java +++ b/hub/src/main/java/cloud/katta/core/DefaultDeviceSetupCallback.java @@ -39,10 +39,9 @@ public AccountKeyAndDeviceName displayAccountKeyAndAskDeviceName(final Host book .passwordPlaceholder(accountKeyAndDeviceName.deviceName()) // Input device name .password(true) - .keychain(true) + .keychain(false) ); return new AccountKeyAndDeviceName() - .withAddToKeychain(input.isSaved()) .withDeviceName(input.getUsername()) .withAccountKey(input.getPassword()); } @@ -66,10 +65,9 @@ public AccountKeyAndDeviceName askForAccountKeyAndDeviceName(final Host bookmark .passwordPlaceholder(LocaleFactory.localizedString("Account Key", "Hub")) // Input account key .password(true) - .keychain(true) + .keychain(false) ); return new AccountKeyAndDeviceName() - .withAddToKeychain(input.isSaved()) .withDeviceName(input.getUsername()) .withAccountKey(input.getPassword()); } diff --git a/hub/src/main/java/cloud/katta/model/AccountKeyAndDeviceName.java b/hub/src/main/java/cloud/katta/model/AccountKeyAndDeviceName.java index 47534599..ceb5ed3c 100644 --- a/hub/src/main/java/cloud/katta/model/AccountKeyAndDeviceName.java +++ b/hub/src/main/java/cloud/katta/model/AccountKeyAndDeviceName.java @@ -7,7 +7,6 @@ public class AccountKeyAndDeviceName { private String accountKey; private String deviceName; - private boolean addToKeychain; public String accountKey() { return accountKey; @@ -17,10 +16,6 @@ public String deviceName() { return deviceName; } - public boolean addToKeychain() { - return addToKeychain; - } - public AccountKeyAndDeviceName withAccountKey(final String accountKey) { this.accountKey = accountKey; return this; @@ -30,9 +25,4 @@ public AccountKeyAndDeviceName withDeviceName(final String deviceName) { this.deviceName = deviceName; return this; } - - public AccountKeyAndDeviceName withAddToKeychain(final boolean addToKeychain) { - this.addToKeychain = addToKeychain; - return this; - } } diff --git a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java index ba721907..2f8b0615 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/HubSession.java @@ -176,7 +176,7 @@ private UserKeys pair(final DeviceSetupCallback setup) throws BackgroundExceptio try { final DeviceKeys deviceKeys = new DeviceKeysServiceImpl(keychain).getOrCreateDeviceKeys(host, setup); log.debug("Retrieved device keys {}", deviceKeys); - final UserKeys userKeys = new UserKeysServiceImpl(this, keychain).getOrCreateUserKeys(host, + final UserKeys userKeys = new UserKeysServiceImpl(this).getOrCreateUserKeys(host, this.getMe(), deviceKeys, setup); log.debug("Retrieved user keys {}", userKeys); return userKeys; diff --git a/hub/src/main/java/cloud/katta/workflows/UserKeysServiceImpl.java b/hub/src/main/java/cloud/katta/workflows/UserKeysServiceImpl.java index 5fc09c71..e2df5bc1 100644 --- a/hub/src/main/java/cloud/katta/workflows/UserKeysServiceImpl.java +++ b/hub/src/main/java/cloud/katta/workflows/UserKeysServiceImpl.java @@ -4,11 +4,7 @@ package cloud.katta.workflows; -import ch.cyberduck.core.BookmarkNameProvider; import ch.cyberduck.core.Host; -import ch.cyberduck.core.PasswordStore; -import ch.cyberduck.core.PasswordStoreFactory; -import ch.cyberduck.core.exception.LocalAccessDeniedException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -44,24 +40,13 @@ public class UserKeysServiceImpl implements UserKeysService { private final UsersResourceApi usersResourceApi; private final DeviceResourceApi deviceResourceApi; - private final PasswordStore store; - public UserKeysServiceImpl(final HubSession hubSession) { - this(hubSession, PasswordStoreFactory.get()); - } - - public UserKeysServiceImpl(final HubSession hubSession, PasswordStore store) { - this(new UsersResourceApi(hubSession.getClient()), new DeviceResourceApi(hubSession.getClient()), store); + this(new UsersResourceApi(hubSession.getClient()), new DeviceResourceApi(hubSession.getClient())); } public UserKeysServiceImpl(final UsersResourceApi usersResourceApi, final DeviceResourceApi deviceResourceApi) { - this(usersResourceApi, deviceResourceApi, PasswordStoreFactory.get()); - } - - public UserKeysServiceImpl(final UsersResourceApi usersResourceApi, final DeviceResourceApi deviceResourceApi, PasswordStore store) { this.usersResourceApi = usersResourceApi; this.deviceResourceApi = deviceResourceApi; - this.store = store; } @Override @@ -92,11 +77,7 @@ public UserKeys getOrCreateUserKeys(final Host hub, final UserDto me, final Devi case 404: log.warn("Device keys from keychain not present in hub. Setting up existing device w/ Account Key for existing user keys."); // Setup existing device w/ Account Key (e.g. same device for multiple hubs) - final AccountKeyAndDeviceName input = prompt.askForAccountKeyAndDeviceName(hub, COMPUTER_NAME); - if(input.addToKeychain()) { - this.save(hub, me, input.accountKey()); - } - return this.recover(me, deviceKeyPair, input); + return this.recover(me, deviceKeyPair, prompt.askForAccountKeyAndDeviceName(hub, COMPUTER_NAME)); default: throw e; } @@ -115,23 +96,11 @@ else if(validate(me)) { final String accountKey = prompt.generateAccountKey(); final AccountKeyAndDeviceName input = prompt.displayAccountKeyAndAskDeviceName(hub, new AccountKeyAndDeviceName().withAccountKey(accountKey).withDeviceName(COMPUTER_NAME)); - if(input.addToKeychain()) { - this.save(hub, me, accountKey); - } return this.uploadDeviceKeys(input.deviceName(), this.uploadUserKeys(me, prompt.generateUserKeys(), accountKey), deviceKeyPair); } } - private void save(final Host hub, final UserDto me, final String accountKey) { - try { - store.addPassword(BookmarkNameProvider.toString(hub), me.getEmail(), accountKey); - } - catch(LocalAccessDeniedException ex) { - log.warn("Failure saving account key", ex); - } - } - private UserKeys recover(final UserDto me, final DeviceKeys deviceKeyPair, final AccountKeyAndDeviceName accountKeyAndDeviceName) throws ApiException, SecurityFailure { try { diff --git a/osx/src/main/java/cloud/katta/controller/DeviceSetupController.java b/osx/src/main/java/cloud/katta/controller/DeviceSetupController.java index 1be3bdb8..3f1f56a7 100644 --- a/osx/src/main/java/cloud/katta/controller/DeviceSetupController.java +++ b/osx/src/main/java/cloud/katta/controller/DeviceSetupController.java @@ -49,7 +49,7 @@ public NSAlert loadAlert() { .append(LocaleFactory.localizedString("Your Account Key is required to link this browser to your account.", "Hub")).toString()); alert.addButtonWithTitle(LocaleFactory.localizedString("Finish Setup", "Hub")); alert.addButtonWithTitle(LocaleFactory.localizedString("Cancel", "Alert")); - alert.setShowsSuppressionButton(true); + alert.setShowsSuppressionButton(false); alert.suppressionButton().setTitle(LocaleFactory.localizedString("Add to Keychain", "Login")); alert.suppressionButton().setState(PreferencesFactory.get().getBoolean("cryptomator.vault.keychain") ? NSCell.NSOnState : NSCell.NSOffState); return alert; @@ -94,13 +94,6 @@ public boolean validate(final int option) { return true; } - @Override - public void callback(final int returncode) { - if(SheetCallback.DEFAULT_OPTION == returncode) { - accountKeyAndDeviceName.withAddToKeychain(this.isSuppressed()); - } - } - @Action public void accountKeyFieldTextDidChange(final NSNotification sender) { accountKeyAndDeviceName.withAccountKey(StringUtils.trim(accountKeyField.stringValue())); diff --git a/osx/src/main/java/cloud/katta/controller/FirstLoginController.java b/osx/src/main/java/cloud/katta/controller/FirstLoginController.java index 0c3dad6d..ec19b027 100644 --- a/osx/src/main/java/cloud/katta/controller/FirstLoginController.java +++ b/osx/src/main/java/cloud/katta/controller/FirstLoginController.java @@ -49,7 +49,7 @@ public NSAlert loadAlert() { .append(LocaleFactory.localizedString("You can see a list of authorized apps on your profile page", "Hub")).toString()); alert.addButtonWithTitle(LocaleFactory.localizedString("Finish Setup", "Hub")); alert.addButtonWithTitle(LocaleFactory.localizedString("Cancel", "Alert")); - alert.setShowsSuppressionButton(true); + alert.setShowsSuppressionButton(false); alert.suppressionButton().setTitle(LocaleFactory.localizedString("Add to Keychain", "Login")); alert.suppressionButton().setState(PreferencesFactory.get().getBoolean("cryptomator.vault.keychain") ? NSCell.NSOnState : NSCell.NSOffState); return alert; @@ -94,13 +94,6 @@ public boolean validate(final int option) { return true; } - @Override - public void callback(final int returncode) { - if(SheetCallback.DEFAULT_OPTION == returncode) { - accountKeyAndDeviceName.withAddToKeychain(this.isSuppressed()); - } - } - @Action public void deviceNameFieldTextDidChange(final NSNotification sender) { accountKeyAndDeviceName.withDeviceName(StringUtils.trim(deviceNameField.stringValue())); From e33210fa70654217998b45c0dbcb420aad0aa514 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 24 Oct 2025 10:18:18 +0200 Subject: [PATCH 132/133] Add comment on usage of "Role Configurable" boolean for storage profile. --- .../hub/serializer/StorageProfileDtoWrapperDeserializer.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java b/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java index 5f16e974..8251dc16 100644 --- a/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java +++ b/hub/src/main/java/cloud/katta/protocols/hub/serializer/StorageProfileDtoWrapperDeserializer.java @@ -114,6 +114,8 @@ public Boolean booleanForKey(final String key) { } break; case ROLE_KEY_CONFIGURABLE_KEY: + // Indicates Role ARN is required for STS `AssumeRoleWithWebIdentity`. + // Determines usage of role grant flags when creating a new vault return dto.getStsRoleArn() != null; } return super.booleanForKey(key); From 9694c95520b9af7eb1282995b28d9f0aaa8e8c78 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 24 Oct 2025 10:28:07 +0200 Subject: [PATCH 133/133] Remove unused bucket name parameter. --- .../katta/core/AbstractHubSynchronizeTest.java | 2 +- .../cloud/katta/testsetup/AbstractHubTest.java | 14 +++++++------- .../java/cloud/katta/testsetup/HubTestConfig.java | 4 +--- .../katta/workflows/AbstractHubWorkflowTest.java | 2 +- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java index 7101bf1e..b0e1e89f 100644 --- a/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java +++ b/hub/src/test/java/cloud/katta/core/AbstractHubSynchronizeTest.java @@ -233,7 +233,7 @@ public void test03AddVault(final HubTestConfig config) throws Exception { log.info("Creating vault in {}", hubSession); final UUID vaultId = UUID.fromString(new UUIDRandomStringService().random()); - final Path bucket = new Path(null == config.vault.bucketName ? null == storageProfileWrapper.getBucketPrefix() ? "katta-test-" + vaultId : storageProfileWrapper.getBucketPrefix() + vaultId : config.vault.bucketName, + final Path bucket = new Path(null == storageProfileWrapper.getBucketPrefix() ? "katta-test-" + vaultId : storageProfileWrapper.getBucketPrefix() + vaultId, EnumSet.of(Path.Type.volume, Path.Type.directory)); final HubStorageLocationService.StorageLocation location = new HubStorageLocationService.StorageLocation(storageProfileWrapper.getId().toString(), storageProfileWrapper.getRegion(), storageProfileWrapper.getName()); diff --git a/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java b/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java index 2a67e164..3f192c86 100644 --- a/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java +++ b/hub/src/test/java/cloud/katta/testsetup/AbstractHubTest.java @@ -45,13 +45,13 @@ public abstract class AbstractHubTest extends VaultTest { } private static final HubTestConfig.VaultSpec minioSTSVaultConfig = new HubTestConfig.VaultSpec("MinIO STS", "732D43FA-3716-46C4-B931-66EA5405EF1C", - null, null, null, "eu-central-1"); + null, null, "eu-central-1"); private static final HubTestConfig.VaultSpec minioStaticVaultConfig = new HubTestConfig.VaultSpec("MinIO static", "71B910E0-2ECC-46DE-A871-8DB28549677E", - null, "minioadmin", "minioadmin", "us-east-1"); + "minioadmin", "minioadmin", "us-east-1"); private static final HubTestConfig.VaultSpec awsSTSVaultConfig = new HubTestConfig.VaultSpec("AWS STS", "844BD517-96D4-4787-BCFA-238E103149F6", - null, null, null, "eu-west-1"); + null, null, "eu-west-1"); private static final HubTestConfig.VaultSpec awsStaticVaultConfig = new HubTestConfig.VaultSpec("AWS static", "72736C19-283C-49D3-80A5-AB74B5202543", - null, PROPERTIES.get("handmade2.s3.amazonaws.com.username"), PROPERTIES.get("handmade2.s3.amazonaws.com.password"), "us-east-1" + PROPERTIES.get("handmade2.s3.amazonaws.com.username"), PROPERTIES.get("handmade2.s3.amazonaws.com.password"), "us-east-1" ); /** @@ -70,7 +70,7 @@ public abstract class AbstractHubTest extends VaultTest { } private static final Function argumentUnattendedLocalOnly = vs -> Arguments.of(Named.of( - String.format("%s %s (Bucket %s)", vs.storageProfileName, LOCAL.hubURL, vs.bucketName), + String.format("%s %s", vs.storageProfileName, LOCAL.hubURL), new HubTestConfig(LOCAL, vs))); @@ -85,7 +85,7 @@ public abstract class AbstractHubTest extends VaultTest { .withAdminConfig(new HubTestConfig.Setup.UserConfig("admin", "admin", staticSetupCode())) .withUserConfig(new HubTestConfig.Setup.UserConfig("alice", "asd", staticSetupCode())); private static final Function argumentAttendedLocalOnly = vs -> Arguments.of(Named.of( - String.format("%s %s (Bucket %s)", vs.storageProfileName, LOCAL_ATTENDED.hubURL, vs.bucketName), + String.format("%s %s", vs.storageProfileName, LOCAL_ATTENDED.hubURL), new HubTestConfig(LOCAL_ATTENDED, vs))); /** @@ -121,7 +121,7 @@ public abstract class AbstractHubTest extends VaultTest { } private static final Function argumentUnattendedHybrid = vs -> Arguments.of(Named.of( - String.format("%s %s (Bucket %s)", vs.storageProfileName, HYBRID.hubURL, vs.bucketName), + String.format("%s %s", vs.storageProfileName, HYBRID.hubURL), new HubTestConfig(HYBRID, vs))); diff --git a/hub/src/test/java/cloud/katta/testsetup/HubTestConfig.java b/hub/src/test/java/cloud/katta/testsetup/HubTestConfig.java index e12cd6bd..b1fb6df2 100644 --- a/hub/src/test/java/cloud/katta/testsetup/HubTestConfig.java +++ b/hub/src/test/java/cloud/katta/testsetup/HubTestConfig.java @@ -105,15 +105,13 @@ public String toString() { public static class VaultSpec { public final String storageProfileName; public final String storageProfileId; - public final String bucketName; public final String username; public final String password; public final String region; - public VaultSpec(final String storageProfileName, final String storageProfileId, final String bucketName, final String username, final String password, final String region) { + public VaultSpec(final String storageProfileName, final String storageProfileId, final String username, final String password, final String region) { this.storageProfileName = storageProfileName; this.storageProfileId = storageProfileId; - this.bucketName = bucketName; this.username = username; this.password = password; this.region = region; diff --git a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java index 6b3a9891..03d6991f 100644 --- a/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java +++ b/hub/src/test/java/cloud/katta/workflows/AbstractHubWorkflowTest.java @@ -64,7 +64,7 @@ public void testHubWorkflow(final HubTestConfig config) throws Exception { .filter(p -> p.getId().toString().equals(config.vault.storageProfileId.toLowerCase())).findFirst().get(); final UUID vaultId = UUID.fromString(new UUIDRandomStringService().random()); - final Path bucket = new Path(null == config.vault.bucketName ? null == storageProfileWrapper.getBucketPrefix() ? "katta-test-" + vaultId : storageProfileWrapper.getBucketPrefix() + vaultId : config.vault.bucketName, + final Path bucket = new Path(null == storageProfileWrapper.getBucketPrefix() ? "katta-test-" + vaultId : storageProfileWrapper.getBucketPrefix() + vaultId, EnumSet.of(Path.Type.volume, Path.Type.directory)); final HubStorageLocationService.StorageLocation location = new HubStorageLocationService.StorageLocation(storageProfileWrapper.getId().toString(), storageProfileWrapper.getRegion(), storageProfileWrapper.getName());