diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/MultiIdentityStore.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/MultiIdentityStore.java new file mode 100644 index 000000000..d899ee17d --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/identity/MultiIdentityStore.java @@ -0,0 +1,90 @@ +package io.xpipe.ext.base.identity; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.app.core.AppCache; +import io.xpipe.app.cred.SshIdentityStrategy; +import io.xpipe.app.cred.UsernameStrategy; +import io.xpipe.app.ext.ValidationException; +import io.xpipe.app.issue.ErrorEventFactory; +import io.xpipe.app.secret.EncryptedValue; +import io.xpipe.app.secret.SecretRetrievalStrategy; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.util.Validators; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import lombok.Value; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@SuperBuilder +@JsonTypeName("multiIdentity") +@Jacksonized +@Value +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MultiIdentityStore extends IdentityStore { + + List> identities; + + public List> getAvailableIdentities() { + return identities.stream().filter(ref -> ref != null && ref.get().getValidity().isUsable()).toList(); + } + + public Optional> getSelected() { + UUID cached = AppCache.getNonNull("id-default-" + getSelfEntry().getUuid(), UUID.class, () -> null); + if (cached != null) { + var entry = DataStorage.get().getStoreEntryIfPresent(cached); + if (entry.isPresent() && entry.get().getValidity().isUsable() && getAvailableIdentities().contains(entry.get().ref())) { + return Optional.of(entry.get().ref()); + } + } + + var fallback = getAvailableIdentities().stream().findFirst(); + if (fallback.isPresent()) { + return fallback; + } + + return Optional.empty(); + } + + public void select(DataStoreEntryRef entry) { + if (!getAvailableIdentities().contains(entry)) { + return; + } + + AppCache.update("id-default-" + getSelfEntry().getUuid(), entry.get().getUuid()); + } + + private DataStoreEntryRef getSelectedOrThrow() { + var found = getSelected(); + if (found.isPresent()) { + return found.get(); + } + throw ErrorEventFactory.expected(new IllegalStateException("No available identity for multi identity " + getSelfEntry().getName())); + } + + @Override + public void checkComplete() throws Throwable { + getSelectedOrThrow(); + } + + public UsernameStrategy getUsername() { + return getSelectedOrThrow().getStore().getUsername(); + } + + @Override + public SecretRetrievalStrategy getPassword() { + return getSelectedOrThrow().getStore().getPassword(); + } + + @Override + public SshIdentityStrategy getSshIdentity() { + return getSelectedOrThrow().getStore().getSshIdentity(); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/MultiIdentityStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/MultiIdentityStoreProvider.java new file mode 100644 index 000000000..3d5605604 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/identity/MultiIdentityStoreProvider.java @@ -0,0 +1,68 @@ +package io.xpipe.ext.base.identity; + +import io.xpipe.app.cred.NoIdentityStrategy; +import io.xpipe.app.cred.SshIdentityStrategyChoiceConfig; +import io.xpipe.app.ext.DataStore; +import io.xpipe.app.ext.GuiDialog; +import io.xpipe.app.hub.comp.StoreListChoiceComp; +import io.xpipe.app.hub.comp.StoreViewState; +import io.xpipe.app.platform.OptionsBuilder; +import io.xpipe.app.platform.OptionsChoiceBuilder; +import io.xpipe.app.secret.EncryptedValue; +import io.xpipe.app.secret.SecretNoneStrategy; +import io.xpipe.app.secret.SecretRetrievalStrategy; +import io.xpipe.app.secret.SecretStrategyChoiceConfig; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreCategory; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.util.DocumentationLink; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleListProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class MultiIdentityStoreProvider extends IdentityStoreProvider { + + @Override + public GuiDialog guiDialog(DataStoreEntry entry, Property store) { + MultiIdentityStore st = (MultiIdentityStore) store.getValue(); + + var identities = new SimpleListProperty<>(FXCollections.observableArrayList(st.getIdentities())); + + return new OptionsBuilder() + .nameAndDescription("multiIdentityList") + .addComp(new StoreListChoiceComp<>(identities, IdentityStore.class, + ref -> !(ref.getStore() instanceof MultiIdentityStore) && !identities.contains(ref), + StoreViewState.get().getAllIdentitiesCategory())) + .bind( + () -> { + return MultiIdentityStore.builder() + .identities(identities) + .build(); + }, + store) + .buildDialog(); + } + + @Override + public DataStore defaultStore(DataStoreCategory category) { + return MultiIdentityStore.builder() + .identities(new ArrayList<>()) + .build(); + } + + @Override + public String getId() { + return "multiIdentity"; + } + + @Override + public List> getStoreClasses() { + return List.of(MultiIdentityStore.class); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/MultiIdentitySwitchBranchProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/MultiIdentitySwitchBranchProvider.java new file mode 100644 index 000000000..721068a58 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/identity/MultiIdentitySwitchBranchProvider.java @@ -0,0 +1,89 @@ +package io.xpipe.ext.base.identity; + +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.hub.action.HubBranchProvider; +import io.xpipe.app.hub.action.HubLeafProvider; +import io.xpipe.app.hub.action.HubMenuItemProvider; +import io.xpipe.app.hub.action.StoreActionCategory; +import io.xpipe.app.platform.LabelGraphic; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntryRef; +import javafx.beans.property.ReadOnlyStringWrapper; +import javafx.beans.value.ObservableValue; + +import java.util.List; +import java.util.stream.Collectors; + +public class MultiIdentitySwitchBranchProvider implements HubBranchProvider { + + @Override + public boolean isMajor() { + return true; + } + + @Override + public List> getChildren(DataStoreEntryRef store) { + var selected = store.getStore().getSelected(); + return store.getStore().getAvailableIdentities().stream() + .map(is -> { + return new IdentityProvider(is, selected.map(ref -> ref.equals(is)).orElse(false)); + }) + .collect(Collectors.toList()); + } + + @Override + public StoreActionCategory getCategory() { + return StoreActionCategory.CONFIGURATION; + } + + @Override + public boolean isApplicable(DataStoreEntryRef o) { + return o.getStore().getAvailableIdentities().size() > 1; + } + + @Override + public ObservableValue getName(DataStoreEntryRef store) { + return AppI18n.observable("switchIdentity"); + } + + @Override + public LabelGraphic getIcon(DataStoreEntryRef store) { + return new LabelGraphic.IconGraphic("mdi2f-format-list-group"); + } + + @Override + public Class getApplicableClass() { + return MultiIdentityStore.class; + } + + private static class IdentityProvider implements HubLeafProvider { + + private final DataStoreEntryRef identity; + private final boolean active; + + private IdentityProvider(DataStoreEntryRef identity, boolean active) { + this.identity = identity; + this.active = active; + } + + @Override + public void execute(DataStoreEntryRef ref) { + ref.getStore().select(identity); + } + + @Override + public ObservableValue getName(DataStoreEntryRef store) { + return new ReadOnlyStringWrapper((active ? "> " : "") + identity.get().getName()); + } + + @Override + public LabelGraphic getIcon(DataStoreEntryRef store) { + return new LabelGraphic.ImageGraphic(identity.get().getEffectiveIconFile(), 16); + } + + @Override + public Class getApplicableClass() { + return MultiIdentityStore.class; + } + } +} diff --git a/ext/base/src/main/java/module-info.java b/ext/base/src/main/java/module-info.java index 73ecd2810..1c42fe5bb 100644 --- a/ext/base/src/main/java/module-info.java +++ b/ext/base/src/main/java/module-info.java @@ -39,6 +39,7 @@ open module io.xpipe.ext.base { AbstractHostCreationActionProvider, HostAddressSwitchBranchProvider, LocalIdentityConvertHubLeafProvider, + MultiIdentitySwitchBranchProvider, RunBackgroundScriptActionProvider, RunHubBatchScriptActionProvider, RunHubScriptActionProvider, @@ -68,6 +69,7 @@ open module io.xpipe.ext.base { LocalIdentityStoreProvider, SyncedIdentityStoreProvider, PasswordManagerIdentityStoreProvider, + MultiIdentityStoreProvider, AbstractHostStoreProvider; provides DataStorageExtensionProvider with ScriptDataStorageProvider; diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-16-dark.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-16-dark.png new file mode 100644 index 000000000..a4bd2d65b Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-16-dark.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-16.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-16.png new file mode 100644 index 000000000..2b5a8bcbd Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-16.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-24-dark.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-24-dark.png new file mode 100644 index 000000000..4fe98d444 Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-24-dark.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-24.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-24.png new file mode 100644 index 000000000..4deb2ad94 Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-24.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-40-dark.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-40-dark.png new file mode 100644 index 000000000..32d9b9964 Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-40-dark.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-40.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-40.png new file mode 100644 index 000000000..582504ef3 Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-40.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-80-dark.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-80-dark.png new file mode 100644 index 000000000..8bc62c144 Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-80-dark.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-80.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-80.png new file mode 100644 index 000000000..c14c846ea Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/multiIdentity_icon-80.png differ diff --git a/img/base/multiIdentity_icon-dark.svg b/img/base/multiIdentity_icon-dark.svg new file mode 100644 index 000000000..a56297322 --- /dev/null +++ b/img/base/multiIdentity_icon-dark.svg @@ -0,0 +1,53 @@ + +identityidentity diff --git a/img/base/multiIdentity_icon.svg b/img/base/multiIdentity_icon.svg new file mode 100644 index 000000000..820ecf43b --- /dev/null +++ b/img/base/multiIdentity_icon.svg @@ -0,0 +1,53 @@ + +identityidentity diff --git a/lang/strings/translations_en.properties b/lang/strings/translations_en.properties index 453612f25..7895b5dd8 100644 --- a/lang/strings/translations_en.properties +++ b/lang/strings/translations_en.properties @@ -2016,3 +2016,8 @@ passwordManagerSshAgentSocketDescription=Override the default agent socket locat passwordManagerSshKeysNotSupported=The current password manager configuration does not support retrieving SSH keys passwordManagerIdentityAgentKey=Additional SSH key passwordManagerIdentityAgentKeyDescription=The SSH key to use from the password manager SSH agent +multiIdentity.displayName=Multi identity +multiIdentity.displayDescription=Choose from multiple different identities to connect to a system +multiIdentityList=Identity list +multiIdentityListDescription=The list of identities to switch between +switchIdentity=Switch identity \ No newline at end of file