mirror of
https://github.com/xpipe-io/xpipe.git
synced 2026-09-24 17:35:39 +00:00
Merge branch keygen
This commit is contained in:
@@ -47,6 +47,7 @@ public class ButtonComp extends Comp<CompStructure<Button>> {
|
||||
@Override
|
||||
public CompStructure<Button> createBase() {
|
||||
var button = new Button(null);
|
||||
button.setMnemonicParsing(false);
|
||||
if (name != null) {
|
||||
name.subscribe(t -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> button.setText(t));
|
||||
|
||||
@@ -5,6 +5,8 @@ import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.SimpleCompStructure;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
import atlantafx.base.layout.InputGroup;
|
||||
@@ -35,6 +37,7 @@ public class InputGroupComp extends Comp<CompStructure<InputGroup>> {
|
||||
if (mainReference != null && entries.contains(mainReference)) {
|
||||
var refIndex = entries.indexOf(mainReference);
|
||||
var ref = b.getChildren().get(refIndex);
|
||||
HBox.setHgrow(ref, Priority.ALWAYS);
|
||||
if (ref instanceof Region refR) {
|
||||
for (int i = 0; i < entries.size(); i++) {
|
||||
if (i == refIndex) {
|
||||
|
||||
@@ -30,6 +30,7 @@ import atlantafx.base.controls.ModalPane;
|
||||
import atlantafx.base.layout.ModalBox;
|
||||
import atlantafx.base.theme.Styles;
|
||||
import atlantafx.base.util.Animations;
|
||||
import net.synedra.validatorfx.GraphicDecorationStackPane;
|
||||
|
||||
public class ModalOverlayComp extends SimpleComp {
|
||||
|
||||
@@ -171,8 +172,10 @@ public class ModalOverlayComp extends SimpleComp {
|
||||
|
||||
private Region toBox(ModalPane pane, ModalOverlay newValue) {
|
||||
Region r = newValue.getContent().createRegion();
|
||||
var validatorPane = new GraphicDecorationStackPane();
|
||||
validatorPane.getChildren().add(r);
|
||||
|
||||
var content = new VBox(r);
|
||||
var content = new VBox(validatorPane);
|
||||
content.getStyleClass().add("content");
|
||||
content.focusedProperty().addListener((o, old, n) -> {
|
||||
if (n) {
|
||||
|
||||
@@ -232,7 +232,6 @@ public class OptionsComp extends Comp<CompStructure<VBox>> {
|
||||
}
|
||||
|
||||
public record Entry(
|
||||
String key,
|
||||
ObservableValue<String> description,
|
||||
String documentationLink,
|
||||
ObservableValue<String> name,
|
||||
|
||||
@@ -7,8 +7,11 @@ import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.CommandControl;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.process.ShellDialect;
|
||||
import io.xpipe.app.secret.SecretRetrievalStrategy;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.vnc.VncBaseStore;
|
||||
import io.xpipe.core.SecretValue;
|
||||
import javafx.beans.property.Property;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ServiceLoader;
|
||||
@@ -28,6 +31,10 @@ public abstract class ProcessControlProvider {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public abstract String generatePublicSshKey(SecretValue privateKey, SecretRetrievalStrategy passphrase);
|
||||
|
||||
public abstract void showSshKeygenDialog(String commentDefault, Property<?> identityProperty);
|
||||
|
||||
public abstract ShellStore subShellEnvironment(DataStoreEntryRef<ShellStore> s, ShellDialect dialect);
|
||||
|
||||
public abstract BrowserStoreSessionTab<?> createVncSession(
|
||||
|
||||
@@ -51,7 +51,7 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
|
||||
"noCompatibleConnection");
|
||||
}
|
||||
|
||||
private String toName(DataStoreEntry entry) {
|
||||
protected String toName(DataStoreEntry entry) {
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ package io.xpipe.app.platform;
|
||||
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
@@ -39,7 +42,7 @@ public class BindingsHelper {
|
||||
}
|
||||
}
|
||||
|
||||
public static <T, U> ObservableValue<U> map(
|
||||
public static <T, U> ObjectBinding<U> map(
|
||||
ObservableValue<T> observableValue, Function<? super T, ? extends U> mapper) {
|
||||
return Bindings.createObjectBinding(
|
||||
() -> {
|
||||
@@ -48,6 +51,15 @@ public class BindingsHelper {
|
||||
observableValue);
|
||||
}
|
||||
|
||||
public static <T> BooleanBinding mapBoolean(
|
||||
ObservableValue<T> observableValue, Function<? super T, Boolean> mapper) {
|
||||
return Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return mapper.apply(observableValue.getValue());
|
||||
},
|
||||
observableValue);
|
||||
}
|
||||
|
||||
public static <T, U> ObservableValue<U> flatMap(
|
||||
ObservableValue<T> observableValue, Function<? super T, ? extends ObservableValue<? extends U>> mapper) {
|
||||
var prop = new SimpleObjectProperty<U>();
|
||||
|
||||
@@ -5,6 +5,7 @@ import io.xpipe.app.comp.base.*;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.GuiDialog;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.DocumentationLink;
|
||||
import io.xpipe.app.util.LicenseProvider;
|
||||
import io.xpipe.core.InPlaceSecretValue;
|
||||
@@ -30,6 +31,36 @@ import java.util.function.Supplier;
|
||||
|
||||
public class OptionsBuilder {
|
||||
|
||||
public <V, T> ObjectProperty<T> map(Property<V> prop, Function<V, T> function) {
|
||||
var mapped = new SimpleObjectProperty<T>();
|
||||
prop.subscribe(v -> {
|
||||
if (mappingUpdate.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (var ignored = new BooleanScope(mappingUpdate).start()) {
|
||||
mapped.setValue(function.apply(v));
|
||||
}
|
||||
});
|
||||
return mapped;
|
||||
}
|
||||
|
||||
public <V, T, R> ObjectProperty<R> map(Property<V> prop, Function<V, T> function, Function<T, R> subFunction) {
|
||||
var mapped = new SimpleObjectProperty<R>();
|
||||
prop.subscribe(v -> {
|
||||
if (mappingUpdate.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
T t = function.apply(v);
|
||||
R r = t != null ? subFunction.apply(t) : null;
|
||||
try (var ignored = new BooleanScope(mappingUpdate).start()) {
|
||||
mapped.setValue(r);
|
||||
}
|
||||
});
|
||||
return mapped;
|
||||
}
|
||||
|
||||
private final Validator ownValidator;
|
||||
private final List<Validator> allValidators = new ArrayList<>();
|
||||
private final List<Check> allChecks = new ArrayList<>();
|
||||
@@ -44,6 +75,8 @@ public class OptionsBuilder {
|
||||
private ObservableValue<String> lastNameReference;
|
||||
private boolean focusFirstIncomplete = true;
|
||||
|
||||
private BooleanProperty mappingUpdate = new SimpleBooleanProperty();
|
||||
|
||||
public OptionsBuilder disableFirstIncompleteFocus() {
|
||||
focusFirstIncomplete = false;
|
||||
return this;
|
||||
@@ -86,6 +119,9 @@ public class OptionsBuilder {
|
||||
validatorList.get(list.indexOf(newValue)).validate();
|
||||
}
|
||||
});
|
||||
selectedIndex.addListener((observable, oldValue, newValue) -> {
|
||||
selected.setValue(list.get(newValue.intValue()));
|
||||
});
|
||||
var pane = new ChoicePaneComp(list, selected);
|
||||
if (transformer != null) {
|
||||
pane.setTransformer(transformer);
|
||||
@@ -114,7 +150,7 @@ public class OptionsBuilder {
|
||||
return;
|
||||
}
|
||||
|
||||
var entry = new OptionsComp.Entry(null, description, documentationLink, name, comp);
|
||||
var entry = new OptionsComp.Entry(description, documentationLink, name, comp);
|
||||
description = null;
|
||||
documentationLink = null;
|
||||
name = null;
|
||||
@@ -171,7 +207,14 @@ public class OptionsBuilder {
|
||||
public OptionsBuilder addTitle(String titleKey) {
|
||||
finishCurrent();
|
||||
entries.add(new OptionsComp.Entry(
|
||||
titleKey, null, null, null, new LabelComp(AppI18n.observable(titleKey)).styleClass("title-header")));
|
||||
null, null, null, new LabelComp(AppI18n.observable(titleKey)).styleClass("title-header")));
|
||||
return this;
|
||||
}
|
||||
|
||||
public OptionsBuilder addTitle(ObservableValue<String> title) {
|
||||
finishCurrent();
|
||||
entries.add(new OptionsComp.Entry(
|
||||
null, null, null, new LabelComp(title).styleClass("title-header")));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -395,6 +438,10 @@ public class OptionsBuilder {
|
||||
public final <T, V extends T> OptionsBuilder bind(Supplier<V> creator, Property<T>... toSet) {
|
||||
props.forEach(prop -> {
|
||||
prop.addListener((c, o, n) -> {
|
||||
if (mappingUpdate.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Property<T> p : toSet) {
|
||||
p.setValue(creator.get());
|
||||
}
|
||||
@@ -412,12 +459,20 @@ public class OptionsBuilder {
|
||||
var listener = new ChangeListener<V>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends V> observable, V oldValue, V newValue) {
|
||||
if (mappingUpdate.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
toSet.setValue(newValue);
|
||||
}
|
||||
};
|
||||
current.get().addListener(listener);
|
||||
props.forEach(prop -> {
|
||||
prop.addListener((c, o, n) -> {
|
||||
if (mappingUpdate.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
current.get().removeListener(listener);
|
||||
current.set(creator.get());
|
||||
toSet.setValue(current.get().getValue());
|
||||
|
||||
@@ -138,6 +138,7 @@ public class OptionsChoiceBuilder {
|
||||
var c = sub.get(i);
|
||||
if (c.isAssignableFrom(newValue.getClass())) {
|
||||
properties.get(i + (allowNull ? 1 : 0)).setValue(newValue);
|
||||
selected.setValue(i + (allowNull ? 1 : 0));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -157,7 +158,7 @@ public class OptionsChoiceBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
return new OptionsBuilder()
|
||||
var options = new OptionsBuilder()
|
||||
.choice(selected, map, transformer)
|
||||
.bindChoice(
|
||||
() -> {
|
||||
@@ -169,5 +170,6 @@ public class OptionsChoiceBuilder {
|
||||
return (Property<? extends T>) prop;
|
||||
},
|
||||
s);
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ public class ShellView {
|
||||
protected String user;
|
||||
protected FilePath userHome;
|
||||
protected Boolean root;
|
||||
protected Boolean administrator;
|
||||
protected PasswdFile passwdFile;
|
||||
protected GroupFile groupFile;
|
||||
protected final Map<String, Boolean> installedApplications = new HashMap<>();
|
||||
@@ -254,4 +255,28 @@ public class ShellView {
|
||||
command.sensitive();
|
||||
command.execute();
|
||||
}
|
||||
|
||||
public synchronized boolean isAdministrator() throws Exception {
|
||||
if (shellControl.getOsType() != OsType.WINDOWS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (administrator != null) {
|
||||
return administrator;
|
||||
}
|
||||
|
||||
if (shellControl.getShellDialect() == ShellDialects.CMD) {
|
||||
administrator = shellControl.command("net.exe session 1>NUL 2>NUL").executeAndCheck();
|
||||
} else if (ShellDialects.isPowershell(shellControl)) {
|
||||
administrator = shellControl.command(String.format(
|
||||
"$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent());"
|
||||
+ "try {if (-not $($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))) {$host.ui"
|
||||
+ ".WriteErrorLine(\"%s\"); throw \"error\"}} catch {}",
|
||||
"Not Administrator")).executeAndCheck();
|
||||
} else {
|
||||
administrator = false;
|
||||
}
|
||||
|
||||
return administrator;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.ext.ValidationException;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.util.Validators;
|
||||
import io.xpipe.core.InPlaceSecretValue;
|
||||
|
||||
@@ -27,11 +28,10 @@ public class SecretCustomCommandStrategy implements SecretRetrievalStrategy {
|
||||
@SuppressWarnings("unused")
|
||||
public static OptionsBuilder createOptions(
|
||||
Property<SecretCustomCommandStrategy> p, SecretStrategyChoiceConfig config) {
|
||||
var cmdProperty =
|
||||
new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getCommand() : null);
|
||||
var content = new TextFieldComp(cmdProperty);
|
||||
return new OptionsBuilder()
|
||||
.addComp(content, cmdProperty)
|
||||
var options = new OptionsBuilder();
|
||||
var cmdProperty = options.map(p, SecretCustomCommandStrategy::getCommand);
|
||||
return options
|
||||
.addComp(new TextFieldComp(cmdProperty), cmdProperty)
|
||||
.nonNull()
|
||||
.bind(
|
||||
() -> {
|
||||
|
||||
@@ -28,11 +28,9 @@ public class SecretInPlaceStrategy implements SecretRetrievalStrategy {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static OptionsBuilder createOptions(Property<SecretInPlaceStrategy> p, SecretStrategyChoiceConfig config) {
|
||||
var options = new OptionsBuilder();
|
||||
var original = p.getValue() != null ? p.getValue().getValue() : null;
|
||||
var secretProperty = new SimpleObjectProperty<>(
|
||||
p.getValue() != null && p.getValue().getValue() != null
|
||||
? p.getValue().getValue()
|
||||
: null);
|
||||
var secretProperty = options.map(p, SecretInPlaceStrategy::getValue);
|
||||
return new OptionsBuilder()
|
||||
.addComp(new SecretFieldComp(secretProperty, true), secretProperty)
|
||||
.nonNull()
|
||||
|
||||
@@ -33,9 +33,9 @@ public class SecretPasswordManagerStrategy implements SecretRetrievalStrategy {
|
||||
@SuppressWarnings("unused")
|
||||
public static OptionsBuilder createOptions(
|
||||
Property<SecretPasswordManagerStrategy> p, SecretStrategyChoiceConfig config) {
|
||||
var options = new OptionsBuilder();
|
||||
var prefs = AppPrefs.get();
|
||||
var keyProperty =
|
||||
new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getKey() : null);
|
||||
var keyProperty = options.map(p, SecretPasswordManagerStrategy::getKey);
|
||||
var content = new HorizontalComp(List.of(
|
||||
new TextFieldComp(keyProperty)
|
||||
.apply(struc -> struc.get()
|
||||
@@ -63,7 +63,7 @@ public class SecretPasswordManagerStrategy implements SecretRetrievalStrategy {
|
||||
struc.get().getChildren().getFirst().requestFocus();
|
||||
}
|
||||
}));
|
||||
return new OptionsBuilder()
|
||||
return options
|
||||
.nameAndDescription("passwordManagerKey")
|
||||
.addComp(content, keyProperty)
|
||||
.nonNull()
|
||||
|
||||
@@ -415,6 +415,8 @@ public abstract class DataStorage {
|
||||
}
|
||||
entry.setStoreInternal(store, false);
|
||||
entry.initializeEntry();
|
||||
|
||||
saveAsync();
|
||||
}
|
||||
|
||||
public void updateCategory(DataStoreCategory category, DataStoreCategory newCategory) {
|
||||
|
||||
@@ -70,6 +70,7 @@ public enum DocumentationLink {
|
||||
TUNNELS_DYNAMIC("guide/ssh-tunnels#dynamic-tunnels"),
|
||||
HYPERV("guide/hyperv"),
|
||||
SSH_MACS("troubleshoot/ssh#no-matching-mac-found"),
|
||||
SSH_FEATURE_NOT_SUPPORTED("troubleshoot/ssh#requested-feature-not-supported"),
|
||||
SSH_JUMP_SERVERS("guide/ssh#gateways-and-jump-servers"),
|
||||
SSH_CUSTOM("guide/ssh-config#custom-ssh-connections"),
|
||||
SSH_CUSTOM_ORDER("guide/ssh-config#jump-hosts"),
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.store-section-mini-comp .item:disabled {
|
||||
-fx-opacity: 1.0;
|
||||
}
|
||||
|
||||
.store-section-mini-comp .item {
|
||||
-fx-padding: 0.25em 0.4em 0.25em 0.4em;
|
||||
-fx-border-color: transparent;
|
||||
|
||||
@@ -199,3 +199,18 @@
|
||||
-fx-font-family: Roboto;
|
||||
}
|
||||
|
||||
.root .label.success .ikonli-font-icon.graphic {
|
||||
-fx-icon-color: -color-success-fg;
|
||||
}
|
||||
|
||||
.root .label.warning .ikonli-font-icon.graphic {
|
||||
-fx-icon-color: -color-warning-fg;
|
||||
}
|
||||
|
||||
.root .label.danger .ikonli-font-icon.graphic {
|
||||
-fx-icon-color: -color-danger-fg;
|
||||
}
|
||||
|
||||
.monospace {
|
||||
-fx-font-family: Monospace;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
package io.xpipe.ext.base.identity;
|
||||
|
||||
import atlantafx.base.theme.Styles;
|
||||
import io.xpipe.app.browser.BrowserFullSessionModel;
|
||||
import io.xpipe.app.browser.file.BrowserFileOpener;
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.base.*;
|
||||
import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.hub.comp.StoreChoiceComp;
|
||||
import io.xpipe.app.hub.comp.StoreViewState;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.platform.BindingsHelper;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
import io.xpipe.app.platform.PlatformThread;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.FilePath;
|
||||
import io.xpipe.core.OsType;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Button;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class IdentityApplyDialog {
|
||||
|
||||
|
||||
@Data
|
||||
private static class SystemState {
|
||||
|
||||
public static SystemState of(ShellControl sc, IdentityStore identity) throws Exception {
|
||||
var s = new SystemState();
|
||||
s.init(sc, identity);
|
||||
return s;
|
||||
}
|
||||
|
||||
boolean inAuthorizedKeys;
|
||||
boolean keyAuthEnabled;
|
||||
boolean keyAuthInMethods;
|
||||
boolean passwordAuthEnabled;
|
||||
boolean passwordAuthInMethods;
|
||||
boolean rootLoginEnabled;
|
||||
boolean mightRequireAdministratorAuthorizedKeys;
|
||||
|
||||
FilePath configFile;
|
||||
FilePath authorizedKeysFile;
|
||||
|
||||
private void init(ShellControl sc, IdentityStore identity) throws Exception {
|
||||
var hasPassword = identity.getPassword() != null && identity.getPassword().expectsQuery();
|
||||
var hasIdentity = identity.getSshIdentity() != null && identity.getSshIdentity().getPublicKey() != null;
|
||||
|
||||
configFile = getSystemConfigPath(sc);
|
||||
authorizedKeysFile = getAuthorizedKeysFile(sc);
|
||||
|
||||
var configContent = getSshdConfigContent(sc);
|
||||
keyAuthEnabled = isSet(configContent, "PubkeyAuthentication", "yes", true,false);
|
||||
passwordAuthEnabled = isSet(configContent, "PasswordAuthentication", "yes", true, false);
|
||||
rootLoginEnabled = isSet(configContent, "PermitRootLogin", "yes", !hasPassword, false) ||
|
||||
(!hasPassword &&
|
||||
!isSet(configContent, "PermitRootLogin", "forced-commands-only", true, false) &&
|
||||
isSet(configContent, "PermitRootLogin", "prohibit-password", true, true));
|
||||
keyAuthInMethods = isSet(configContent, "AuthenticationMethods", "publickey", true, false);
|
||||
passwordAuthInMethods = isSet(configContent, "AuthenticationMethods", "password", true, false);
|
||||
mightRequireAdministratorAuthorizedKeys = sc.getOsType() == OsType.WINDOWS &&
|
||||
isSet(configContent, "Match", "Group administrators", false, false) &&
|
||||
isSet(configContent, "AuthorizedKeysFile", "administrators_authorized_keys", false, false);
|
||||
|
||||
if (hasIdentity) {
|
||||
var authorizedKeysContent = getAuthorizedKeysContent(sc);
|
||||
|
||||
var publicKey = identity.getSshIdentity().getPublicKey();
|
||||
var split = publicKey.split("\\s+");
|
||||
var basePublicKey = split[0] + " " + split[1];
|
||||
|
||||
inAuthorizedKeys = authorizedKeysContent.toLowerCase().contains(basePublicKey.toLowerCase());
|
||||
} else {
|
||||
inAuthorizedKeys = true;
|
||||
}
|
||||
}
|
||||
|
||||
private FilePath getSystemConfigPath(ShellControl sc) throws Exception {
|
||||
if (sc.getOsType() == OsType.WINDOWS) {
|
||||
var base = sc.view().getEnvironmentVariableOrThrow("programdata");
|
||||
return FilePath.of(base).join("ssh", "sshd_config");
|
||||
}
|
||||
|
||||
return FilePath.of("/etc/ssh/sshd_config");
|
||||
}
|
||||
|
||||
private FilePath getAuthorizedKeysFile(ShellControl sc) throws Exception {
|
||||
var v = sc.view();
|
||||
var authorizedKeysFile = v.userHome().join(".ssh", "authorized_keys");
|
||||
return authorizedKeysFile;
|
||||
}
|
||||
|
||||
private String getAuthorizedKeysContent(ShellControl sc) throws Exception {
|
||||
var v = sc.view();
|
||||
var authorizedKeysContent = v.fileExists(authorizedKeysFile) ? v.readTextFile(authorizedKeysFile) : "";
|
||||
return authorizedKeysContent;
|
||||
}
|
||||
|
||||
private String getSshdConfigContent(ShellControl sc) throws Exception {
|
||||
var v = sc.view();
|
||||
var configContent = v.fileExists(configFile) ? v.readTextFile(configFile) : "";
|
||||
return configContent;
|
||||
}
|
||||
|
||||
private boolean isSet(String config, String name, String value, boolean notFoundDef, boolean notSpecifiedDef) {
|
||||
var found = config.lines().filter(s -> {
|
||||
return !s.strip().startsWith("#");
|
||||
}).filter(s -> {
|
||||
return s.toLowerCase().contains(name.toLowerCase());
|
||||
}).toList();
|
||||
if (found.isEmpty()) {
|
||||
return notFoundDef;
|
||||
}
|
||||
|
||||
for (String line : found) {
|
||||
var matches = line.toLowerCase().contains(value.toLowerCase());
|
||||
if (matches) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return notSpecifiedDef;
|
||||
}
|
||||
}
|
||||
|
||||
private static void addPublicKey(SystemState systemState, ShellControl sc, String publicKey) throws Exception {
|
||||
var v = sc.view();
|
||||
var authorizedKeysFile = systemState.getAuthorizedKeysFile();
|
||||
v.mkdir(authorizedKeysFile.getParent());
|
||||
String authorizedKeysContent;
|
||||
if (v.fileExists(authorizedKeysFile)) {
|
||||
var text = v.readTextFile(authorizedKeysFile).strip();
|
||||
authorizedKeysContent = text.isBlank() ? publicKey + "\n" : text + "\n" + publicKey + "\n";
|
||||
} else {
|
||||
authorizedKeysContent = publicKey + "\n";
|
||||
}
|
||||
v.writeTextFile(authorizedKeysFile, authorizedKeysContent);
|
||||
if (sc.getOsType() != OsType.WINDOWS) {
|
||||
sc.command(CommandBuilder.of().add("chmod", "600").addFile(authorizedKeysFile)).execute();
|
||||
}
|
||||
}
|
||||
|
||||
private static Comp<?> success() {
|
||||
var graphic = new LabelGraphic.IconGraphic("mdi2c-checkbox-marked-outline");
|
||||
return new LabelComp(AppI18n.observable("valid"), new ReadOnlyObjectWrapper<>(graphic)).styleClass(Styles.SUCCESS).apply(struc -> {
|
||||
AppFontSizes.lg(struc.get());
|
||||
});
|
||||
}
|
||||
|
||||
private static Comp<?> warning() {
|
||||
var graphic = new LabelGraphic.IconGraphic("mdi2a-alert-box-outline");
|
||||
return new LabelComp(AppI18n.observable("warning"), new ReadOnlyObjectWrapper<>(graphic)).styleClass(Styles.WARNING).apply(struc -> {
|
||||
AppFontSizes.lg(struc.get());
|
||||
});
|
||||
}
|
||||
|
||||
private static Comp<?> fail(Comp<?> fixComp) {
|
||||
var graphic = new LabelGraphic.IconGraphic("mdi2c-close-box-outline");
|
||||
var label = new LabelComp(AppI18n.observable("notValid"), new ReadOnlyObjectWrapper<>(graphic)).styleClass(Styles.DANGER);
|
||||
label.apply(struc -> {
|
||||
AppFontSizes.lg(struc.get());
|
||||
});
|
||||
if (fixComp != null) {
|
||||
var hbox = new HorizontalComp(List.of(label, fixComp, Comp.hspacer()));
|
||||
hbox.spacing(10);
|
||||
return hbox;
|
||||
} else {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
|
||||
private static Comp<?> createAuthorizedKeysOptions(Property<DataStoreEntryRef<ShellStore>> system, ObjectProperty<SystemState> systemState, IdentityStore identity, BooleanProperty busy) {
|
||||
var showAddAuthorizedHost = BindingsHelper.mapBoolean(systemState, s -> {
|
||||
return s != null && !s.isInAuthorizedKeys();
|
||||
});
|
||||
|
||||
var editButton = new ButtonComp(AppI18n.observable("identityApplyEditAuthorizedKeysButton"), () -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
BooleanScope.executeExclusive(busy, () -> {
|
||||
var sc = system.getValue().getStore().getOrStartSession();
|
||||
var file = systemState.get().getAuthorizedKeysFile();
|
||||
var model = BrowserFullSessionModel.DEFAULT.openFileSystemSync(system.getValue(), null, m -> file.getParent(), null, false);
|
||||
var found = model.findFile(file);
|
||||
if (found.isEmpty()) {
|
||||
model.getFileSystem().touch(file);
|
||||
if (sc.getOsType() != OsType.WINDOWS) {
|
||||
sc.command(CommandBuilder.of().add("chmod", "600").addFile(file)).execute();
|
||||
}
|
||||
model.refreshSync();
|
||||
found = model.findFile(file);
|
||||
}
|
||||
if (found.isPresent()) {
|
||||
BrowserFileOpener.openInTextEditor(model, found.get());
|
||||
}
|
||||
});
|
||||
});
|
||||
}).padding(new Insets(4, 8, 4, 8));
|
||||
|
||||
var addButton = new ButtonComp(AppI18n.observable("identityApplyAuthorizedHostButton"),
|
||||
() -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
BooleanScope.executeExclusive(busy, () -> {
|
||||
var sc = system.getValue().getStore().getOrStartSession();
|
||||
addPublicKey(systemState.get(), sc, identity.getSshIdentity().getPublicKey());
|
||||
systemState.setValue(SystemState.of(sc, identity));
|
||||
});
|
||||
});
|
||||
}).padding(new Insets(4, 8, 4, 8));
|
||||
|
||||
var options = new OptionsBuilder()
|
||||
.nameAndDescription("identityApplyAuthorizedHost")
|
||||
.addComp(success())
|
||||
.hide(showAddAuthorizedHost)
|
||||
.nameAndDescription("identityApplyAuthorizedHost")
|
||||
.addComp(fail(addButton))
|
||||
.hide(showAddAuthorizedHost.not())
|
||||
.nameAndDescription("identityApplyEditAuthorizedKeys")
|
||||
.addComp(editButton);
|
||||
|
||||
return options.buildComp().hide(Bindings.isNull(systemState));
|
||||
}
|
||||
|
||||
private static Comp<?> createConfigOptions(Property<DataStoreEntryRef<ShellStore>> system, Property<SystemState> systemState, IdentityStore identity, BooleanProperty busy) {
|
||||
var showAdminWarning = BindingsHelper.mapBoolean(systemState, s -> {
|
||||
return s != null && identity.getSshIdentity() != null && identity.getSshIdentity().providesKey() && s.isMightRequireAdministratorAuthorizedKeys();
|
||||
});
|
||||
var showPasswordEnabledWarning = BindingsHelper.mapBoolean(systemState, s -> {
|
||||
return s != null && (identity.getPassword() == null || !identity.getPassword().expectsQuery()) && s.isPasswordAuthEnabled() && s.isPasswordAuthInMethods();
|
||||
});
|
||||
var showPasswordDisabledWarning = BindingsHelper.mapBoolean(systemState, s -> {
|
||||
return s != null && identity.getPassword() != null && identity.getPassword().expectsQuery() && (!s.isPasswordAuthEnabled() || !s.isPasswordAuthInMethods());
|
||||
});
|
||||
var showKeyEnabledWarning = BindingsHelper.mapBoolean(systemState, s -> {
|
||||
return s != null && (identity.getSshIdentity() == null || !identity.getSshIdentity().providesKey()) && s.keyAuthEnabled;
|
||||
});
|
||||
var showKeyDisabledWarning = BindingsHelper.mapBoolean(systemState, s -> {
|
||||
return s != null && identity.getSshIdentity() != null && identity.getSshIdentity().providesKey() && !s.keyAuthEnabled;
|
||||
});
|
||||
var showRootDisabledWarning = BindingsHelper.mapBoolean(systemState, s -> {
|
||||
return s != null && identity.getUsername().getFixedUsername().map(u -> u.equals("root")).orElse(false) && !s.rootLoginEnabled;
|
||||
});
|
||||
var showConfigSection = showKeyEnabledWarning.or(showRootDisabledWarning).or(showPasswordEnabledWarning)
|
||||
.or(showPasswordDisabledWarning).or(showKeyDisabledWarning).or(showAdminWarning);
|
||||
|
||||
var editButton = new ButtonComp(AppI18n.observable("identityApplyEditConfigButton"), () -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
BooleanScope.executeExclusive(busy, () -> {
|
||||
var file = systemState.getValue().getConfigFile();
|
||||
var model = BrowserFullSessionModel.DEFAULT.openFileSystemSync(system.getValue(), null, m -> file.getParent(), null, false);
|
||||
var found = model.findFile(file);
|
||||
if (found.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
BrowserFileOpener.openInTextEditor(model, found.get());
|
||||
});
|
||||
});
|
||||
}).padding(new Insets(4, 8, 4, 8));
|
||||
|
||||
var options = new OptionsBuilder()
|
||||
.nameAndDescription("identityApplyConfigPasswordEnabled")
|
||||
.addComp(warning())
|
||||
.hide(showPasswordEnabledWarning.not())
|
||||
.nameAndDescription("identityApplyConfigPasswordDisabled")
|
||||
.addComp(warning())
|
||||
.hide(showPasswordDisabledWarning.not())
|
||||
.nameAndDescription("identityApplyConfigKeyEnabled")
|
||||
.addComp(warning())
|
||||
.hide(showKeyEnabledWarning.not())
|
||||
.nameAndDescription("identityApplyConfigKeyDisabled")
|
||||
.addComp(warning())
|
||||
.hide(showKeyDisabledWarning.not())
|
||||
.nameAndDescription("identityApplyConfigRootDisabledWarning")
|
||||
.addComp(fail(null))
|
||||
.hide(showRootDisabledWarning.not())
|
||||
.nameAndDescription("identityApplyConfigAdminWarning")
|
||||
.documentationLink("https://learn.microsoft.com/en-us/windows-server/administration/openssh/openssh_keymanagement#administrative-user")
|
||||
.addComp(warning())
|
||||
.hide(showAdminWarning.not())
|
||||
.nameAndDescription("identityApplyEditConfig")
|
||||
.addComp(editButton).buildComp()
|
||||
.hide(showConfigSection.not());
|
||||
return options;
|
||||
}
|
||||
|
||||
public static void show(DataStoreEntryRef<IdentityStore> identity) {
|
||||
var busy = new SimpleBooleanProperty();
|
||||
var system = new SimpleObjectProperty<DataStoreEntryRef<ShellStore>>();
|
||||
var systemState = new SimpleObjectProperty<SystemState>();
|
||||
var showSetIdentityButton = new SimpleBooleanProperty();
|
||||
var showIdentityAlreadySet = new SimpleBooleanProperty();
|
||||
system.addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue == null) {
|
||||
systemState.setValue(null);
|
||||
showSetIdentityButton.set(false);
|
||||
showIdentityAlreadySet.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
BooleanScope.executeExclusive(busy, () -> {
|
||||
var sc = newValue.getStore().getOrStartSession();
|
||||
systemState.setValue(SystemState.of(sc, identity.getStore()));
|
||||
showSetIdentityButton.set(newValue.getStore() instanceof IdentitySwitchStore iss && !iss.getIdentity().unwrap().equals(identity.getStore()));
|
||||
showIdentityAlreadySet.set(newValue.getStore() instanceof IdentitySwitchStore iss && iss.getIdentity().unwrap().equals(identity.getStore()));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var systemChoice = new StoreChoiceComp<>(null, system, ShellStore.class, null, StoreViewState.get().getAllConnectionsCategory()) {
|
||||
|
||||
@Override
|
||||
protected String toName(DataStoreEntry entry) {
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return DataStorage.get().getStoreEntryDisplayName(entry) + " -> " + IdentitySummary.createSummary(identity.getStore());
|
||||
}
|
||||
};
|
||||
var systemChoiceBusy = new LoadingOverlayComp(systemChoice, busy, false);
|
||||
|
||||
var applyButton = new ButtonComp(AppI18n.observable("identityApplySetStoreIdentityButton"),
|
||||
() -> {
|
||||
DataStorage.get().updateEntryStore(system.get().get(),
|
||||
((IdentitySwitchStore) system.get().getStore()).withIdentity(new IdentityValue.Ref(identity)));
|
||||
showSetIdentityButton.set(false);
|
||||
showIdentityAlreadySet.set(true);
|
||||
});
|
||||
applyButton.padding(new Insets(4, 8, 4, 8));
|
||||
|
||||
var options = new OptionsBuilder()
|
||||
.nameAndDescription("identityApplyTargetHost")
|
||||
.addComp(systemChoiceBusy, system)
|
||||
.addComp(createAuthorizedKeysOptions(system, systemState, identity.getStore(), busy))
|
||||
.addComp(createConfigOptions(system, systemState, identity.getStore(), busy))
|
||||
.nameAndDescription("identityApplySetStoreIdentity")
|
||||
.addComp(success())
|
||||
.hide(showIdentityAlreadySet.not())
|
||||
.nameAndDescription("identityApplySetStoreIdentity")
|
||||
.addComp(fail(applyButton))
|
||||
.hide(showSetIdentityButton.not());
|
||||
|
||||
var modal = ModalOverlay.of("identityApplyTitle", options.buildComp().prefWidth(600).prefHeight(500));
|
||||
modal.persist();
|
||||
modal.addButton(ModalButton.cancel());
|
||||
modal.addButton(ModalButton.ok().augment(button -> {
|
||||
button.disableProperty().bind(PlatformThread.sync(busy));
|
||||
}));
|
||||
modal.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package io.xpipe.ext.base.identity;
|
||||
|
||||
import io.xpipe.app.action.AbstractAction;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.hub.action.HubLeafProvider;
|
||||
import io.xpipe.app.hub.action.StoreAction;
|
||||
import io.xpipe.app.hub.action.StoreActionCategory;
|
||||
import io.xpipe.app.hub.comp.StoreCreationDialog;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.app.process.ShellTtyState;
|
||||
import io.xpipe.app.process.SystemState;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.ext.base.identity.ssh.NoIdentityStrategy;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
public class IdentityApplyHubLeafProvider implements HubLeafProvider<IdentityStore> {
|
||||
|
||||
@Override
|
||||
public StoreActionCategory getCategory() {
|
||||
return StoreActionCategory.OPEN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMajor() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(DataStoreEntryRef<IdentityStore> o) {
|
||||
var state = o.get().getStorePersistentState();
|
||||
if (state instanceof SystemState systemState) {
|
||||
return (systemState.getShellDialect() == null
|
||||
|| systemState.getShellDialect().getDumbMode().supportsAnyPossibleInteraction())
|
||||
&& (systemState.getTtyState() == null || systemState.getTtyState() == ShellTtyState.NONE);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(DataStoreEntryRef<IdentityStore> store) {
|
||||
return AppI18n.observable("applyIdentityToHost");
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(DataStoreEntryRef<IdentityStore> store) {
|
||||
return new LabelGraphic.IconGraphic("mdi2e-export");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getApplicableClass() {
|
||||
return IdentityStore.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractAction createAction(DataStoreEntryRef<IdentityStore> ref) {
|
||||
return Action.builder().ref(ref).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "applyIdentity";
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends StoreAction<IdentityStore> {
|
||||
|
||||
@Override
|
||||
public void executeImpl() {
|
||||
if (ref.getStore().getSshIdentity() != null && !(ref.getStore().getSshIdentity() instanceof NoIdentityStrategy) && ref.getStore().getSshIdentity().getPublicKey() == null) {
|
||||
AppDialog.confirm("identityApplyMissingPublicKey");
|
||||
StoreCreationDialog.showEdit(ref.get());
|
||||
return;
|
||||
}
|
||||
|
||||
IdentityApplyDialog.show(ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
package io.xpipe.ext.base.identity;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.base.ButtonComp;
|
||||
import io.xpipe.app.comp.base.ChoicePaneComp;
|
||||
import io.xpipe.app.comp.base.IconButtonComp;
|
||||
import io.xpipe.app.comp.base.InputGroupComp;
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
import io.xpipe.app.platform.OptionsChoiceBuilder;
|
||||
import io.xpipe.app.secret.EncryptedValue;
|
||||
@@ -11,10 +19,14 @@ import io.xpipe.ext.base.identity.ssh.SshIdentityStrategyChoiceConfig;
|
||||
|
||||
import javafx.beans.property.*;
|
||||
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.layout.HBox;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Value
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@@ -41,6 +53,26 @@ public class IdentityChoiceBuilder {
|
||||
return i.build();
|
||||
}
|
||||
|
||||
public static OptionsBuilder keyAuthChoice(Property<SshIdentityStrategy> identity, SshIdentityStrategyChoiceConfig config) {
|
||||
return OptionsChoiceBuilder.builder()
|
||||
.allowNull(false)
|
||||
.property(identity)
|
||||
.customConfiguration(config)
|
||||
.available(SshIdentityStrategy.getSubclasses())
|
||||
.transformer(entryComboBox -> {
|
||||
var button = new ButtonComp(null, new LabelGraphic.IconGraphic("mdi2k-key-plus"), () -> {
|
||||
ProcessControlProvider.get().showSshKeygenDialog(null, identity);
|
||||
});
|
||||
button.tooltipKey("generateKey");
|
||||
var comboComp = Comp.of(() -> entryComboBox);
|
||||
var hbox = new InputGroupComp(List.of(comboComp, button));
|
||||
hbox.setMainReference(comboComp);
|
||||
return hbox.createRegion();
|
||||
})
|
||||
.build()
|
||||
.build();
|
||||
}
|
||||
|
||||
public OptionsBuilder build() {
|
||||
var existing = identity.getValue();
|
||||
var user = new SimpleStringProperty(
|
||||
@@ -88,15 +120,7 @@ public class IdentityChoiceBuilder {
|
||||
options.name("keyAuthentication")
|
||||
.description("keyAuthenticationDescription")
|
||||
.documentationLink(DocumentationLink.SSH_KEYS)
|
||||
.sub(
|
||||
OptionsChoiceBuilder.builder()
|
||||
.allowNull(false)
|
||||
.property(identityStrategy)
|
||||
.customConfiguration(sshIdentityChoiceConfig)
|
||||
.available(SshIdentityStrategy.getSubclasses())
|
||||
.build()
|
||||
.build(),
|
||||
identityStrategy)
|
||||
.sub(keyAuthChoice(identityStrategy, sshIdentityChoiceConfig), identityStrategy)
|
||||
.nonNullIf(inPlaceSelected)
|
||||
.disable(refSelected)
|
||||
.hide(refSelected);
|
||||
|
||||
@@ -41,12 +41,6 @@ public abstract class IdentityStoreProvider implements DataStoreProvider {
|
||||
@Override
|
||||
public ObservableValue<String> informationString(StoreSection section) {
|
||||
var st = (IdentityStore) section.getWrapper().getStore().getValue();
|
||||
var user = st.getUsername().hasUser()
|
||||
? st.getUsername().getFixedUsername().map(s -> "User " + s).orElse("User")
|
||||
: "Anonymous User";
|
||||
var s = user
|
||||
+ (st.getPassword() == null || st.getPassword() instanceof SecretNoneStrategy ? "" : " + Password")
|
||||
+ (st.getSshIdentity() == null || st.getSshIdentity() instanceof NoIdentityStrategy ? "" : " + Key");
|
||||
return new SimpleStringProperty(s);
|
||||
return new SimpleStringProperty(IdentitySummary.createSummary(st));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package io.xpipe.ext.base.identity;
|
||||
|
||||
import io.xpipe.app.secret.SecretNoneStrategy;
|
||||
import io.xpipe.ext.base.identity.ssh.NoIdentityStrategy;
|
||||
|
||||
public class IdentitySummary {
|
||||
|
||||
public static String createSummary(IdentityStore st) {
|
||||
var user = st.getUsername().hasUser()
|
||||
? st.getUsername().getFixedUsername().map(s -> "User " + s).orElse("User")
|
||||
: "Anonymous User";
|
||||
var s = user
|
||||
+ (st.getPassword() == null || st.getPassword() instanceof SecretNoneStrategy ? "" : " + Password")
|
||||
+ (st.getSshIdentity() == null || st.getSshIdentity() instanceof NoIdentityStrategy ? "" : " + Key");
|
||||
return s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package io.xpipe.ext.base.identity;
|
||||
|
||||
import io.xpipe.app.ext.HostAddress;
|
||||
import io.xpipe.ext.base.host.HostAddressStore;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface IdentitySwitchStore extends HostAddressStore {
|
||||
|
||||
IdentityValue getIdentity();
|
||||
|
||||
IdentitySwitchStore withIdentity(IdentityValue identity);
|
||||
}
|
||||
@@ -64,15 +64,7 @@ public class LocalIdentityStoreProvider extends IdentityStoreProvider {
|
||||
.name("keyAuthentication")
|
||||
.description("keyAuthenticationDescription")
|
||||
.documentationLink(DocumentationLink.SSH_KEYS)
|
||||
.sub(
|
||||
OptionsChoiceBuilder.builder()
|
||||
.allowNull(false)
|
||||
.property(identity)
|
||||
.customConfiguration(sshIdentityChoiceConfig)
|
||||
.available(SshIdentityStrategy.getSubclasses())
|
||||
.build()
|
||||
.build(),
|
||||
identity)
|
||||
.sub(IdentityChoiceBuilder.keyAuthChoice(identity, sshIdentityChoiceConfig), identity)
|
||||
.bind(
|
||||
() -> {
|
||||
return LocalIdentityStore.builder()
|
||||
|
||||
@@ -96,15 +96,7 @@ public class SyncedIdentityStoreProvider extends IdentityStoreProvider {
|
||||
.name("keyAuthentication")
|
||||
.description("keyAuthenticationDescription")
|
||||
.documentationLink(DocumentationLink.SSH_KEYS)
|
||||
.sub(
|
||||
OptionsChoiceBuilder.builder()
|
||||
.allowNull(false)
|
||||
.property(identity)
|
||||
.customConfiguration(sshIdentityChoiceConfig)
|
||||
.available(SshIdentityStrategy.getSubclasses())
|
||||
.build()
|
||||
.build(),
|
||||
identity)
|
||||
.sub(IdentityChoiceBuilder.keyAuthChoice(identity, sshIdentityChoiceConfig), identity)
|
||||
.check(val -> Validator.create(val, AppI18n.observable("keyNotSynced"), identity, i -> {
|
||||
var wrong = i instanceof KeyFileStrategy f
|
||||
&& f.getFile() != null
|
||||
|
||||
@@ -94,4 +94,9 @@ public class CustomPkcs11LibraryStrategy implements SshIdentityStrategy {
|
||||
new KeyValue("IdentityFile", "none"),
|
||||
new KeyValue("IdentityAgent", "none"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPublicKey() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
package io.xpipe.ext.base.identity.ssh;
|
||||
|
||||
import atlantafx.base.theme.Styles;
|
||||
import io.xpipe.app.comp.base.ButtonComp;
|
||||
import io.xpipe.app.comp.base.InputGroupComp;
|
||||
import io.xpipe.app.comp.base.TextAreaComp;
|
||||
import io.xpipe.app.comp.base.TextFieldComp;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.AppSystemInfo;
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.ext.ValidationException;
|
||||
import io.xpipe.app.platform.ClipboardHelper;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
import io.xpipe.app.platform.OptionsChoiceBuilder;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
@@ -13,6 +21,7 @@ import io.xpipe.app.util.LocalFileTracker;
|
||||
import io.xpipe.app.util.Validators;
|
||||
import io.xpipe.core.*;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
@@ -22,6 +31,7 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
@@ -36,10 +46,11 @@ public class InPlaceKeyStrategy implements SshIdentityStrategy {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static OptionsBuilder createOptions(Property<InPlaceKeyStrategy> p, SshIdentityStrategyChoiceConfig config) {
|
||||
var key = new SimpleStringProperty(
|
||||
p.getValue().getKey() != null ? p.getValue().getKey().getSecretValue() : null);
|
||||
var keyPasswordProperty =
|
||||
new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getPassword() : null);
|
||||
var options = new OptionsBuilder();
|
||||
|
||||
var key = options.map(p, InPlaceKeyStrategy::getKey, SecretValue::getSecretValue);
|
||||
var publicKey = options.map(p, InPlaceKeyStrategy::getPublicKey);
|
||||
var keyPasswordProperty = options.map(p, InPlaceKeyStrategy::getPassword);
|
||||
|
||||
var passwordChoice = OptionsChoiceBuilder.builder()
|
||||
.allowNull(false)
|
||||
@@ -49,8 +60,28 @@ public class InPlaceKeyStrategy implements SshIdentityStrategy {
|
||||
.available(SecretRetrievalStrategy.getSubclasses())
|
||||
.build()
|
||||
.build();
|
||||
var publicKeyField = new TextFieldComp(publicKey).apply(struc -> {
|
||||
struc.get().promptTextProperty().bind(Bindings.createStringBinding(() -> {
|
||||
return "ssh-... ABCDEF.... (" + AppI18n.get("publicKeyGenerateNotice") + ")";
|
||||
}, AppI18n.activeLanguage()));
|
||||
struc.get().setEditable(false);
|
||||
});
|
||||
var generateButton = new ButtonComp(null, new LabelGraphic.IconGraphic("mdi2c-cog-refresh-outline"), () -> {
|
||||
var generated = ProcessControlProvider.get().generatePublicSshKey(InPlaceSecretValue.of(key.get()), keyPasswordProperty.get());
|
||||
if (generated != null) {
|
||||
publicKey.set(generated);
|
||||
}
|
||||
}).tooltipKey("generatePublicKey").disable(key.isNull().or(publicKey.isNotNull()).or(keyPasswordProperty.isNull()));
|
||||
var copyButton = new ButtonComp(null, new FontIcon("mdi2c-clipboard-multiple-outline"), () -> {
|
||||
ClipboardHelper.copyText(publicKey.get());
|
||||
})
|
||||
.disable(publicKey.isNull())
|
||||
.tooltipKey("copyPublicKey");
|
||||
|
||||
return new OptionsBuilder()
|
||||
var publicKeyBox = new InputGroupComp(List.of(publicKeyField, copyButton, generateButton));
|
||||
publicKeyBox.setMainReference(publicKeyField);
|
||||
|
||||
return options
|
||||
.nameAndDescription("inPlaceKeyText")
|
||||
.addComp(
|
||||
new TextAreaComp(key).apply(struc -> {
|
||||
@@ -59,8 +90,10 @@ public class InPlaceKeyStrategy implements SshIdentityStrategy {
|
||||
"""
|
||||
-----BEGIN ... PRIVATE KEY-----
|
||||
|
||||
-----END ... PRIVATE KEY-----
|
||||
|
||||
-----END ... PRIVATE KEY-----
|
||||
""");
|
||||
struc.getTextArea().setPrefRowCount(4);
|
||||
}),
|
||||
key)
|
||||
.nonNull()
|
||||
@@ -68,16 +101,22 @@ public class InPlaceKeyStrategy implements SshIdentityStrategy {
|
||||
.description("sshConfigHost.identityPassphraseDescription")
|
||||
.sub(passwordChoice, keyPasswordProperty)
|
||||
.nonNull()
|
||||
.nameAndDescription("inPlacePublicKey")
|
||||
.addComp(
|
||||
publicKeyBox,
|
||||
publicKey)
|
||||
.bind(
|
||||
() -> {
|
||||
return new InPlaceKeyStrategy(
|
||||
key.get() != null ? InPlaceSecretValue.of(key.get()) : null,
|
||||
keyPasswordProperty.get());
|
||||
key.getValue() != null ? InPlaceSecretValue.of(key.getValue()) : null,
|
||||
publicKey.get(),
|
||||
keyPasswordProperty.getValue());
|
||||
},
|
||||
p);
|
||||
}
|
||||
|
||||
SecretValue key;
|
||||
String publicKey;
|
||||
SecretRetrievalStrategy password;
|
||||
|
||||
public void checkComplete() throws ValidationException {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package io.xpipe.ext.base.identity.ssh;
|
||||
|
||||
import io.xpipe.app.comp.base.ContextualFileReferenceChoiceComp;
|
||||
import io.xpipe.app.comp.base.ContextualFileReferenceSync;
|
||||
import io.xpipe.app.comp.base.*;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.AppSystemInfo;
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.ext.ValidationException;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.platform.ClipboardHelper;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
import io.xpipe.app.platform.OptionsChoiceBuilder;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
@@ -13,11 +16,15 @@ import io.xpipe.app.secret.SecretRetrievalStrategy;
|
||||
import io.xpipe.app.secret.SecretStrategyChoiceConfig;
|
||||
import io.xpipe.app.storage.ContextualFileReference;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.app.util.Validators;
|
||||
import io.xpipe.core.FilePath;
|
||||
import io.xpipe.core.InPlaceSecretValue;
|
||||
import io.xpipe.core.KeyValue;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
@@ -27,7 +34,10 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
@Value
|
||||
@@ -62,6 +72,7 @@ public class KeyFileStrategy implements SshIdentityStrategy {
|
||||
});
|
||||
var keyPasswordProperty =
|
||||
new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getPassword() : null);
|
||||
var publicKey = new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getPublicKey() : null);
|
||||
|
||||
var sync = ContextualFileReferenceSync.of(
|
||||
DataStorage.get().getDataDir().resolve("keys"),
|
||||
@@ -77,6 +88,45 @@ public class KeyFileStrategy implements SshIdentityStrategy {
|
||||
.build()
|
||||
.build();
|
||||
|
||||
var publicKeyField = new TextFieldComp(publicKey).apply(struc -> {
|
||||
struc.get().promptTextProperty().bind(Bindings.createStringBinding(() -> {
|
||||
return "ssh-... ABCDEF.... (" + AppI18n.get("publicKeyGenerateNotice") + ")";
|
||||
}, AppI18n.activeLanguage()));
|
||||
struc.get().setEditable(false);
|
||||
});
|
||||
var generateButton = new ButtonComp(null, new LabelGraphic.IconGraphic("mdi2c-cog-refresh-outline"), () -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
Path path = keyPath.get().asLocalPath();
|
||||
if (!Files.exists(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var pubKeyPath = Path.of(path + ".pub");
|
||||
if (Files.exists(pubKeyPath)) {
|
||||
var contents = Files.readString(pubKeyPath).strip();
|
||||
Platform.runLater(() -> {
|
||||
publicKey.set(contents);
|
||||
});
|
||||
}
|
||||
|
||||
var contents = Files.readAllBytes(path);
|
||||
var generated = ProcessControlProvider.get().generatePublicSshKey(InPlaceSecretValue.of(contents), keyPasswordProperty.get());
|
||||
if (generated != null) {
|
||||
Platform.runLater(() -> {
|
||||
publicKey.set(generated);
|
||||
});
|
||||
}
|
||||
});
|
||||
}).tooltipKey("generatePublicKey").disable(keyPath.isNull().or(publicKey.isNotNull()).or(keyPasswordProperty.isNull()));
|
||||
var copyButton = new ButtonComp(null, new FontIcon("mdi2c-clipboard-multiple-outline"), () -> {
|
||||
ClipboardHelper.copyText(publicKey.get());
|
||||
})
|
||||
.disable(publicKey.isNull())
|
||||
.tooltipKey("copyPublicKey");
|
||||
|
||||
var publicKeyBox = new InputGroupComp(List.of(publicKeyField, copyButton, generateButton));
|
||||
publicKeyBox.setMainReference(publicKeyField);
|
||||
|
||||
return new OptionsBuilder()
|
||||
.name("location")
|
||||
.description("locationDescription")
|
||||
@@ -94,16 +144,21 @@ public class KeyFileStrategy implements SshIdentityStrategy {
|
||||
.description("sshConfigHost.identityPassphraseDescription")
|
||||
.sub(passwordChoice, keyPasswordProperty)
|
||||
.nonNull()
|
||||
.nameAndDescription("inPlacePublicKey")
|
||||
.addComp(
|
||||
publicKeyBox,
|
||||
publicKey)
|
||||
.bind(
|
||||
() -> {
|
||||
return new KeyFileStrategy(
|
||||
ContextualFileReference.of(keyPath.get()), keyPasswordProperty.get());
|
||||
ContextualFileReference.of(keyPath.get()), keyPasswordProperty.get(), publicKey.get());
|
||||
},
|
||||
p);
|
||||
}
|
||||
|
||||
ContextualFileReference file;
|
||||
SecretRetrievalStrategy password;
|
||||
String publicKey;
|
||||
|
||||
public void checkComplete() throws ValidationException {
|
||||
Validators.nonNull(file);
|
||||
|
||||
@@ -28,4 +28,14 @@ public class NoIdentityStrategy implements SshIdentityStrategy {
|
||||
new KeyValue("IdentityFile", "none"),
|
||||
new KeyValue("PKCS11Provider", "none"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPublicKey() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean providesKey() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ public interface SshIdentityStrategy {
|
||||
static List<Class<?>> getSubclasses() {
|
||||
var l = new ArrayList<Class<?>>();
|
||||
l.add(NoIdentityStrategy.class);
|
||||
l.add(KeyFileStrategy.class);
|
||||
l.add(InPlaceKeyStrategy.class);
|
||||
l.add(KeyFileStrategy.class);
|
||||
l.add(OpenSshAgentStrategy.class);
|
||||
if (OsType.ofLocal() != OsType.WINDOWS) {
|
||||
l.add(CustomAgentStrategy.class);
|
||||
@@ -85,6 +85,10 @@ public interface SshIdentityStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
default boolean providesKey() {
|
||||
return true;
|
||||
}
|
||||
|
||||
default void checkComplete() throws ValidationException {}
|
||||
|
||||
void prepareParent(ShellControl parent) throws Exception;
|
||||
@@ -96,4 +100,6 @@ public interface SshIdentityStrategy {
|
||||
default SecretRetrievalStrategy getAskpassStrategy() {
|
||||
return new SecretNoneStrategy();
|
||||
}
|
||||
|
||||
String getPublicKey();
|
||||
}
|
||||
|
||||
@@ -79,4 +79,9 @@ public class YubikeyPivStrategy implements SshIdentityStrategy {
|
||||
new KeyValue("IdentityFile", "none"),
|
||||
new KeyValue("IdentityAgent", "none"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPublicKey() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import io.xpipe.ext.base.host.AbstractHostCreationActionProvider;
|
||||
import io.xpipe.ext.base.host.AbstractHostStoreProvider;
|
||||
import io.xpipe.ext.base.host.HostAddressSwitchBranchProvider;
|
||||
import io.xpipe.ext.base.identity.*;
|
||||
import io.xpipe.ext.base.identity.IdentityApplyHubLeafProvider;
|
||||
import io.xpipe.ext.base.script.*;
|
||||
import io.xpipe.ext.base.service.*;
|
||||
import io.xpipe.ext.base.store.*;
|
||||
@@ -35,6 +36,7 @@ open module io.xpipe.ext.base {
|
||||
requires javafx.base;
|
||||
|
||||
provides ActionProvider with
|
||||
IdentityApplyHubLeafProvider,
|
||||
AbstractHostCreationActionProvider,
|
||||
HostAddressSwitchBranchProvider,
|
||||
LocalIdentityConvertHubLeafProvider,
|
||||
|
||||
Generated
+4
-1
@@ -149,4 +149,7 @@ tailscale=Tailscale
|
||||
netbird=Netbird
|
||||
appImageDist=AppImage
|
||||
nixDist=Nix
|
||||
antigravity=Antigravity
|
||||
antigravity=Antigravity
|
||||
rsa=RSA
|
||||
ed25519=ED25519
|
||||
ed25519Sk=ED25519 (FIDO2)
|
||||
|
||||
Generated
+60
-2
@@ -808,7 +808,8 @@ location=Location
|
||||
keyAuthentication=Key-based authentication
|
||||
keyAuthenticationDescription=The authentication method to use if key-based authentication is required
|
||||
locationDescription=The file path of your corresponding private key
|
||||
keyFile=Key file
|
||||
#force
|
||||
keyFile=Local key file
|
||||
keyPassword=Passphrase
|
||||
key=Key
|
||||
yubikeyPiv=Yubikey PIV
|
||||
@@ -1682,7 +1683,8 @@ largeFileWarningTitle=Large file edit
|
||||
largeFileWarningContent=The file you want to edit is quite large with $SIZE$. Do you really want to open this file in your text editor?
|
||||
rdpAskpassUser=RDP username for host $HOST$
|
||||
rdpAskpassPassword=Password for user $USER$
|
||||
inPlaceKey=In-place key
|
||||
#force
|
||||
inPlaceKey=Key
|
||||
inPlaceKeyText=Private key content
|
||||
inPlaceKeyTextDescription=The private key contents
|
||||
netbirdSelfhosted=Self-hosted netbird instance
|
||||
@@ -1708,3 +1710,59 @@ tags=Tags
|
||||
tag=Tag
|
||||
addNewTag=Create new tag
|
||||
createTag=Create tag ...
|
||||
inPlacePublicKey=Public key
|
||||
inPlacePublicKeyDescription=The associated public key for the private key
|
||||
sshKeygenTitle=Generate new SSH key
|
||||
sshKeygenAlgorithm=Algorithm
|
||||
sshKeygenAlgorithmDescription=The asymmetric keygen algorithm to use for the key
|
||||
rsaBits=Bits
|
||||
rsaBitsDescription=Number of bits in generated key
|
||||
sshKeygenComment=Comment
|
||||
sshKeygenCommentDescription=The optional comment for this key
|
||||
sshKeygenPassphrase=Passphrase
|
||||
sshKeygenPassphraseDescription=The optional passphrase for this key
|
||||
ed25519SkResident=Make resident key
|
||||
ed25519SkResidentDescription=Store private key on the hardware security key
|
||||
ed25519SkResidentKeyName=Resident key label
|
||||
ed25519SkResidentKeyNameDescription=Give the resident key a label. Needed when storing multiple keys on the security key
|
||||
ed25519SkPinRequired=Require PIN
|
||||
ed25519SkPinRequiredDescription=Require PIN entry on use
|
||||
ed25519SkUserPresenceRequired=Require user presence
|
||||
ed25519SkUserPresenceRequiredDescription=Require touch or similar on use. Some security keys require this to be enabled
|
||||
copyPublicKey=Copy public key
|
||||
generatePublicKey=Generate public key
|
||||
publicKeyGenerateNotice=Can be generated from public key
|
||||
identityApplyTargetHost=Target
|
||||
identityApplyTargetHostDescription=The system to apply the identity to
|
||||
identityApplyAuthorizedHost=SSH key authorized
|
||||
identityApplyAuthorizedHostDescription=The SSH key is added to authorized hosts file
|
||||
identityApplyAuthorizedHostButton=Append key to file
|
||||
applyIdentityToHost=Apply identity to host ...
|
||||
identityApplyMissingPublicKeyTitle=Missing public key
|
||||
identityApplyMissingPublicKeyContent=The identity's SSH key does not have a public key associated with it. Check out the configuration for details.
|
||||
valid=Valid
|
||||
notValid=Not valid
|
||||
warning=Warning
|
||||
identityApplyTitle=Apply identity
|
||||
identityApplyConfigPasswordEnabled=Password auth enabled
|
||||
identityApplyConfigPasswordEnabledDescription=Password authentication is still enabled in the sshd config
|
||||
identityApplyConfigPasswordDisabled=Password auth disabled
|
||||
identityApplyConfigPasswordDisabledDescription=Password authentication is disabled in the sshd config
|
||||
identityApplyConfigKeyEnabled=Key auth enabled
|
||||
identityApplyConfigKeyEnabledDescription=Key-based authentication is still enabled in the sshd config
|
||||
identityApplyConfigKeyDisabled=Key auth disabled
|
||||
identityApplyConfigKeyDisabledDescription=Key-based authentication is disabled in the sshd config
|
||||
identityApplyConfigRootDisabledWarning=Root login disabled
|
||||
identityApplyConfigRootDisabledWarningDescription=Root login is not enabled in the sshd config
|
||||
identityApplyConfigAdminWarning=Administrator keys configured
|
||||
identityApplyConfigAdminWarningDescription=The key might have to be added to administrators_authorized_keys instead for admin users
|
||||
identityApplyEditConfig=Edit config
|
||||
identityApplyEditConfigDescription=Open the sshd config in the file browser to fix any issues
|
||||
identityApplyEditAuthorizedKeys=Edit authorized keys
|
||||
identityApplyEditAuthorizedKeysDescription=Open the authorized_keys file in the file browser to edit or remove other keys
|
||||
identityApplyEditConfigButton=Open sshd_config
|
||||
identityApplyEditAuthorizedKeysButton=Open authorized_keys
|
||||
identityApplySetStoreIdentity=Connection identity set
|
||||
identityApplySetStoreIdentityDescription=The identity has been configured to be used by the connection
|
||||
identityApplySetStoreIdentityButton=Apply identity
|
||||
generateKey=Generate key
|
||||
|
||||
Reference in New Issue
Block a user