This commit is contained in:
crschnick
2025-12-13 00:08:15 +00:00
parent 3bc9bee383
commit a0c4e790ca
15 changed files with 131 additions and 152 deletions

View File

@@ -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());

View File

@@ -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()

View File

@@ -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;
}
if (!capsPopover.isShowing()) {
capsPopover.show(field);
}
var capslock = Platform.isKeyLocked(KeyCode.CAPS);
if (!capslock.orElse(false)) {
capsPopover.hide();
return;
}
if (!capsPopover.isShowing()) {
capsPopover.show(field);
}
});
});
HBox.setHgrow(field, Priority.ALWAYS);

View File

@@ -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);
}

View File

@@ -123,6 +123,7 @@ public class AppBaseMode extends AppOperationMode {
syncPrefsLoaded.countDown();
AppMainWindow.loadingText("loadingConnections");
DataStorage.init();
AppPrefs.initStorage();
storageLoaded.countDown();
AppMcpServer.init();
StoreViewState.init();

View File

@@ -126,6 +126,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);
@@ -326,6 +330,13 @@ public class OptionsBuilder {
return this;
}
public OptionsBuilder name(ObservableValue<String> name) {
finishCurrent();
this.name = name;
lastNameReference = name;
return this;
}
public OptionsBuilder description(String descriptionKey) {
finishCurrent();
description = AppI18n.observable(descriptionKey);

View File

@@ -15,6 +15,7 @@ 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;
@@ -41,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.*;
@@ -68,6 +70,10 @@ public final class AppPrefs {
});
}
public static void initStorage() {
INSTANCE.vaultAuthentication.set(DataStorageUserHandler.getInstance().getVaultAuthenticationType());
}
public static void reset() {
INSTANCE.save();
@@ -270,11 +276,14 @@ 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<>(new DataStorageGroupStrategy.None()))
.key("groupSecretStrategy")
.valueClass(DataStorageGroupStrategy.class)
.requiresRestart(true)
.vaultSpecific(true)
.licenseFeatureId("team")
.build());
final ObjectProperty<StartupBehaviour> startupBehaviour = map(Mapping.builder()
@@ -423,6 +432,10 @@ public final class AppPrefs {
private AppPrefs() {}
public ObservableValue<VaultAuthentication> vaultAuthentication() {
return vaultAuthentication;
}
public ObservableValue<DataStorageGroupStrategy> groupSecretStrategy() {
return groupSecretStrategy;
}
@@ -721,7 +734,10 @@ public final class AppPrefs {
PlatformThread.runLaterIfNeededBlocking(() -> {
writable.setValue(newValue);
});
save();
if (mapping.stream().anyMatch(m -> m.property == prop)) {
save();
}
}
private void fixLocalValues() {

View File

@@ -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;
}

View File

@@ -2,10 +2,12 @@ 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;
@@ -17,6 +19,7 @@ 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;
@@ -62,40 +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()), false);
authChoice.apply(struc -> struc.get().setOpacity(1.0));
authChoice.maxWidth(600);
authChoice.disable(Bindings.createBooleanBinding(() -> {
return uh.getUserCount() > 0 && prefs.vaultAuthentication.get() == VaultAuthentication.USER ||
prefs.groupSecretStrategy.get().requiresUnlock() && prefs.vaultAuthentication.get() == VaultAuthentication.GROUP;
}, prefs.vaultAuthentication, prefs.groupSecretStrategy));
builder.addTitle("vault")
.sub(new OptionsBuilder()
.name("vaultTypeName" + vaultTypeKey)
.description("vaultTypeContent" + vaultTypeKey)
.documentationLink(DocumentationLink.TEAM_VAULTS)
.addComp(Comp.empty())
.pref( uh.getActiveUser() != null
? "userManagement"
: "userManagementEmpty", true, null, null)
.addComp(uh.createOverview().maxWidth(getCompWidth()))
.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()))
.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(false).available(DataStorageGroupStrategy.getClasses())
.build().build().buildComp().maxWidth(getCompWidth()),
prefs.groupSecretStrategy)
.licenseRequirement("team")
.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(Bindings.createBooleanBinding(() -> {
return uh.getUserCount() > 1 || !(prefs.groupSecretStrategy.get() instanceof DataStorageGroupStrategy.None);
}, prefs.groupSecretStrategy))
.hide(uh.getUserCount() > 1)
);
builder.sub(new OptionsBuilder().pref(prefs.encryptAllVaultData).addToggle(encryptVault));
return builder.buildComp();

View File

@@ -48,6 +48,10 @@ public interface DataStorageGroupStrategy {
return l;
}
default boolean requiresUnlock() {
return true;
}
default void checkComplete() throws ValidationException {}
byte[] queryEncryptionSecret() throws Exception;
@@ -56,6 +60,11 @@ public interface DataStorageGroupStrategy {
@Value
public class None implements DataStorageGroupStrategy {
@Override
public boolean requiresUnlock() {
return false;
}
@Override
public byte[] queryEncryptionSecret() throws Exception {
throw new UnsupportedOperationException();

View File

@@ -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();
}

View File

@@ -1,48 +0,0 @@
package io.xpipe.app.storage;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.xpipe.app.secret.EncryptionToken;
public enum DataStoreScope {
@JsonProperty("vault")
VAULT() {
@Override
public EncryptionToken getToken() {
return EncryptionToken.ofVaultKey();
}
@Override
public String getId() {
return "vault";
}
},
@JsonProperty("group")
GROUP() {
@Override
public EncryptionToken getToken() {
return EncryptionToken.ofVaultKey();
}
@Override
public String getId() {
return "group";
}
},
@JsonProperty("user")
USER() {
@Override
public EncryptionToken getToken() {
return EncryptionToken.ofVaultKey();
}
@Override
public String getId() {
return "user";
}
};
public abstract String getId();
public abstract EncryptionToken getToken();
}

View File

@@ -1,64 +0,0 @@
package io.xpipe.ext.base.identity;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TreeTraversingParser;
import java.io.IOException;
public class SyncedIdentityMigrationDeserializer extends DelegatingDeserializer {
public SyncedIdentityMigrationDeserializer(JsonDeserializer<?> d) {
super(d);
}
@Override
protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) {
return new SyncedIdentityMigrationDeserializer(newDelegatee);
}
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return super.deserialize(restructure(p), ctxt);
}
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt, Object intoValue) throws IOException {
return super.deserialize(restructure(p), ctxt, intoValue);
}
public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer typeDeserializer)
throws IOException {
return super.deserializeWithType(restructure(jp), ctxt, typeDeserializer);
}
public JsonParser restructure(JsonParser p) throws IOException {
var node = p.readValueAsTree();
if (node == null) {
return p;
}
if (!node.isObject()) {
var newJsonParser = new TreeTraversingParser((ObjectNode) node, p.getCodec());
newJsonParser.nextToken();
return newJsonParser;
}
migrate((ObjectNode) node);
var newJsonParser = new TreeTraversingParser((ObjectNode) node, p.getCodec());
newJsonParser.nextToken();
return newJsonParser;
}
private void migrate(ObjectNode containerNode) {
if (containerNode.has("perUser")) {
containerNode.remove("perUser");
containerNode.put("scope", "user");
}
}
}

View File

@@ -28,7 +28,6 @@ public class SyncedIdentityStore extends IdentityStore implements UserScopeStore
// per user stores are additionally encrypted on the entry level
EncryptedValue.VaultKey<SecretRetrievalStrategy> password;
EncryptedValue.VaultKey<SshIdentityStrategy> sshIdentity;
DataStoreScope scope;
boolean perUser;
public UsernameStrategy.Fixed getUsername() {

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
@@ -585,6 +587,10 @@ vaultTypeContentLegacy=You are currently using a legacy personal vault for your
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-based access control
userManagementEmpty=User-based access control
@@ -596,8 +602,8 @@ 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.
@@ -615,6 +621,8 @@ 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.
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. XPipe will restart to apply the group changes.
killTransfer=Kill transfer
destination=Destination
configuration=Configuration
@@ -1217,20 +1225,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
@@ -1702,7 +1715,7 @@ openSftp=Open in SFTP session
capslockWarning=You have capslock enabled
inherit=Inherit
groupSecretStrategy=Group-based access control
groupSecretStrategyDescription=In addition to individual users, you can also configure group-based encryption for connections and identities, meaning that people of a certain group can share access to them.\n\nHere 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.
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=Secret HTTP response
@@ -1711,3 +1724,5 @@ fileSecretChoiceDescription=The path to the file containing the group encryption
commandSecretField=Retrieval script
commandSecretFieldDescription=The command or script that will return the secret encryption key for the current group. The key should be printed to stdout.
httpRequestSecretField=Request URI
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.