diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java index 1f88f8a66..f482e24fb 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java @@ -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()); diff --git a/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java b/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java index 2470b83e3..8c0168d55 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java @@ -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> { private static Boolean WEB_VIEW_SUPPORTED; - private static Path TEMP; + private static Path DIR; private final ObservableValue markdown; private final UnaryOperator htmlTransformation; private final boolean bodyPadding; @@ -53,8 +53,8 @@ public class MarkdownComp extends Comp> { } 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> { } 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> { 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() diff --git a/app/src/main/java/io/xpipe/app/comp/base/SecretFieldComp.java b/app/src/main/java/io/xpipe/app/comp/base/SecretFieldComp.java index 8e0848392..7af25f6bc 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/SecretFieldComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/SecretFieldComp.java @@ -94,16 +94,16 @@ public class SecretFieldComp extends Comp { } 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); diff --git a/app/src/main/java/io/xpipe/app/core/AppI18n.java b/app/src/main/java/io/xpipe/app/core/AppI18n.java index 99add4082..f1afb9e7a 100644 --- a/app/src/main/java/io/xpipe/app/core/AppI18n.java +++ b/app/src/main/java/io/xpipe/app/core/AppI18n.java @@ -52,6 +52,10 @@ public class AppI18n { return INSTANCE.observableImpl(s, vars); } + public static ObservableValue observable(ObservableValue 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); } diff --git a/app/src/main/java/io/xpipe/app/core/mode/AppBaseMode.java b/app/src/main/java/io/xpipe/app/core/mode/AppBaseMode.java index 24878152a..f0b171c66 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/AppBaseMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/AppBaseMode.java @@ -123,6 +123,7 @@ public class AppBaseMode extends AppOperationMode { syncPrefsLoaded.countDown(); AppMainWindow.loadingText("loadingConnections"); DataStorage.init(); + AppPrefs.initStorage(); storageLoaded.countDown(); AppMcpServer.init(); StoreViewState.init(); diff --git a/app/src/main/java/io/xpipe/app/platform/OptionsBuilder.java b/app/src/main/java/io/xpipe/app/platform/OptionsBuilder.java index 665767a40..9e1e1c88a 100644 --- a/app/src/main/java/io/xpipe/app/platform/OptionsBuilder.java +++ b/app/src/main/java/io/xpipe/app/platform/OptionsBuilder.java @@ -126,6 +126,10 @@ public class OptionsBuilder { return name(key).description(key + "Description"); } + public OptionsBuilder nameAndDescription(ObservableValue 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 name) { + finishCurrent(); + this.name = name; + lastNameReference = name; + return this; + } + public OptionsBuilder description(String descriptionKey) { finishCurrent(); description = AppI18n.observable(descriptionKey); diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java index 1a461ed82..9dae71756 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java @@ -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 = new GlobalObjectProperty<>(); + final ObjectProperty 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 = map(Mapping.builder() @@ -423,6 +432,10 @@ public final class AppPrefs { private AppPrefs() {} + public ObservableValue vaultAuthentication() { + return vaultAuthentication; + } + public ObservableValue 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() { diff --git a/app/src/main/java/io/xpipe/app/prefs/VaultAuthentication.java b/app/src/main/java/io/xpipe/app/prefs/VaultAuthentication.java new file mode 100644 index 000000000..ebd1df482 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/VaultAuthentication.java @@ -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; +} diff --git a/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java b/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java index 9c4330c1b..4263b0839 100644 --- a/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java @@ -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(); diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorageGroupStrategy.java b/app/src/main/java/io/xpipe/app/storage/DataStorageGroupStrategy.java index 5480e17eb..fd70835ae 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorageGroupStrategy.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorageGroupStrategy.java @@ -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(); diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorageUserHandler.java b/app/src/main/java/io/xpipe/app/storage/DataStorageUserHandler.java index aa6d10b08..4d1ba8fbc 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorageUserHandler.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorageUserHandler.java @@ -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(); } diff --git a/app/src/main/java/io/xpipe/app/storage/DataStoreScope.java b/app/src/main/java/io/xpipe/app/storage/DataStoreScope.java deleted file mode 100644 index 660e4d976..000000000 --- a/app/src/main/java/io/xpipe/app/storage/DataStoreScope.java +++ /dev/null @@ -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(); -} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/SyncedIdentityMigrationDeserializer.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/SyncedIdentityMigrationDeserializer.java deleted file mode 100644 index b927946b0..000000000 --- a/ext/base/src/main/java/io/xpipe/ext/base/identity/SyncedIdentityMigrationDeserializer.java +++ /dev/null @@ -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"); - } - } -} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/SyncedIdentityStore.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/SyncedIdentityStore.java index 73269ce06..211ba6c9f 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/identity/SyncedIdentityStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/identity/SyncedIdentityStore.java @@ -28,7 +28,6 @@ public class SyncedIdentityStore extends IdentityStore implements UserScopeStore // per user stores are additionally encrypted on the entry level EncryptedValue.VaultKey password; EncryptedValue.VaultKey sshIdentity; - DataStoreScope scope; boolean perUser; public UsernameStrategy.Fixed getUsername() { diff --git a/lang/strings/translations_en.properties b/lang/strings/translations_en.properties index d04d89ce9..3fcb81836 100644 --- a/lang/strings/translations_en.properties +++ b/lang/strings/translations_en.properties @@ -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.