Merge branch group-vault

This commit is contained in:
crschnick
2025-12-16 08:19:45 +00:00
parent afdf3a05ea
commit 7aaa7ba4be
27 changed files with 514 additions and 71 deletions
@@ -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();
+53 -12
View File
@@ -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