mirror of
https://github.com/xpipe-io/xpipe.git
synced 2026-05-04 03:40:32 +00:00
Merge branch group-vault
This commit is contained in:
@@ -46,8 +46,12 @@ public class AskpassExchangeImpl extends AskpassExchange {
|
||||
return Response.builder().value(InPlaceSecretValue.of("")).build();
|
||||
}
|
||||
|
||||
var prompt = msg.getPrompt();
|
||||
// sudo-rs uses a different prefix which we don't really need
|
||||
prompt = prompt.replace("[sudo: authenticate]", "[sudo]");
|
||||
|
||||
if (msg.getRequest() == null) {
|
||||
var r = AskpassAlert.queryRaw(msg.getPrompt(), null, true);
|
||||
var r = AskpassAlert.queryRaw(prompt, null, true);
|
||||
return Response.builder().value(r.getSecret()).build();
|
||||
}
|
||||
|
||||
@@ -59,7 +63,7 @@ public class AskpassExchangeImpl extends AskpassExchange {
|
||||
}
|
||||
|
||||
var p = found.get();
|
||||
var secret = p.process(msg.getPrompt());
|
||||
var secret = p.process(prompt);
|
||||
if (p.getState() != SecretQueryState.NORMAL) {
|
||||
var ex = new BeaconClientException(SecretQueryState.toErrorMessage(p.getState()));
|
||||
ErrorEventFactory.preconfigure(ErrorEventFactory.fromThrowable(ex).ignore());
|
||||
|
||||
@@ -100,7 +100,7 @@ public class AppMainWindowContentComp extends SimpleComp {
|
||||
});
|
||||
|
||||
var loadingTextCounter = new SimpleIntegerProperty();
|
||||
GlobalTimer.scheduleUntil(Duration.ofMillis(300), false, () -> {
|
||||
GlobalTimer.scheduleUntil(Duration.ofMillis(500), false, () -> {
|
||||
if (loaded.getValue() != null) {
|
||||
return true;
|
||||
}
|
||||
@@ -146,6 +146,16 @@ public class AppMainWindowContentComp extends SimpleComp {
|
||||
PlatformThread.runNestedLoopIteration();
|
||||
struc.show();
|
||||
TrackEvent.info("Window content node shown");
|
||||
} else if (!pane.getChildren().contains(vbox)) {
|
||||
loadingTextCounter.set(3);
|
||||
TrackEvent.info("Window content node removed");
|
||||
PlatformThread.runNestedLoopIteration();
|
||||
pane.getChildren().clear();
|
||||
pane.getStyleClass().add("background");
|
||||
pane.getChildren().add(vbox);
|
||||
sidebarPresent.set(false);
|
||||
loadingAnimation.start();
|
||||
PlatformThread.runNestedLoopIteration();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ package io.xpipe.app.comp.base;
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.SimpleCompStructure;
|
||||
import io.xpipe.app.core.AppCache;
|
||||
import io.xpipe.app.core.AppProperties;
|
||||
import io.xpipe.app.core.AppResources;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.platform.MarkdownHelper;
|
||||
import io.xpipe.app.platform.PlatformThread;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.process.ShellTemp;
|
||||
import io.xpipe.app.util.Hyperlinks;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
@@ -34,7 +34,7 @@ import java.util.function.UnaryOperator;
|
||||
public class MarkdownComp extends Comp<CompStructure<StackPane>> {
|
||||
|
||||
private static Boolean WEB_VIEW_SUPPORTED;
|
||||
private static Path TEMP;
|
||||
private static Path DIR;
|
||||
private final ObservableValue<String> markdown;
|
||||
private final UnaryOperator<String> htmlTransformation;
|
||||
private final boolean bodyPadding;
|
||||
@@ -53,8 +53,8 @@ public class MarkdownComp extends Comp<CompStructure<StackPane>> {
|
||||
}
|
||||
|
||||
private Path getHtmlFile(String markdown) {
|
||||
if (TEMP == null) {
|
||||
TEMP = ShellTemp.getLocalTempDataDirectory("webview");
|
||||
if (DIR == null) {
|
||||
DIR = AppCache.getBasePath().resolve("md");
|
||||
}
|
||||
|
||||
if (markdown == null) {
|
||||
@@ -68,7 +68,7 @@ public class MarkdownComp extends Comp<CompStructure<StackPane>> {
|
||||
} else {
|
||||
hash = markdown.hashCode();
|
||||
}
|
||||
var file = TEMP.resolve("md-" + hash + ".html");
|
||||
var file = DIR.resolve("md-" + hash + ".html");
|
||||
if (Files.exists(file)) {
|
||||
return file;
|
||||
}
|
||||
@@ -94,7 +94,7 @@ public class MarkdownComp extends Comp<CompStructure<StackPane>> {
|
||||
wv.setPageFill(Color.TRANSPARENT);
|
||||
wv.getEngine()
|
||||
.setUserDataDirectory(
|
||||
AppProperties.get().getDataDir().resolve("webview").toFile());
|
||||
AppCache.getBasePath().resolve("webview").toFile());
|
||||
var theme = AppPrefs.get() != null
|
||||
&& AppPrefs.get().theme().getValue() != null
|
||||
&& AppPrefs.get().theme().getValue().isDark()
|
||||
|
||||
@@ -214,12 +214,10 @@ public class ModalOverlayComp extends SimpleComp {
|
||||
max.set(d);
|
||||
}
|
||||
});
|
||||
}
|
||||
node.minWidthProperty().bind(max);
|
||||
buttonBar.getChildren().add(node);
|
||||
if (o instanceof ModalButton) {
|
||||
node.minWidthProperty().bind(max);
|
||||
node.prefHeightProperty().bind(buttonBar.heightProperty());
|
||||
}
|
||||
buttonBar.getChildren().add(node);
|
||||
}
|
||||
content.getChildren().add(buttonBar);
|
||||
AppFontSizes.apply(buttonBar, sizes -> {
|
||||
|
||||
@@ -94,16 +94,16 @@ public class SecretFieldComp extends Comp<SecretFieldComp.Structure> {
|
||||
}
|
||||
|
||||
field.setText(n != null ? n.getSecretValue() : null);
|
||||
});
|
||||
|
||||
var capslock = Platform.isKeyLocked(KeyCode.CAPS);
|
||||
if (!capslock.orElse(false)) {
|
||||
capsPopover.hide();
|
||||
return;
|
||||
}
|
||||
var capslock = Platform.isKeyLocked(KeyCode.CAPS);
|
||||
if (!capslock.orElse(false)) {
|
||||
capsPopover.hide();
|
||||
return;
|
||||
}
|
||||
if (!capsPopover.isShowing() && field.getScene() != null) {
|
||||
capsPopover.show(field);
|
||||
}
|
||||
capsPopover.show(field);
|
||||
}
|
||||
});
|
||||
});
|
||||
HBox.setHgrow(field, Priority.ALWAYS);
|
||||
|
||||
|
||||
@@ -52,6 +52,10 @@ public class AppI18n {
|
||||
return INSTANCE.observableImpl(s, vars);
|
||||
}
|
||||
|
||||
public static ObservableValue<String> observable(ObservableValue<String> s, Object... vars) {
|
||||
return BindingsHelper.flatMap(s, v -> INSTANCE.observableImpl(v, vars));
|
||||
}
|
||||
|
||||
public static String get(String s, Object... vars) {
|
||||
return INSTANCE.getLocalised(s, vars);
|
||||
}
|
||||
|
||||
@@ -123,6 +123,7 @@ public class AppBaseMode extends AppOperationMode {
|
||||
syncPrefsLoaded.countDown();
|
||||
AppMainWindow.loadingText("loadingConnections");
|
||||
DataStorage.init();
|
||||
AppPrefs.initStorage();
|
||||
storageLoaded.countDown();
|
||||
AppMcpServer.init();
|
||||
StoreViewState.init();
|
||||
|
||||
@@ -129,7 +129,7 @@ public class AppMainWindow {
|
||||
}
|
||||
|
||||
public static void loadingText(String key) {
|
||||
loadingText.setValue(key != null && AppI18n.get() != null ? AppI18n.get(key) : "...");
|
||||
loadingText.setValue(key != null && AppI18n.get() != null ? AppI18n.get(key) : "?");
|
||||
}
|
||||
|
||||
public static synchronized void initContent() {
|
||||
@@ -146,6 +146,13 @@ public class AppMainWindow {
|
||||
});
|
||||
}
|
||||
|
||||
public static synchronized void resetContent() {
|
||||
PlatformThread.runLaterIfNeededBlocking(() -> {
|
||||
loadingText.setValue(AppI18n.get("savingChanges"));
|
||||
loadedContent.setValue(null);
|
||||
});
|
||||
}
|
||||
|
||||
public static AppMainWindow get() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@@ -164,6 +164,10 @@ public class OptionsBuilder {
|
||||
return name(key).description(key + "Description");
|
||||
}
|
||||
|
||||
public OptionsBuilder nameAndDescription(ObservableValue<String> key) {
|
||||
return name(AppI18n.observable(key)).description(AppI18n.observable(BindingsHelper.map(key, k -> k + "Description")));
|
||||
}
|
||||
|
||||
public OptionsBuilder subAdvanced(OptionsBuilder builder) {
|
||||
name("advanced");
|
||||
subExpandable("showAdvancedOptions", builder);
|
||||
@@ -374,6 +378,7 @@ public class OptionsBuilder {
|
||||
public OptionsBuilder name(ObservableValue<String> name) {
|
||||
finishCurrent();
|
||||
this.name = name;
|
||||
lastNameReference = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ public class OptionsChoiceBuilder {
|
||||
|
||||
private final Property<?> property;
|
||||
private final List<Class<?>> available;
|
||||
private final List<Class<?>> selectable;
|
||||
private final Function<ComboBox<ChoicePaneComp.Entry>, Region> transformer;
|
||||
private final boolean allowNull;
|
||||
private final Object customConfiguration;
|
||||
|
||||
@@ -14,6 +14,8 @@ import io.xpipe.app.process.ShellScript;
|
||||
import io.xpipe.app.pwman.PasswordManager;
|
||||
import io.xpipe.app.rdp.ExternalRdpClient;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStorageGroupStrategy;
|
||||
import io.xpipe.app.storage.DataStorageUserHandler;
|
||||
import io.xpipe.app.terminal.ExternalTerminalType;
|
||||
import io.xpipe.app.terminal.TerminalMultiplexer;
|
||||
import io.xpipe.app.terminal.TerminalPrompt;
|
||||
@@ -40,6 +42,7 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.Value;
|
||||
import org.w3c.dom.UserDataHandler;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.util.*;
|
||||
@@ -60,6 +63,7 @@ public final class AppPrefs {
|
||||
|
||||
public static void initSynced() throws Exception {
|
||||
INSTANCE.loadSharedRemote();
|
||||
INSTANCE.fixSyncedValues();
|
||||
INSTANCE.encryptAllVaultData.addListener((observableValue, aBoolean, t1) -> {
|
||||
if (DataStorage.get() != null) {
|
||||
DataStorage.get().forceRewrite();
|
||||
@@ -67,6 +71,10 @@ public final class AppPrefs {
|
||||
});
|
||||
}
|
||||
|
||||
public static void initStorage() {
|
||||
INSTANCE.vaultAuthentication.set(DataStorageUserHandler.getInstance().getVaultAuthenticationType());
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
INSTANCE.save();
|
||||
|
||||
@@ -269,6 +277,16 @@ public final class AppPrefs {
|
||||
.log(false)
|
||||
.documentationLink(DocumentationLink.TERMINAL_PROMPT)
|
||||
.build());
|
||||
final ObjectProperty<VaultAuthentication> vaultAuthentication = new GlobalObjectProperty<>();
|
||||
|
||||
final ObjectProperty<DataStorageGroupStrategy> groupSecretStrategy = map(Mapping.builder()
|
||||
.property(new GlobalObjectProperty<>())
|
||||
.key("groupSecretStrategy")
|
||||
.valueClass(DataStorageGroupStrategy.class)
|
||||
.requiresRestart(true)
|
||||
.vaultSpecific(true)
|
||||
.licenseFeatureId("team")
|
||||
.build());
|
||||
final ObjectProperty<StartupBehaviour> startupBehaviour = map(Mapping.builder()
|
||||
.property(new GlobalObjectProperty<>(StartupBehaviour.GUI))
|
||||
.key("startupBehaviour")
|
||||
@@ -415,6 +433,14 @@ public final class AppPrefs {
|
||||
|
||||
private AppPrefs() {}
|
||||
|
||||
public ObservableValue<VaultAuthentication> vaultAuthentication() {
|
||||
return vaultAuthentication;
|
||||
}
|
||||
|
||||
public ObservableValue<DataStorageGroupStrategy> groupSecretStrategy() {
|
||||
return groupSecretStrategy;
|
||||
}
|
||||
|
||||
public ObservableStringValue notesTemplate() {
|
||||
return notesTemplate;
|
||||
}
|
||||
@@ -709,7 +735,10 @@ public final class AppPrefs {
|
||||
PlatformThread.runLaterIfNeededBlocking(() -> {
|
||||
writable.setValue(newValue);
|
||||
});
|
||||
save();
|
||||
|
||||
if (mapping.stream().anyMatch(m -> m.property == prop)) {
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
private void fixLocalValues() {
|
||||
@@ -751,6 +780,17 @@ public final class AppPrefs {
|
||||
PrefsProvider.getAll().forEach(prov -> prov.fixLocalValues());
|
||||
}
|
||||
|
||||
private void fixSyncedValues() {
|
||||
if (groupSecretStrategy.getValue() != null) {
|
||||
try {
|
||||
groupSecretStrategy.get().checkComplete();
|
||||
} catch (Exception e) {
|
||||
groupSecretStrategy.setValue(null);
|
||||
ErrorEventFactory.fromThrowable(e).omit().expected().handle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void initDefaultValues() {
|
||||
externalEditor.setValue(ExternalEditorType.determineDefault(externalEditor.get()));
|
||||
terminalType.set(ExternalTerminalType.determineDefault(terminalType.get()));
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package io.xpipe.app.prefs;
|
||||
|
||||
import io.xpipe.app.core.mode.AppOperationMode;
|
||||
import io.xpipe.app.ext.PrefsChoiceValue;
|
||||
import io.xpipe.core.XPipeDaemonMode;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum VaultAuthentication implements PrefsChoiceValue {
|
||||
USER("userAuth"),
|
||||
GROUP("groupAuth");
|
||||
|
||||
private final String id;
|
||||
}
|
||||
@@ -2,22 +2,30 @@ package io.xpipe.app.prefs;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.base.ButtonComp;
|
||||
import io.xpipe.app.comp.base.ChoiceComp;
|
||||
import io.xpipe.app.comp.base.ModalButton;
|
||||
import io.xpipe.app.comp.base.ModalOverlay;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.platform.GlobalObjectProperty;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
import io.xpipe.app.platform.OptionsChoiceBuilder;
|
||||
import io.xpipe.app.storage.DataStorageGroupStrategy;
|
||||
import io.xpipe.app.storage.DataStorageSyncHandler;
|
||||
import io.xpipe.app.storage.DataStorageUserHandler;
|
||||
import io.xpipe.app.util.DocumentationLink;
|
||||
import io.xpipe.app.util.LicenseProvider;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class VaultCategory extends AppPrefsCategory {
|
||||
|
||||
@Override
|
||||
@@ -57,34 +65,50 @@ public class VaultCategory extends AppPrefsCategory {
|
||||
var uh = DataStorageUserHandler.getInstance();
|
||||
var vaultTypeKey = uh.getUserCount() == 0
|
||||
? "Default"
|
||||
: uh.getUserCount() == 1
|
||||
: uh.getUserCount() == 1 && uh.getVaultAuthenticationType() != VaultAuthentication.GROUP
|
||||
? (uh.getActiveUser() != null && uh.getActiveUser().equals("legacy") ? "Legacy" : "Personal")
|
||||
: "Team";
|
||||
|
||||
var authChoice = ChoiceComp.ofTranslatable(prefs.vaultAuthentication, Arrays.asList(VaultAuthentication.values()), true);
|
||||
authChoice.apply(struc -> struc.get().setOpacity(1.0));
|
||||
authChoice.maxWidth(600);
|
||||
|
||||
builder.addTitle("vault")
|
||||
.sub(new OptionsBuilder()
|
||||
.name("vaultTypeName" + vaultTypeKey)
|
||||
.description("vaultTypeContent" + vaultTypeKey)
|
||||
.documentationLink(DocumentationLink.TEAM_VAULTS)
|
||||
.addComp(Comp.empty())
|
||||
.name("userManagement")
|
||||
.description(
|
||||
uh.getActiveUser() != null
|
||||
? "userManagementDescription"
|
||||
: "userManagementDescriptionEmpty")
|
||||
.licenseRequirement("team")
|
||||
.nameAndDescription("vaultAuthentication")
|
||||
.addComp(authChoice, prefs.vaultAuthentication)
|
||||
.nameAndDescription(Bindings.createStringBinding(() -> {
|
||||
var empty = uh.getUserCount() == 0;
|
||||
if (prefs.vaultAuthentication.get() == VaultAuthentication.GROUP) {
|
||||
return empty ? "groupManagementEmpty" : "groupManagement";
|
||||
}
|
||||
|
||||
return empty ? "userManagementEmpty" : "userManagement";
|
||||
}, prefs.vaultAuthentication))
|
||||
.addComp(uh.createOverview().maxWidth(getCompWidth()))
|
||||
.pref(prefs.groupSecretStrategy)
|
||||
.addComp(OptionsChoiceBuilder.builder().property(prefs.groupSecretStrategy)
|
||||
.allowNull(true).available(DataStorageGroupStrategy.getClasses())
|
||||
.build().build().buildComp().maxWidth(getCompWidth()),
|
||||
prefs.groupSecretStrategy)
|
||||
.nonNull()
|
||||
.hide(prefs.vaultAuthentication.isNotEqualTo(VaultAuthentication.GROUP))
|
||||
.nameAndDescription("syncVault")
|
||||
.addComp(new ButtonComp(AppI18n.observable("enableGitSync"), () -> AppPrefs.get()
|
||||
.selectCategory("vaultSync")))
|
||||
.hide(new SimpleBooleanProperty(
|
||||
DataStorageSyncHandler.getInstance().supportsSync()))
|
||||
.nameAndDescription("teamVaults")
|
||||
.addComp(Comp.empty())
|
||||
.licenseRequirement("team")
|
||||
.disable(!LicenseProvider.get().getFeature("team").isSupported())
|
||||
.hide(new SimpleBooleanProperty(uh.getUserCount() > 1))
|
||||
.nameAndDescription("syncTeamVaults")
|
||||
.addComp(new ButtonComp(AppI18n.observable("enableGitSync"), () -> AppPrefs.get()
|
||||
.selectCategory("vaultSync")))
|
||||
.licenseRequirement("team")
|
||||
.disable(!LicenseProvider.get().getFeature("team").isSupported())
|
||||
.hide(new SimpleBooleanProperty(
|
||||
DataStorageSyncHandler.getInstance().supportsSync())));
|
||||
.hide(uh.getUserCount() > 1)
|
||||
);
|
||||
builder.sub(new OptionsBuilder().pref(prefs.encryptAllVaultData).addToggle(encryptVault));
|
||||
return builder.buildComp();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package io.xpipe.app.process;
|
||||
|
||||
import io.xpipe.app.util.GlobalTimer;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.FilePath;
|
||||
|
||||
import java.io.InputStream;
|
||||
@@ -71,6 +73,21 @@ public interface CommandControl extends ProcessControl {
|
||||
|
||||
Optional<String> readStdoutIfPossible() throws Exception;
|
||||
|
||||
default void killOnTimeout(CountDown countDown) {
|
||||
GlobalTimer.scheduleUntil(Duration.ofSeconds(1), false, () -> {
|
||||
if (!isRunning(true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!countDown.countDown()) {
|
||||
kill();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
default boolean discardAndCheckExit() throws ProcessOutputException {
|
||||
try {
|
||||
discardOrThrow();
|
||||
|
||||
@@ -20,11 +20,12 @@ public class CountDown {
|
||||
return new CountDown();
|
||||
}
|
||||
|
||||
public synchronized void start(long millisecondsLeft) {
|
||||
public synchronized CountDown start(long millisecondsLeft) {
|
||||
this.millisecondsLeft = millisecondsLeft;
|
||||
this.maxMillis = millisecondsLeft;
|
||||
lastMillis = System.currentTimeMillis();
|
||||
active = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
|
||||
@@ -18,7 +18,10 @@ import javax.crypto.SecretKey;
|
||||
public class EncryptionToken {
|
||||
|
||||
private static EncryptionToken vaultToken;
|
||||
private static EncryptionToken groupToken;
|
||||
private static EncryptionToken userToken;
|
||||
|
||||
|
||||
private final String token;
|
||||
|
||||
@JsonIgnore
|
||||
|
||||
@@ -9,16 +9,9 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = SecretNoneStrategy.class),
|
||||
@JsonSubTypes.Type(value = SecretInPlaceStrategy.class),
|
||||
@JsonSubTypes.Type(value = SecretPromptStrategy.class),
|
||||
@JsonSubTypes.Type(value = SecretCustomCommandStrategy.class),
|
||||
@JsonSubTypes.Type(value = SecretPasswordManagerStrategy.class)
|
||||
})
|
||||
public interface SecretRetrievalStrategy {
|
||||
|
||||
static List<Class<?>> getSubclasses() {
|
||||
static List<Class<?>> getClasses() {
|
||||
var l = new ArrayList<Class<?>>();
|
||||
l.add(SecretNoneStrategy.class);
|
||||
l.add(SecretInPlaceStrategy.class);
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
package io.xpipe.app.storage;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import io.xpipe.app.comp.base.*;
|
||||
import io.xpipe.app.core.App;
|
||||
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.process.CountDown;
|
||||
import io.xpipe.app.process.ShellScript;
|
||||
import io.xpipe.app.secret.SecretPasswordManagerStrategy;
|
||||
import io.xpipe.app.secret.SecretQueryState;
|
||||
import io.xpipe.app.util.HttpHelper;
|
||||
import io.xpipe.app.util.Validators;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
|
||||
public interface DataStorageGroupStrategy {
|
||||
|
||||
static List<Class<?>> getClasses() {
|
||||
var l = new ArrayList<Class<?>>();
|
||||
l.add(PasswordManager.class);
|
||||
l.add(File.class);
|
||||
l.add(Command.class);
|
||||
l.add(HttpRequest.class);
|
||||
return l;
|
||||
}
|
||||
|
||||
default void checkComplete() throws ValidationException {}
|
||||
|
||||
String queryEncryptionSecret() throws Exception;
|
||||
|
||||
@JsonTypeName("passwordManager")
|
||||
@Builder
|
||||
@Jacksonized
|
||||
@Value
|
||||
public class PasswordManager implements DataStorageGroupStrategy {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static String getOptionsNameKey() {
|
||||
return "passwordManager";
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static OptionsBuilder createOptions(Property<PasswordManager> p) {
|
||||
var key = new SimpleStringProperty(p.getValue().getKey());
|
||||
|
||||
var prefs = AppPrefs.get();
|
||||
var content = new HorizontalComp(List.of(
|
||||
new TextFieldComp(key)
|
||||
.apply(struc -> struc.get()
|
||||
.promptTextProperty()
|
||||
.bind(Bindings.createStringBinding(
|
||||
() -> {
|
||||
return prefs.passwordManager()
|
||||
.getValue()
|
||||
!= null
|
||||
? prefs.passwordManager()
|
||||
.getValue()
|
||||
.getKeyPlaceholder()
|
||||
: "?";
|
||||
},
|
||||
prefs.passwordManager())))
|
||||
.hgrow(),
|
||||
new ButtonComp(null, new FontIcon("mdomz-settings"), () -> {
|
||||
AppPrefs.get().selectCategory("passwordManager");
|
||||
App.getApp().getStage().requestFocus();
|
||||
})
|
||||
.grow(false, true)))
|
||||
.apply(struc -> struc.get().setSpacing(10))
|
||||
.apply(struc -> struc.get().focusedProperty().addListener((c, o, n) -> {
|
||||
if (n) {
|
||||
struc.get().getChildren().getFirst().requestFocus();
|
||||
}
|
||||
}));
|
||||
|
||||
return new OptionsBuilder()
|
||||
.nameAndDescription("passwordManagerKey")
|
||||
.addString(key)
|
||||
.nonNull()
|
||||
.bind(
|
||||
() -> {
|
||||
return PasswordManager.builder().key(key.get()).build();
|
||||
},
|
||||
p);
|
||||
}
|
||||
|
||||
String key;
|
||||
|
||||
@Override
|
||||
public void checkComplete() throws ValidationException {
|
||||
Validators.nonNull(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String queryEncryptionSecret() throws Exception {
|
||||
var r = SecretPasswordManagerStrategy.builder().key(key).build().query().query("Group secret");
|
||||
return r.getState() == SecretQueryState.NORMAL ? r.getSecret().getSecretValue() : null;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonTypeName("file")
|
||||
@Builder
|
||||
@Jacksonized
|
||||
@Value
|
||||
public class File implements DataStorageGroupStrategy {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static String getOptionsNameKey() {
|
||||
return "fileSecret";
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static OptionsBuilder createOptions(Property<File> p) {
|
||||
var file = new SimpleObjectProperty<>(p.getValue().getFile() != null ? p.getValue().getFile().toLocalAbsoluteFilePath() : null);
|
||||
return new OptionsBuilder()
|
||||
.nameAndDescription("fileSecretChoice")
|
||||
.addComp(new ContextualFileReferenceChoiceComp(
|
||||
new ReadOnlyObjectWrapper<>(DataStorage.get().local().ref()),
|
||||
file, null, List.of(), e -> e.equals(DataStorage.get().local())), file)
|
||||
.nonNull()
|
||||
.bind(
|
||||
() -> {
|
||||
return File.builder().file(ContextualFileReference.of(file.get())).build();
|
||||
},
|
||||
p);
|
||||
}
|
||||
|
||||
ContextualFileReference file;
|
||||
|
||||
@Override
|
||||
public void checkComplete() throws ValidationException {
|
||||
Validators.nonNull(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String queryEncryptionSecret() throws Exception {
|
||||
var abs = file.toLocalAbsoluteFilePath().asLocalPath();
|
||||
if (!Files.exists(abs)) {
|
||||
throw ErrorEventFactory.expected(new IllegalArgumentException("Group key file " + file + " does not exist"));
|
||||
}
|
||||
|
||||
var read = Files.readString(abs);
|
||||
if (read.length() == 0) {
|
||||
throw ErrorEventFactory.expected(new IllegalArgumentException("Group key file " + file + " is empty"));
|
||||
}
|
||||
|
||||
return read;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonTypeName("command")
|
||||
@Builder
|
||||
@Jacksonized
|
||||
@Value
|
||||
public class Command implements DataStorageGroupStrategy {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static String getOptionsNameKey() {
|
||||
return "commandSecret";
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static OptionsBuilder createOptions(Property<Command> p) {
|
||||
var command = new SimpleStringProperty(p.getValue().getCommand() != null ?
|
||||
p.getValue().getCommand().getValue() : null);
|
||||
return new OptionsBuilder()
|
||||
.nameAndDescription("commandSecretField")
|
||||
.addComp(new TextAreaComp(command), command)
|
||||
.nonNull()
|
||||
.bind(
|
||||
() -> {
|
||||
return Command.builder().command(command.get() != null ? new ShellScript(
|
||||
command.get()) : null).build();
|
||||
},
|
||||
p);
|
||||
}
|
||||
|
||||
ShellScript command;
|
||||
|
||||
@Override
|
||||
public void checkComplete() throws ValidationException {
|
||||
Validators.nonNull(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String queryEncryptionSecret() throws Exception {
|
||||
try (var sc = ProcessControlProvider.get().createLocalProcessControl(true)) {
|
||||
try (var cc = sc.command(command).start()) {
|
||||
cc.killOnTimeout(CountDown.of().start(30_000));
|
||||
var out = cc.readStdoutOrThrow();
|
||||
if (out.length() == 0) {
|
||||
throw ErrorEventFactory.expected(new IllegalArgumentException("Command did not return any output"));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@JsonTypeName("httpRequest")
|
||||
@Builder
|
||||
@Jacksonized
|
||||
@Value
|
||||
public class HttpRequest implements DataStorageGroupStrategy {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static String getOptionsNameKey() {
|
||||
return "httpRequestSecret";
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static OptionsBuilder createOptions(Property<HttpRequest> p) {
|
||||
var uri = new SimpleStringProperty(p.getValue().getUri());
|
||||
return new OptionsBuilder()
|
||||
.nameAndDescription("httpRequestSecretField")
|
||||
.addString(uri)
|
||||
.nonNull()
|
||||
.bind(
|
||||
() -> {
|
||||
return HttpRequest.builder().uri(uri.get()).build();
|
||||
},
|
||||
p);
|
||||
}
|
||||
|
||||
String uri;
|
||||
|
||||
@Override
|
||||
public void checkComplete() throws ValidationException {
|
||||
Validators.nonNull(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String queryEncryptionSecret() throws Exception {
|
||||
var uri = URI.create(getUri());
|
||||
var request = java.net.http.HttpRequest.newBuilder()
|
||||
.uri(uri)
|
||||
.POST(java.net.http.HttpRequest.BodyPublishers.noBody())
|
||||
.build();
|
||||
var result = HttpHelper.client().send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (result.statusCode() >= 400) {
|
||||
throw ErrorEventFactory.expected(new IOException(result.body()));
|
||||
}
|
||||
var body = result.body();
|
||||
if (body.length() == 0) {
|
||||
throw ErrorEventFactory.expected(new IllegalArgumentException("Http response body is empty"));
|
||||
}
|
||||
return body;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package io.xpipe.app.storage;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.prefs.VaultAuthentication;
|
||||
|
||||
import java.io.IOException;
|
||||
import javax.crypto.SecretKey;
|
||||
@@ -25,4 +26,6 @@ public interface DataStorageUserHandler {
|
||||
Comp<?> createOverview();
|
||||
|
||||
String getActiveUser();
|
||||
|
||||
VaultAuthentication getVaultAuthenticationType();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.xpipe.app.storage;
|
||||
|
||||
import io.xpipe.app.core.AppProperties;
|
||||
import io.xpipe.app.core.mode.AppOperationMode;
|
||||
import io.xpipe.app.core.window.AppMainWindow;
|
||||
import io.xpipe.app.ext.DataStorageExtensionProvider;
|
||||
import io.xpipe.app.ext.LocalStore;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
@@ -314,6 +315,11 @@ public class StandardStorage extends DataStorage {
|
||||
.build()
|
||||
.handle();
|
||||
}
|
||||
|
||||
if (dataStorageUserHandler.getUserCount() > 0) {
|
||||
AppMainWindow.loadingText("unlockingVault");
|
||||
}
|
||||
|
||||
dataStorageUserHandler.login();
|
||||
|
||||
reloadContent();
|
||||
|
||||
@@ -8,10 +8,7 @@ import io.xpipe.app.pwman.KeePassXcAssociationKey;
|
||||
import io.xpipe.app.pwman.KeePassXcPasswordManager;
|
||||
import io.xpipe.app.pwman.PasswordManager;
|
||||
import io.xpipe.app.rdp.ExternalRdpClient;
|
||||
import io.xpipe.app.secret.EncryptedValue;
|
||||
import io.xpipe.app.secret.EncryptionToken;
|
||||
import io.xpipe.app.secret.PasswordLockSecretValue;
|
||||
import io.xpipe.app.secret.VaultKeySecretValue;
|
||||
import io.xpipe.app.secret.*;
|
||||
import io.xpipe.app.storage.*;
|
||||
import io.xpipe.app.terminal.ExternalTerminalType;
|
||||
import io.xpipe.app.terminal.TerminalMultiplexer;
|
||||
@@ -86,6 +83,8 @@ public class AppJacksonModule extends SimpleModule {
|
||||
context.registerSubtypes(TerminalPrompt.getClasses());
|
||||
context.registerSubtypes(ExternalVncClient.getClasses());
|
||||
context.registerSubtypes(ExternalRdpClient.getClasses());
|
||||
context.registerSubtypes(SecretRetrievalStrategy.getClasses());
|
||||
context.registerSubtypes(DataStorageGroupStrategy.getClasses());
|
||||
|
||||
super.setupModule(context);
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ public class IdentityChoiceBuilder {
|
||||
.property(pass)
|
||||
.customConfiguration(
|
||||
SecretStrategyChoiceConfig.builder().allowNone(true).build())
|
||||
.available(SecretRetrievalStrategy.getSubclasses())
|
||||
.available(SecretRetrievalStrategy.getClasses())
|
||||
.build()
|
||||
.build();
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ public class LocalIdentityStoreProvider extends IdentityStoreProvider {
|
||||
.property(pass)
|
||||
.customConfiguration(
|
||||
SecretStrategyChoiceConfig.builder().allowNone(true).build())
|
||||
.available(SecretRetrievalStrategy.getSubclasses())
|
||||
.available(SecretRetrievalStrategy.getClasses())
|
||||
.build()
|
||||
.build();
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import io.xpipe.app.hub.comp.StoreEntryWrapper;
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
import io.xpipe.app.platform.OptionsChoiceBuilder;
|
||||
import io.xpipe.app.platform.Validator;
|
||||
import io.xpipe.app.prefs.VaultAuthentication;
|
||||
import io.xpipe.app.secret.EncryptedValue;
|
||||
import io.xpipe.app.secret.SecretNoneStrategy;
|
||||
import io.xpipe.app.secret.SecretRetrievalStrategy;
|
||||
@@ -83,10 +84,11 @@ public class SyncedIdentityStoreProvider extends IdentityStoreProvider {
|
||||
.property(pass)
|
||||
.customConfiguration(
|
||||
SecretStrategyChoiceConfig.builder().allowNone(true).build())
|
||||
.available(SecretRetrievalStrategy.getSubclasses())
|
||||
.available(SecretRetrievalStrategy.getClasses())
|
||||
.build()
|
||||
.build();
|
||||
|
||||
var handler = DataStorageUserHandler.getInstance();
|
||||
return new OptionsBuilder()
|
||||
.nameAndDescription("username")
|
||||
.addString(user)
|
||||
@@ -104,11 +106,11 @@ public class SyncedIdentityStoreProvider extends IdentityStoreProvider {
|
||||
return !wrong;
|
||||
}))
|
||||
.nameAndDescription(
|
||||
DataStorageUserHandler.getInstance().getActiveUser() != null
|
||||
? "identityPerUser"
|
||||
handler.getActiveUser() != null
|
||||
? (handler.getVaultAuthenticationType() == VaultAuthentication.GROUP ? "identityPerGroup" : "identityPerUser")
|
||||
: "identityPerUserDisabled")
|
||||
.addToggle(perUser)
|
||||
.disable(DataStorageUserHandler.getInstance().getActiveUser() == null)
|
||||
.disable(handler.getActiveUser() == null)
|
||||
.bind(
|
||||
() -> {
|
||||
return SyncedIdentityStore.builder()
|
||||
|
||||
@@ -57,7 +57,7 @@ public class InPlaceKeyStrategy implements SshIdentityStrategy {
|
||||
.property(keyPasswordProperty)
|
||||
.customConfiguration(
|
||||
SecretStrategyChoiceConfig.builder().allowNone(true).build())
|
||||
.available(SecretRetrievalStrategy.getSubclasses())
|
||||
.available(SecretRetrievalStrategy.getClasses())
|
||||
.build()
|
||||
.build();
|
||||
var publicKeyField = new TextFieldComp(publicKey).apply(struc -> {
|
||||
|
||||
@@ -84,7 +84,7 @@ public class KeyFileStrategy implements SshIdentityStrategy {
|
||||
.property(keyPasswordProperty)
|
||||
.customConfiguration(
|
||||
SecretStrategyChoiceConfig.builder().allowNone(true).build())
|
||||
.available(SecretRetrievalStrategy.getSubclasses())
|
||||
.available(SecretRetrievalStrategy.getClasses())
|
||||
.build()
|
||||
.build();
|
||||
|
||||
|
||||
Generated
+53
-12
@@ -570,6 +570,8 @@ identitiesIntroBottomContent=You can add identities locally or also sync them up
|
||||
identitiesIntroBottomButton=Setup sync
|
||||
identitiesIntroButton=Create identity
|
||||
userName=Username
|
||||
userAuth=User-based password authentication
|
||||
groupAuth=Group-based secret authentication
|
||||
team=Team
|
||||
teamSettings=Team settings
|
||||
teamVaults=Team vaults
|
||||
@@ -577,20 +579,31 @@ vaultTypeNameDefault=Default vault
|
||||
vaultTypeNameLegacy=Legacy personal vault
|
||||
vaultTypeNamePersonal=Personal vault
|
||||
vaultTypeNameTeam=Team vault
|
||||
teamVaultsDescription=Team vaults allow multiple users to have secure access to a shared vault. You can configure connections and identities to either be shared for all users or only have them available for your personal user by encrypting them with your personal key. Other vault users can't access your personal connections and identities.
|
||||
#force
|
||||
teamVaultsDescription=Team vaults allow multiple users and groups to have secure access to a shared vault. You can configure connections and identities to either be shared for all users or only have them available for individual users and groups by encrypting them with their own key. Other vault users can't access personal and group-based connections and identities if they don't have access to the key.
|
||||
vaultTypeContentDefault=You are currently using a default vault with no user and custom passphrase set. Secrets are encrypted with the local vault key. You can upgrade to a personal vault by creating a vault user account. This allows you to encrypt vault secrets with your own personal passphrase that you have to input on each login to unlock the vault.
|
||||
vaultTypeContentLegacy=You are currently using a legacy personal vault for your user. Secrets are encrypted with your personal passphrase. This legacy compatibility has limited features and can't be upgraded to a team vault in-place.
|
||||
vaultTypeContentPersonal=You are currently using a personal vault for your user. Secrets are encrypted with your personal passphrase. You can upgrade to a team vault by adding additional vault users.
|
||||
vaultTypeContentTeam=You are currently using a team vault, which allows multiple users to have secure access to a shared vault. You can configure connections and identities to either be shared for all users or only have them available for your personal user by encrypting them with your personal key. Other vault users can't access your personal connections and identities.
|
||||
#force
|
||||
vaultTypeContentPersonal=You are currently using a personal vault for your user. Secrets are encrypted with your personal passphrase. You can upgrade to a team vault by adding additional vault users or add a group-based access configuration.
|
||||
#force
|
||||
vaultTypeContentTeam=You are currently using a team vault, which allows multiple users to have secure access to a shared vault. You can configure connections and identities to either be shared for all users or only have them available for your personal user or group by encrypting them with your personal or group key. Other vault users can't access your personal and group-based connections and identities if they don't have access to the key.
|
||||
groupManagement=Group management
|
||||
groupManagementEmpty=Group management
|
||||
groupManagementDescription=Manage existing vault groups or create new ones. Each vault group has its own individual secret key which is used to encrypt connections and identities that should only be available to the group and not to others.
|
||||
groupManagementEmptyDescription=Manage existing vault groups or create new ones. Each vault group has its own individual secret key which is used to encrypt connections and identities that should only be available to the group and not to others.\n\nGroup-based accounts for a team are supported in the professional plan.
|
||||
#force
|
||||
userManagement=User management
|
||||
userManagementDescription=Manage existing vault users or create new ones.
|
||||
userManagementDescriptionEmpty=Manage existing vault users or create new ones. Create a user for yourself to be able to encrypt connections and identities with your personal key.\n\nA single user account is supported in the community edition. Multiple user accounts for a team are supported in the professional plan.
|
||||
userManagementEmpty=User management
|
||||
#force
|
||||
userManagementDescription=Manage existing vault users or create new ones. Each vault user has its own individual password which is used to encrypt connections and identities that should only be available to the user and not to others.
|
||||
#force
|
||||
userManagementEmptyDescription=Manage existing vault users or create new ones. Each vault user has its own individual password which is used to encrypt connections and identities that should only be available to the user and not to others. Create a user for yourself to be able to encrypt connections and identities with your personal key.\n\nA single user account is supported in the community edition. Multiple user accounts for a team are supported in the professional plan.
|
||||
userIntroHeader=User management
|
||||
userIntroContent=Create the first user account for yourself to get started. This allows you to lock this workspace with a password.
|
||||
addReusableIdentity=Add reusable identity
|
||||
users=Users
|
||||
syncTeamVaults=Team vault synchronization
|
||||
syncTeamVaultsDescription=To synchronize your vault with multiple team members, enable the git synchronization.
|
||||
syncVault=Vault synchronization
|
||||
syncVaultDescription=To synchronize your vault with across multiple systems or with multiple team members, enable the git synchronization for this vault.
|
||||
enableGitSync=Enable git sync
|
||||
browseVault=Vault data
|
||||
browseVaultDescription=You can take a look at the vault directory yourself in your native file manager. Note that external edits are not recommended and can cause a variety of issues.
|
||||
@@ -604,10 +617,14 @@ loadingGit=Syncing with git repo
|
||||
loadingGpg=Starting GnuPG daemon for git
|
||||
loadingSettings=Loading settings
|
||||
loadingConnections=Loading connections
|
||||
unlockingVault=Unlocking vault
|
||||
loadingUserInterface=Loading user interface
|
||||
ptbNotice=Notice for the public test build
|
||||
userDeletionTitle=User deletion
|
||||
userDeletionContent=Do you want to delete this vault user? This will reencrypt all your personal identities and connection secrets using the vault key that is available to all users. XPipe will restart to apply the user changes.
|
||||
#force
|
||||
userDeletionContent=Do you want to delete this vault user? This will reencrypt all your personal identities and connection secrets using the vault key that is available to all users. This will take a while and XPipe will restart to apply the user changes.
|
||||
groupDeletionTitle=Group deletion
|
||||
groupDeletionContent=Do you want to delete this vault group? This will reencrypt all group-only identities and connection secrets using the vault key that is available to all users. This will take a while and XPipe will restart to apply the group changes.
|
||||
killTransfer=Kill transfer
|
||||
destination=Destination
|
||||
configuration=Configuration
|
||||
@@ -799,10 +816,13 @@ identity.displayDescription=Create a reusable identity for connections
|
||||
local=Local
|
||||
shared=Global
|
||||
userDescription=The username or predefined identity to log in as
|
||||
identityPerUserDescription=Restrict access to this identity and its associated connections to your vault user only
|
||||
identityAccessLevel=Access level
|
||||
identityPerUser=Personal identity access
|
||||
identityPerUserDescription=Restrict access to this identity and its associated connections to your vault user only
|
||||
identityPerUserDisabled=Personal identity access (disabled)
|
||||
identityPerUserDisabledDescription=Restrict access to this identity and its associated connections to your vault user only (Requires team to be configured)
|
||||
identityPerGroup=Group-only identity access
|
||||
identityPerGroupDescription=Restrict access to this identity and its associated connections to this vault group only
|
||||
library=Library
|
||||
location=Location
|
||||
keyAuthentication=Key-based authentication
|
||||
@@ -1210,20 +1230,25 @@ libvirt=libvirt domains
|
||||
customIp=Custom IP
|
||||
customIpDescription=Override the default local VM IP detection if you use advanced networking
|
||||
automaticallyDetect=Automatically detect
|
||||
lockCreationAlertTitle=User creation
|
||||
userAddDialogTitle=User creation
|
||||
groupAddDialogTitle=Group creation
|
||||
passphrase=Passphrase
|
||||
repeatPassphrase=Repeat passphrase
|
||||
lockCreationAlertHeader=Create new vault user
|
||||
groupSecret=Group secret
|
||||
repeatGroupSecret=Repeat group secret
|
||||
vaultGroup=Vault group
|
||||
loginAlertTitle=Login required
|
||||
loginAlertHeader=Unlock vault to access your personal connections
|
||||
vaultUser=Vault user
|
||||
#context: dative case
|
||||
me=Me
|
||||
addGroup=Add group ...
|
||||
addGroupDescription=Create a new group for this vault
|
||||
addUser=Add user ...
|
||||
addUserDescription=Create a new user for this vault
|
||||
skip=Skip
|
||||
userChangePasswordAlertTitle=Password change
|
||||
userChangePasswordAlertHeader=Set new password for user
|
||||
groupChangeSecretAlertTitle=Secret change
|
||||
docs=Documentation
|
||||
lxd.displayName=LXD Container
|
||||
lxd.displayDescription=Connect to a LXD container via lxc
|
||||
@@ -1766,3 +1791,19 @@ identityApplySetStoreIdentity=Connection identity set
|
||||
identityApplySetStoreIdentityDescription=The identity has been configured to be used by the connection
|
||||
identityApplySetStoreIdentityButton=Apply identity
|
||||
generateKey=Generate key
|
||||
groupSecretStrategy=Group-based access control
|
||||
groupSecretStrategyDescription=Here you can choose how to retrieve the group secret used for encryption. The retrieval method you choose will be run when a user logs into the vault. Their access level to certain connections is determined by the raw key that the retrieval method returns.
|
||||
fileSecret=File-based secret
|
||||
commandSecret=Secret retrieval command
|
||||
httpRequestSecret=HTTP response
|
||||
fileSecretChoice=File location
|
||||
fileSecretChoiceDescription=The path to the file containing the group encryption secret. Since this file is queried on all platforms, you can use ~ in the path to refer to the home directory. The file must be available on all systems you unlock the vault from, otherwise group-based connection decryption will fail.
|
||||
commandSecretField=Retrieval script
|
||||
commandSecretFieldDescription=The command that will return the secret encryption key for the current group. The command is run in the local system default shell and the key should be printed to stdout.
|
||||
httpRequestSecretField=Request URI
|
||||
httpRequestSecretFieldDescription=The URI to send an HTTP request to. The group secret is taken from the HTTP response body.
|
||||
vaultAuthentication=Vault authentication
|
||||
vaultAuthenticationDescription=How to authenticate / unlock the vault data. There are multiple different ways of encrypting and unlocking vault data, depending on who you want to share the vault data with.
|
||||
groupAuthFailed=Secret authentication failed
|
||||
userAuthFailed=Password authentication failed
|
||||
savingChanges=Saving changes
|
||||
|
||||
Reference in New Issue
Block a user