diff --git a/app/src/main/java/io/xpipe/app/cred/PasswordManagerAgentStrategy.java b/app/src/main/java/io/xpipe/app/cred/PasswordManagerAgentStrategy.java index 0c11eaddc..35f720660 100644 --- a/app/src/main/java/io/xpipe/app/cred/PasswordManagerAgentStrategy.java +++ b/app/src/main/java/io/xpipe/app/cred/PasswordManagerAgentStrategy.java @@ -39,22 +39,26 @@ public class PasswordManagerAgentStrategy implements SshIdentityAgentStrategy { var identifier = new SimpleStringProperty(p.getValue() != null ? p.getValue().getIdentifier() : null); - var pwmanBinding = Bindings.createObjectBinding(() -> { + var pwmanError = Bindings.createObjectBinding(() -> { var pwman = AppPrefs.get().passwordManager().getValue(); if (pwman == null) { return AppI18n.get("passwordManagerEmpty"); } - if (!pwman.getKeyConfiguration().useAgent()) { + if (!pwman.supportsKeyConfiguration()) { return AppI18n.get("passwordManagerNoAgentSupport"); } + if (!pwman.getKeyConfiguration().useAgent()) { + return AppI18n.get("passwordManagerNoAgentConfigured"); + } + return null; }, AppPrefs.get().passwordManager(), AppI18n.activeLanguage()); - var pwmanProp = new SimpleStringProperty(); - pwmanProp.bind(pwmanBinding); + var pwmanErrorProp = new SimpleStringProperty(); + pwmanErrorProp.bind(pwmanError); var pwmanDisplay = new HorizontalComp(List.of( - new LabelComp(pwmanProp) + new LabelComp(pwmanErrorProp) .maxWidth(10000) .apply(label -> label.setAlignment(Pos.CENTER_LEFT)) .hgrow(), @@ -67,9 +71,10 @@ public class PasswordManagerAgentStrategy implements SshIdentityAgentStrategy { return new OptionsBuilder() .nameAndDescription("passwordManagerSshKeyConfig") .addComp(pwmanDisplay) - .hide(pwmanProp.isNull()) + .hide(pwmanErrorProp.isNull()) .nameAndDescription(useKeyName() ? "agentKeyName" : "publicKey") .addComp(new SshAgentKeyListComp(config.getFileSystem(), p, identifier, useKeyName()), identifier) + .disable(pwmanErrorProp.isNotNull()) .nonNull() .hide(!config.isAllowAgentForward()) .bind( diff --git a/app/src/main/java/io/xpipe/app/cred/SshAgentKeyList.java b/app/src/main/java/io/xpipe/app/cred/SshAgentKeyList.java index 047730ad9..0c5232c6e 100644 --- a/app/src/main/java/io/xpipe/app/cred/SshAgentKeyList.java +++ b/app/src/main/java/io/xpipe/app/cred/SshAgentKeyList.java @@ -61,7 +61,7 @@ public class SshAgentKeyList { var type = matcher.group(1); var publicKey = matcher.group(2); - var name = matcher.groupCount() > 3 ? matcher.group(3) : null; + var name = matcher.groupCount() > 2 ? matcher.group(3) : null; list.add(new Entry(type, publicKey, name)); } return list; diff --git a/app/src/main/java/io/xpipe/app/cred/SshAgentKeyListComp.java b/app/src/main/java/io/xpipe/app/cred/SshAgentKeyListComp.java index d1a460995..66581551e 100644 --- a/app/src/main/java/io/xpipe/app/cred/SshAgentKeyListComp.java +++ b/app/src/main/java/io/xpipe/app/cred/SshAgentKeyListComp.java @@ -74,6 +74,7 @@ public class SshAgentKeyListComp extends SimpleRegionBuilder { }); entryButton.setMinWidth(400); entryButton.setAlignment(Pos.CENTER_LEFT); + entryButton.setMnemonicParsing(false); content.getChildren().add(entryButton); } popover.setContentNode(content); diff --git a/app/src/main/java/io/xpipe/app/cred/SshAgentTestComp.java b/app/src/main/java/io/xpipe/app/cred/SshAgentTestComp.java index 9bf35b7bb..850c73800 100644 --- a/app/src/main/java/io/xpipe/app/cred/SshAgentTestComp.java +++ b/app/src/main/java/io/xpipe/app/cred/SshAgentTestComp.java @@ -52,6 +52,7 @@ public class SshAgentTestComp extends SimpleRegionBuilder { entryButton.getStyleClass().add(Styles.FLAT); entryButton.setMinWidth(400); entryButton.setAlignment(Pos.CENTER_LEFT); + entryButton.setMnemonicParsing(false); content.getChildren().add(entryButton); } popover.setContentNode(content); diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreCategoryComp.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreCategoryComp.java index ea5b650cf..792061d35 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreCategoryComp.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreCategoryComp.java @@ -234,6 +234,7 @@ public class StoreCategoryComp extends SimpleRegionBuilder { newCategory.setOnAction(event -> { StoreViewState.get().createNewCategory(category); }); + newCategory.setDisable(!DataStorage.get().canCreateStoreCategoryWithin(category.getCategory())); contextMenu.getItems().add(newCategory); contextMenu.getItems().add(new SeparatorMenuItem()); diff --git a/app/src/main/java/io/xpipe/app/prefs/PasswordManagerTestComp.java b/app/src/main/java/io/xpipe/app/prefs/PasswordManagerTestComp.java index a902aaaa9..a96905435 100644 --- a/app/src/main/java/io/xpipe/app/prefs/PasswordManagerTestComp.java +++ b/app/src/main/java/io/xpipe/app/prefs/PasswordManagerTestComp.java @@ -107,12 +107,11 @@ public class PasswordManagerTestComp extends SimpleRegionBuilder { } List elements = new ArrayList<>(); - if (r.getCredentials() != null && r.getCredentials().getUsername() != null) { - elements.add(r.getCredentials().getUsername()); - } - - if (r.getCredentials() != null && r.getCredentials().getPassword() != null) { - elements.add("[" + r.getCredentials().getPassword().getSecretValue() + "]"); + if (r.getCredentials() != null) { + elements.add(r.getCredentials().getUsername() != null ? r.getCredentials().getUsername() : ""); + elements.add(r.getCredentials().getPassword() != null ? ("[" + r.getCredentials().getPassword().getSecretValue() + "]") : ""); + } else { + elements.add(""); } if (r.getSshKey() != null) { diff --git a/app/src/main/java/io/xpipe/app/pwman/BitwardenPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/BitwardenPasswordManager.java index bb87d533a..28b4ac9e4 100644 --- a/app/src/main/java/io/xpipe/app/pwman/BitwardenPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/BitwardenPasswordManager.java @@ -273,6 +273,11 @@ public class BitwardenPasswordManager implements PasswordManager { return "https://bitwarden.com/"; } + @Override + public boolean supportsKeyConfiguration() { + return true; + } + @Override public PasswordManagerKeyConfiguration getKeyConfiguration() { return PasswordManagerKeyConfiguration.of(true, false, true, keyStrategy, getSocketLocation()); diff --git a/app/src/main/java/io/xpipe/app/pwman/DashlanePasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/DashlanePasswordManager.java index 3ccfd6f61..13136615f 100644 --- a/app/src/main/java/io/xpipe/app/pwman/DashlanePasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/DashlanePasswordManager.java @@ -24,6 +24,11 @@ public class DashlanePasswordManager implements PasswordManager { private static ShellControl SHELL; + @Override + public boolean supportsKeyConfiguration() { + return false; + } + @Override public boolean selectInitial() throws Exception { return LocalShell.getShell().view().findProgram("dcli").isPresent(); diff --git a/app/src/main/java/io/xpipe/app/pwman/EnpassPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/EnpassPasswordManager.java index 764d4963a..350d85dd5 100644 --- a/app/src/main/java/io/xpipe/app/pwman/EnpassPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/EnpassPasswordManager.java @@ -47,6 +47,11 @@ public class EnpassPasswordManager implements PasswordManager { private static ShellControl SHELL; private final FilePath vaultPath; + @Override + public boolean supportsKeyConfiguration() { + return false; + } + @Override public boolean selectInitial() throws Exception { return LocalShell.getShell().view().findProgram("enpass-cli").isPresent(); diff --git a/app/src/main/java/io/xpipe/app/pwman/HashicorpVaultPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/HashicorpVaultPasswordManager.java index aa76213b5..f415bb322 100644 --- a/app/src/main/java/io/xpipe/app/pwman/HashicorpVaultPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/HashicorpVaultPasswordManager.java @@ -38,6 +38,11 @@ import java.util.*; @JsonTypeName("hashicorpVault") public class HashicorpVaultPasswordManager implements PasswordManager { + @Override + public boolean supportsKeyConfiguration() { + return false; + } + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") public interface VaultAuth { diff --git a/app/src/main/java/io/xpipe/app/pwman/KeePassXcPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/KeePassXcPasswordManager.java index 8fba96b5b..199da21fc 100644 --- a/app/src/main/java/io/xpipe/app/pwman/KeePassXcPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/KeePassXcPasswordManager.java @@ -43,6 +43,11 @@ public class KeePassXcPasswordManager implements PasswordManager { private final List associationKeys; private final PasswordManagerKeyStrategy keyStrategy; + @Override + public boolean supportsKeyConfiguration() { + return true; + } + @Override public boolean selectInitial() throws Exception { return findKeePassProxy().isPresent(); diff --git a/app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java index bd180df84..0573ee90e 100644 --- a/app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java @@ -51,6 +51,11 @@ public class KeeperPasswordManager implements PasswordManager { return socket; } + @Override + public boolean supportsKeyConfiguration() { + return true; + } + @Override public boolean selectInitial() throws Exception { return LocalShell.getShell().view().findProgram("keeper-commander").isPresent(); diff --git a/app/src/main/java/io/xpipe/app/pwman/LastpassPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/LastpassPasswordManager.java index b8aafa522..3a958b543 100644 --- a/app/src/main/java/io/xpipe/app/pwman/LastpassPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/LastpassPasswordManager.java @@ -37,6 +37,11 @@ public class LastpassPasswordManager implements PasswordManager { private static ShellControl SHELL; + @Override + public boolean supportsKeyConfiguration() { + return false; + } + @SuppressWarnings("unused") public static OptionsBuilder createOptions(Property p) { return new OptionsBuilder() diff --git a/app/src/main/java/io/xpipe/app/pwman/OnePasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/OnePasswordManager.java index 740beeb63..c04841c70 100644 --- a/app/src/main/java/io/xpipe/app/pwman/OnePasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/OnePasswordManager.java @@ -31,6 +31,11 @@ import java.util.regex.Pattern; @Getter public class OnePasswordManager implements PasswordManager { + @Override + public boolean supportsKeyConfiguration() { + return true; + } + @Override public PasswordManagerKeyConfiguration getKeyConfiguration() { return PasswordManagerKeyConfiguration.of(true, false, true, keyStrategy, getSocketLocation()); diff --git a/app/src/main/java/io/xpipe/app/pwman/PassboltPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/PassboltPasswordManager.java index 8c8eb9522..5710534f6 100644 --- a/app/src/main/java/io/xpipe/app/pwman/PassboltPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/PassboltPasswordManager.java @@ -47,6 +47,11 @@ public class PassboltPasswordManager implements PasswordManager { private final InPlaceSecretValue passphrase; private final Path privateKey; + @Override + public boolean supportsKeyConfiguration() { + return false; + } + @Override public boolean selectInitial() throws Exception { return LocalShell.getShell().view().findProgram("passbolt").isPresent(); diff --git a/app/src/main/java/io/xpipe/app/pwman/PasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/PasswordManager.java index dddcced23..5172a009b 100644 --- a/app/src/main/java/io/xpipe/app/pwman/PasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/PasswordManager.java @@ -92,6 +92,8 @@ public interface PasswordManager { String getWebsite(); + boolean supportsKeyConfiguration(); + PasswordManagerKeyConfiguration getKeyConfiguration(); boolean selectInitial() throws Exception; diff --git a/app/src/main/java/io/xpipe/app/pwman/PasswordManagerCommand.java b/app/src/main/java/io/xpipe/app/pwman/PasswordManagerCommand.java index f7f048017..7fbd61004 100644 --- a/app/src/main/java/io/xpipe/app/pwman/PasswordManagerCommand.java +++ b/app/src/main/java/io/xpipe/app/pwman/PasswordManagerCommand.java @@ -42,6 +42,11 @@ public class PasswordManagerCommand implements PasswordManager { return PasswordManagerKeyConfiguration.none(); } + @Override + public boolean supportsKeyConfiguration() { + return false; + } + @SuppressWarnings("unused") static OptionsBuilder createOptions(Property property) { var template = new SimpleObjectProperty(); diff --git a/app/src/main/java/io/xpipe/app/pwman/ProtonPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/ProtonPasswordManager.java index ebefba156..05d2dfbf0 100644 --- a/app/src/main/java/io/xpipe/app/pwman/ProtonPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/ProtonPasswordManager.java @@ -25,6 +25,11 @@ import java.util.List; @Getter public class ProtonPasswordManager implements PasswordManager { + @Override + public boolean supportsKeyConfiguration() { + return true; + } + @Override public PasswordManagerKeyConfiguration getKeyConfiguration() { return PasswordManagerKeyConfiguration.of(false, false, true, keyStrategy, getSocketLocation()); diff --git a/app/src/main/java/io/xpipe/app/pwman/PsonoPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/PsonoPasswordManager.java index 10061b476..88dc338ef 100644 --- a/app/src/main/java/io/xpipe/app/pwman/PsonoPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/PsonoPasswordManager.java @@ -39,6 +39,11 @@ public class PsonoPasswordManager implements PasswordManager { private final InPlaceSecretValue apiSecretKey; private final String serverUrl; + @Override + public boolean supportsKeyConfiguration() { + return false; + } + @Override public boolean selectInitial() throws Exception { return LocalShell.getShell().view().findProgram("psonoci").isPresent(); diff --git a/app/src/main/java/io/xpipe/app/pwman/WindowsCredentialManager.java b/app/src/main/java/io/xpipe/app/pwman/WindowsCredentialManager.java index 1af8f72f4..9ce35fc27 100644 --- a/app/src/main/java/io/xpipe/app/pwman/WindowsCredentialManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/WindowsCredentialManager.java @@ -17,6 +17,11 @@ public class WindowsCredentialManager implements PasswordManager { private static boolean loaded = false; + @Override + public boolean supportsKeyConfiguration() { + return false; + } + @Override public PasswordManagerKeyConfiguration getKeyConfiguration() { return PasswordManagerKeyConfiguration.none(); diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorage.java b/app/src/main/java/io/xpipe/app/storage/DataStorage.java index cabcd3cf5..2ad0dd244 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java @@ -423,6 +423,7 @@ public abstract class DataStorage { other.finalizeEntry(); } } + entry.finalizeEntry(); } private Collection> getDependencies(DataStoreEntry entry) { @@ -1028,6 +1029,10 @@ public abstract class DataStorage { saveAsync(); } + public boolean canCreateStoreCategoryWithin(@NonNull DataStoreCategory cat) { + return !cat.getUuid().equals(ALL_IDENTITIES_CATEGORY_UUID); + } + public boolean canDeleteStoreCategory(@NonNull DataStoreCategory cat) { if (cat.getParentCategory() == null) { return false; diff --git a/build.gradle b/build.gradle index 22c8cb4b8..c50de53f5 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ import java.util.stream.Stream plugins { id("io.github.gradle-nexus.publish-plugin") version "2.0.0" id 'org.gradlex.extra-java-module-info' version '1.14' apply false - id("com.diffplug.spotless") version "8.2.1" apply false + id("com.diffplug.spotless") version "8.3.0" apply false } allprojects { subproject -> diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityStore.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityStore.java index 46282c45c..e2f1b9162 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityStore.java @@ -3,6 +3,7 @@ package io.xpipe.ext.base.identity; import io.xpipe.app.cred.UsernameStrategy; import io.xpipe.app.ext.DataStore; import io.xpipe.app.ext.SelfReferentialStore; +import io.xpipe.app.ext.ValidationException; import io.xpipe.app.secret.SecretRetrievalStrategy; import io.xpipe.app.cred.SshIdentityStrategy; @@ -24,7 +25,7 @@ public abstract class IdentityStore implements SelfReferentialStore, DataStore { public abstract SshIdentityStrategy getSshIdentity(); @Override - public void checkComplete() throws Throwable { + public void checkComplete() throws ValidationException { if (getPassword() != null) { getPassword().checkComplete(); } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityValue.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityValue.java index 9afd9e76b..9c1d83b4d 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityValue.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityValue.java @@ -160,6 +160,7 @@ public interface IdentityValue { public void checkComplete() throws ValidationException { Validators.nonNull(ref); Validators.isType(ref, IdentityStore.class); + ref.getStore().checkComplete(); } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/PasswordManagerIdentityStore.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/PasswordManagerIdentityStore.java index 907ad1f68..d27a88eb2 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/identity/PasswordManagerIdentityStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/identity/PasswordManagerIdentityStore.java @@ -4,6 +4,7 @@ import io.xpipe.app.cred.*; import io.xpipe.app.ext.InternalCacheDataStore; import io.xpipe.app.ext.UserScopeStore; import io.xpipe.app.ext.ValidatableStore; +import io.xpipe.app.ext.ValidationException; import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.process.CommandBuilder; @@ -73,6 +74,11 @@ public class PasswordManagerIdentityStore extends IdentityStore new UnsupportedOperationException("Credentials were requested but not supplied")); } + if (r.getSshKey() != null && r.getCredentials() == null) { + throw ErrorEventFactory.expected( + new UnsupportedOperationException("Identity " + key + " does not provide credentials, only a key. Use another credentials entry as a base and reference the key via the password manager agent option instead")); + } + if (r.getCredentials() == null) { throw ErrorEventFactory.expected( new UnsupportedOperationException("Identity " + key + " does not provide credentials")); @@ -219,7 +225,7 @@ public class PasswordManagerIdentityStore extends IdentityStore } @Override - public void checkComplete() throws Throwable { + public void checkComplete() throws ValidationException { Validators.nonNull(key); if (sshKey != null) { sshKey.checkComplete(); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/SyncedIdentityStore.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/SyncedIdentityStore.java index c146660e1..b22ff3f58 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/identity/SyncedIdentityStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/identity/SyncedIdentityStore.java @@ -53,7 +53,7 @@ public class SyncedIdentityStore extends IdentityStore implements UserScopeStore } @Override - public void checkComplete() throws Throwable { + public void checkComplete() throws ValidationException { super.checkComplete(); if (getSshIdentity() instanceof KeyFileStrategy f) { if (!f.getFile().isInDataDirectory()) { diff --git a/lang/strings/translations_en.properties b/lang/strings/translations_en.properties index 1e18550aa..589069e38 100644 --- a/lang/strings/translations_en.properties +++ b/lang/strings/translations_en.properties @@ -2007,7 +2007,8 @@ passwordManagerKeyStrategy=SSH key retrieval method passwordManagerKeyStrategyDescription=How to retrieve SSH keys stored in the password manager. When disabled, no keys can be retrieved. When an SSH agent is configured, the agent must be properly started in the password manager. sshAgentNoKeys=The agent does not have any keys at the moment sshAgentHasKeys=The agent currently offers the following keys: -passwordManagerNoAgentSupport=Password manager does not support retrieving SSH keys +passwordManagerNoAgentSupport=Password manager does not support retrieving SSH keys via an agent +passwordManagerNoAgentConfigured=XPipe is not configured for retrieving SSH keys from the agent passwordManagerEmpty=No password manager is configured passwordManagerSshKeyConfig=Password manager configuration passwordManagerSshKeyConfigDescription=Check the status of the password manager