This commit is contained in:
crschnick
2026-08-18 15:23:13 +00:00
parent 8ce0a2427e
commit a5ebaf91c0
27 changed files with 166 additions and 61 deletions
@@ -107,7 +107,7 @@ public class StoreCategoryComp extends SimpleRegionBuilder {
return new LabelGraphic.IconGraphic("mdomz-settings");
}
if (!DataStorage.get().supportsSync()
if (!DataStorage.get().syncEnabled()
|| (!category.getCategory().canShare())) {
return new LabelGraphic.IconGraphic("mdi2g-git");
}
@@ -155,7 +155,7 @@ public class StoreCategoryComp extends SimpleRegionBuilder {
var dragOver = new SimpleBooleanProperty();
var dragIntoIndicator = createDragIntoIndicator(dragOver);
var showStatus = hover.or(new SimpleBooleanProperty(DataStorage.get().supportsSync()))
var showStatus = hover.or(new SimpleBooleanProperty(DataStorage.get().syncEnabled()))
.or(showing)
.or(focus);
var h = new HorizontalComp(List.of(
@@ -86,7 +86,7 @@ public class StoreCategoryConfigComp extends SimpleRegionBuilder {
var options = new OptionsBuilder();
var specialCategorySync = !wrapper.getCategory().canShare();
var syncDisable = !DataStorage.get().supportsSync()
var syncDisable = !DataStorage.get().syncEnabled()
|| ((sync.getValue() == null || !sync.getValue())
&& !wrapper.getCategory().canShare());
options.title("sync")
@@ -167,7 +167,7 @@ public interface StoreEntryBadge {
return null;
}
return of("mdi2c-console", s).withCopyAction();
return of("mdi2c-console", s);
}
static StoreEntryBadge ofKey(String s) {
@@ -22,7 +22,7 @@ public class StoreIdentitiesIntroComp extends SimpleRegionBuilder {
top.setButtonDefault(true);
top.setButtonGraphic(new LabelGraphic.IconGraphic("mdi2p-play-circle"));
top.setButtonAction(() -> {
var canSync = DataStorage.get().supportsSync();
var canSync = DataStorage.get().syncEnabled();
var prov = canSync
? DataStoreProvider.byId("syncedIdentity").orElseThrow()
: DataStoreProvider.byId("localIdentity").orElseThrow();
@@ -6,6 +6,7 @@ import io.xpipe.app.util.OsType;
import java.nio.file.AccessDeniedException;
import java.nio.file.FileSystemException;
import java.nio.file.InvalidPathException;
import java.nio.file.NoSuchFileException;
import java.util.*;
@@ -114,6 +115,10 @@ public class ErrorEventFactory {
b.expected();
}
if (t instanceof InvalidPathException ipe) {
b.description("Invalid file path: " + ipe.getMessage());
}
return b;
}
}
@@ -5,6 +5,32 @@ import javax.crypto.SecretKey;
public interface EncryptionPrincipal {
static EncryptionPrincipal inaccessible() {
var dummyId = UUID.fromString("73e2d533-6fa4-497e-87a8-24da4fdf4d63");
return new EncryptionPrincipal() {
@Override
public UUID getUuid() {
return dummyId;
}
@Override
public String getName() {
return "inaccessible";
}
@Override
public boolean isAccessible() {
return false;
}
@Override
public SecretKey getSecretKey() {
throw new UnsupportedOperationException();
}
};
}
static EncryptionPrincipal getTargetPrincipal(EncryptionPrincipal principal) {
if (!principal.isAccessible()) {
return principal;
@@ -132,7 +132,7 @@ public abstract class DataStorage {
}
private void dispose() {
save(true);
save(true, true);
var finalizing = false;
for (DataStoreEntry entry : getStoreEntries()) {
// Prevent blocking of shutdown
@@ -202,7 +202,7 @@ public abstract class DataStorage {
localIdentities.get().setParentCategory(ALL_IDENTITIES_CATEGORY_UUID);
}
if (supportsSync()) {
if (syncEnabled()) {
var sharedIdentities = getStoreCategoryIfPresent(SYNCED_IDENTITIES_CATEGORY_UUID);
if (sharedIdentities.isEmpty()) {
var cat = DataStoreCategory.createNew(
@@ -267,9 +267,9 @@ public abstract class DataStorage {
public abstract void saveAsync();
public abstract void save(boolean dispose);
public abstract void save(boolean dispose, boolean forceSync);
public abstract boolean supportsSync();
public abstract boolean syncEnabled();
public boolean shouldSync(DataStoreCategory category) {
// Don't sync lone identities category
@@ -207,6 +207,10 @@ public class DataStorageSecret {
newEntries.add(newEntry);
}
if (newEntries.equals(entries)) {
return this;
}
return new DataStorageSecret(newEntries, secret);
}
@@ -7,6 +7,7 @@ import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@@ -15,6 +16,14 @@ import java.util.stream.Collectors;
@ToString
public class DataStoreAccessScope {
public static DataStoreAccessScope merge(List<DataStoreAccessScope> scopes) {
var matching = DataStorageAccessHandler.getInstance().getAllEncryptionPrincipals().stream()
.filter(encryptionPrincipal -> {
return scopes.stream().allMatch(s -> s.getPrincipals().contains(encryptionPrincipal));
}).collect(Collectors.toSet());
return !matching.isEmpty() ? of(matching) : DataStoreAccessScope.of(Set.of(EncryptionPrincipal.inaccessible()));
}
public static DataStoreAccessScope getTargetScope(DataStoreAccessScope scope) {
var newPrincipals = scope.getPrincipals().stream()
.map(encryptionPrincipal -> EncryptionPrincipal.getTargetPrincipal(encryptionPrincipal))
@@ -32,11 +41,6 @@ public class DataStoreAccessScope {
Set.of(DataStorageAccessHandler.getInstance().getEncryptAllPrincipal()));
}
public static DataStoreAccessScope current() {
var handler = DataStorageAccessHandler.getInstance();
return new DataStoreAccessScope(handler.getCurrentEncryptionPrincipals());
}
public static DataStoreAccessScope of(Set<EncryptionPrincipal> encryptionPrincipals) {
return new DataStoreAccessScope(encryptionPrincipals);
}
@@ -370,8 +370,7 @@ public class DataStoreEntry extends DataStorageElement {
}
// Check whether we need to write the node due to external changes
var scope = store instanceof AccessScopeStore s && s.getAccessScope() != null
? s.getAccessScope()
var scope = store instanceof AccessScopeStore s ? s.getAccessScope()
: DataStoreAccessScope.encryption();
var shouldEncrypt = (encryptIfRestricted && scope.isAccessRestricted())
|| AppPrefs.get().encryptAllVaultData().get();
@@ -425,7 +424,7 @@ public class DataStoreEntry extends DataStorageElement {
public DataStoreAccessScope getAccessScope() {
try {
if (getStore() instanceof AccessScopeStore s && s.getAccessScope() != null) {
if (getStore() instanceof AccessScopeStore s) {
return s.getAccessScope();
}
} catch (Exception ignored) {
@@ -768,7 +767,7 @@ public class DataStoreEntry extends DataStorageElement {
DataStoreAccessScope newAccessScope = null;
try {
if (newStore instanceof AccessScopeStore u && u.getAccessScope() != null) {
if (newStore instanceof AccessScopeStore u) {
newAccessScope = u.getAccessScope();
}
} catch (Exception ignored) {
@@ -70,15 +70,17 @@ public class DataStoreEntryNode<T> {
return null;
}
var scope = entry.getAccessScope();
var shouldEncrypt = (encryptIfRestricted && scope.isAccessRestricted())
var currentScope = enc.getSecret() != null ? enc.getSecret().getScope() : DataStoreAccessScope.vault();
var targetScope = DataStoreAccessScope.getTargetScope(entry.getAccessScope());
var shouldEncrypt = (encryptIfRestricted && targetScope.isAccessRestricted())
|| AppPrefs.get().encryptAllVaultData().get();
var encryptionChange = shouldEncrypt && !enc.isEncrypted() || !shouldEncrypt && enc.isEncrypted();
var scopeTargetChange = !DataStoreAccessScope.getTargetScope(scope).equals(scope);
var scopeTargetChange = !targetScope.equals(currentScope);
var valueChange = !getValue().equals(newValue);
if (encryptionChange || valueChange || scopeTargetChange) {
return new DataStoreEntryNode<>(
shouldEncrypt ? EncryptedValue.of(newValue, scope) : EncryptedValue.ofRaw(newValue), false);
shouldEncrypt ? EncryptedValue.of(newValue, targetScope) : EncryptedValue.ofRaw(newValue), false);
} else {
return this;
}
@@ -99,25 +101,27 @@ public class DataStoreEntryNode<T> {
}
if (getValue() instanceof AccessScopeStore s
&& (s.getAccessScope() == null || !s.getAccessScope().isAccessible())) {
&& !s.getAccessScope().isAccessible()) {
return this;
}
T newValue = getValue() instanceof AccessScopeStore s ? (T) s.withUpdatedPrincipals() : getValue();
if (newValue instanceof AccessScopeStore s
&& (s.getAccessScope() == null || !s.getAccessScope().isAccessible())) {
&& !s.getAccessScope().isAccessible()) {
return this;
}
var scope = entry.getAccessScope();
var shouldEncrypt = (encryptIfRestricted && scope.isAccessRestricted())
var currentScope = enc.getSecret() != null ? enc.getSecret().getScope() : DataStoreAccessScope.vault();
var targetScope = DataStoreAccessScope.getTargetScope(entry.getAccessScope());
var shouldEncrypt = (encryptIfRestricted && targetScope.isAccessRestricted())
|| AppPrefs.get().encryptAllVaultData().get();
var encryptionChange = shouldEncrypt && !enc.isEncrypted() || !shouldEncrypt && enc.isEncrypted();
var scopeTargetChange = !DataStoreAccessScope.getTargetScope(scope).equals(scope);
var scopeTargetChange = !targetScope.equals(currentScope);
var valueChange = !getValue().equals(newValue);
if (encryptionChange || scopeTargetChange || valueChange) {
return new DataStoreEntryNode<>(
shouldEncrypt ? EncryptedValue.of(newValue, scope) : EncryptedValue.ofRaw(newValue), false);
shouldEncrypt ? EncryptedValue.of(newValue, targetScope) : EncryptedValue.ofRaw(newValue), false);
} else {
return this;
}
@@ -48,10 +48,10 @@ public class ImpersistentStorage extends DataStorage {
public void saveAsync() {}
@Override
public synchronized void save(boolean dispose) {}
public synchronized void save(boolean dispose, boolean forceSync) {}
@Override
public boolean supportsSync() {
public boolean syncEnabled() {
return false;
}
@@ -392,11 +392,11 @@ public class StandardStorage extends DataStorage {
}
ThreadHelper.runAsync(() -> {
save(false);
save(false, false);
});
}
public void save(boolean dispose) {
public void save(boolean dispose, boolean forceSync) {
try {
// If another save operation is in progress, we have to wait on dispose
// Otherwise the application may quit and kill the daemon thread that is performing the other save operation
@@ -408,8 +408,11 @@ public class StandardStorage extends DataStorage {
return;
}
// We don't need to wait on normal saves though
if (!dispose && !busyIo.tryLock()) {
// Wait for sync lock
if (forceSync) {
busyIo.lock();
} else if (!dispose && !busyIo.tryLock()) {
// We don't need to wait on normal saves though
saveQueued = true;
return;
}
@@ -426,8 +429,10 @@ public class StandardStorage extends DataStorage {
if (syncEnabled) {
GlobalTimer.delay(
() -> {
if (saveActive.get()) {
AppLayoutModel.get().showQueueEntry(queueEntry, null, false);
synchronized (queueEntry) {
if (saveActive.get()) {
AppLayoutModel.get().showQueueEntry(queueEntry, null, false);
}
}
},
Duration.ofSeconds(5));
@@ -499,8 +504,10 @@ public class StandardStorage extends DataStorage {
disposed = true;
}
saveActive.set(false);
queueEntry.hide();
synchronized (queueEntry) {
saveActive.set(false);
queueEntry.hide();
}
busyIo.unlock();
if (!dispose && saveQueued) {
@@ -545,7 +552,7 @@ public class StandardStorage extends DataStorage {
}
@Override
public boolean supportsSync() {
public boolean syncEnabled() {
return dataStorageSyncHandler.supportsSync();
}
@@ -0,0 +1,18 @@
package io.xpipe.app.store;
import io.xpipe.app.storage.DataStoreAccessScope;
public interface AccessScopeDependencyStore extends AccessScopeStore, DependentDataStore {
default DataStoreAccessScope getAccessScope() {
var deps = getDependencies();
var scopes = deps.stream().map(ref -> ref.getStore() instanceof AccessScopeStore s ? s.getAccessScope() : null)
.filter(s -> s != null)
.toList();
if (scopes.isEmpty()) {
return DataStoreAccessScope.encryption();
}
return DataStoreAccessScope.merge(scopes);
}
}
@@ -466,7 +466,7 @@ public class AppJacksonModule extends SimpleModule {
var e = DataStorage.get()
.getStoreEntryIfPresent(id)
.filter(dataStoreEntry -> dataStoreEntry.getValidity() != DataStoreEntry.Validity.LOAD_FAILED
|| !dataStoreEntry.getStoreNode().isAccessible())
|| (dataStoreEntry.getStoreNode() != null && !dataStoreEntry.getStoreNode().isAccessible()))
.orElse(null);
if (e == null) {
return null;
@@ -39,9 +39,9 @@ the calculation of the default scrollbar skin.
Without this, the min height will be too small
*/
.root .scroll-bar >.decrement-button >.decrement-arrow {
.root .scroll-bar:vertical >.decrement-button >.decrement-arrow {
-fx-padding: 0 3 0 4;
}
.root .scroll-bar >.increment-button >.increment-arrow {
.root .scroll-bar:vertical >.increment-button >.increment-arrow {
-fx-padding: 0 3 0 4;
}
@@ -298,7 +298,14 @@
-fx-background-radius: 4;
-fx-font-size: 1.0em;
-fx-padding: 2 5 2 5;
-fx-background-color: -color-bg-default-transparent;
}
.root:light .store-entry-badge {
-fx-background-color: derive(-color-bg-default-transparent, -5%);
}
.root:dark .store-entry-badge {
-fx-background-color: derive(-color-bg-default-transparent, 5%);
}
.store-entry-badge:focused {
@@ -77,7 +77,7 @@ public class IdentityChoiceBuilder {
private void addSyncCheckListener() {
identity.addListener((observable, oldValue, newValue) -> {
if (DataStorage.get().supportsSync()
if (DataStorage.get().syncEnabled()
&& syncedBase.getValue()
&& newValue instanceof IdentityValue.Ref r
&& r.unwrap() instanceof LocalIdentityStore) {
@@ -28,7 +28,7 @@ public class LocalIdentityConvertHubLeafProvider implements HubLeafProvider<Loca
@Override
public boolean isApplicable(DataStoreEntryRef<LocalIdentityStore> o) {
return DataStorage.get().supportsSync() && !MultiIdentityStore.isExclusivelyHeld(o.asNeeded());
return DataStorage.get().syncEnabled() && !MultiIdentityStore.isExclusivelyHeld(o.asNeeded());
}
@Override
@@ -1,14 +1,18 @@
package io.xpipe.ext.base.identity;
import io.xpipe.app.beacon.BeaconAuthMethod;
import io.xpipe.app.identity.NoIdentityStrategy;
import io.xpipe.app.identity.SshIdentityStrategy;
import io.xpipe.app.identity.UsernameStrategy;
import io.xpipe.app.secret.EncryptedValue;
import io.xpipe.app.secret.SecretNoneStrategy;
import io.xpipe.app.secret.SecretRetrievalStrategy;
import io.xpipe.app.storage.DataStoreAccessScope;
import io.xpipe.app.storage.DataStoreEntryRef;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.app.store.AccessScopeStore;
import io.xpipe.app.store.DataStore;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.Value;
@@ -23,12 +27,26 @@ import java.util.List;
@Value
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class LocalIdentityStore extends IdentityStore {
public class LocalIdentityStore extends IdentityStore implements AccessScopeStore {
String username;
EncryptedValue<SecretRetrievalStrategy> password;
EncryptedValue<SshIdentityStrategy> sshIdentity;
@Override
public DataStoreAccessScope getAccessScope() {
return DataStoreAccessScope.encryption();
}
@Override
public DataStore withUpdatedPrincipals() {
return LocalIdentityStore.builder()
.username(username)
.password(password != null ? password.withUpdatedPrincipals() : null)
.sshIdentity(sshIdentity != null ? sshIdentity.withUpdatedPrincipals() : null)
.build();
}
@Override
public String toSummary() {
var user = getUsername().hasUser()
@@ -5,10 +5,7 @@ import io.xpipe.app.identity.NoIdentityStrategy;
import io.xpipe.app.identity.SshIdentityStrategyChoiceConfig;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.platform.OptionsChoiceBuilder;
import io.xpipe.app.secret.EncryptedValue;
import io.xpipe.app.secret.SecretNoneStrategy;
import io.xpipe.app.secret.SecretRetrievalStrategy;
import io.xpipe.app.secret.SecretStrategyChoiceConfig;
import io.xpipe.app.secret.*;
import io.xpipe.app.storage.*;
import io.xpipe.app.store.DataStore;
import io.xpipe.app.util.*;
@@ -39,9 +36,10 @@ public class LocalIdentityStoreProvider extends IdentityStoreProvider {
var pass = new SimpleObjectProperty<>(st.getPassword());
var identity = new SimpleObjectProperty<>(st.getSshIdentity());
var current = DataStoreAccessScope.encryption();
var sshIdentityChoiceConfig = SshIdentityStrategyChoiceConfig.builder()
.allowKeyFileSync(false)
.scopeCheck(() -> DataStoreAccessScope.current())
.scopeCheck(() -> current)
.build();
var passwordChoice = OptionsChoiceBuilder.builder()
@@ -70,12 +68,12 @@ public class LocalIdentityStoreProvider extends IdentityStoreProvider {
.password(
st.getEncryptedPassword() != null
? st.getEncryptedPassword().with(pass.get())
: EncryptedValue.of(pass.get(), DataStoreAccessScope.current()))
: EncryptedValue.of(pass.get(), current))
.sshIdentity(
st.getEncryptedSshIdentity() != null
? st.getEncryptedSshIdentity()
.with(identity.get())
: EncryptedValue.of(identity.get(), DataStoreAccessScope.current()))
: EncryptedValue.of(identity.get(), current))
.build();
},
store)
@@ -84,9 +82,10 @@ public class LocalIdentityStoreProvider extends IdentityStoreProvider {
@Override
public DataStore defaultStore(DataStoreCategory category) {
var current = DataStoreAccessScope.encryption();
return LocalIdentityStore.builder()
.password(EncryptedValue.of(new SecretNoneStrategy(), DataStoreAccessScope.current()))
.sshIdentity(EncryptedValue.of(new NoIdentityStrategy(), DataStoreAccessScope.current()))
.password(EncryptedValue.of(new SecretNoneStrategy(), current))
.sshIdentity(EncryptedValue.of(new NoIdentityStrategy(), current))
.build();
}
@@ -31,7 +31,7 @@ public class MultiIdentityConvertHubLeafProvider implements HubLeafProvider<Mult
@Override
public boolean isApplicable(DataStoreEntryRef<MultiIdentityStore> o) {
return DataStorage.get().supportsSync()
return DataStorage.get().syncEnabled()
&& o.getStore().areAnyChildrenLocal()
&& o.getStore().areAllIdentitiesAccessible()
&& !o.getStore().hasNestedMultiIdentities();
@@ -53,6 +53,11 @@ public class MultiIdentityStore extends IdentityStore
Boolean exclusive;
DataStoreAccessScope accessScope;
@Override
public DataStoreAccessScope getAccessScope() {
return accessScope != null ? accessScope : DataStoreAccessScope.encryption();
}
@Override
public String toSummary() {
var selected = getSelected();
@@ -32,12 +32,15 @@ import java.util.List;
public class SyncedIdentityStore extends IdentityStore implements AccessScopeStore {
String username;
// We can encrypt it with only the vault key as
// per user stores are additionally encrypted on the entry level
EncryptedValue<SecretRetrievalStrategy> password;
EncryptedValue<SshIdentityStrategy> sshIdentity;
DataStoreAccessScope accessScope;
@Override
public DataStoreAccessScope getAccessScope() {
return accessScope != null ? accessScope : DataStoreAccessScope.encryption();
}
@Override
public String toSummary() {
var user = getUsername().hasUser()
@@ -1,6 +1,7 @@
package io.xpipe.ext.base.identity;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.hub.creation.StoreCreationModel;
import io.xpipe.app.hub.entry.StoreEntryWrapper;
import io.xpipe.app.identity.KeyFileStrategy;
@@ -10,6 +11,7 @@ import io.xpipe.app.identity.SshIdentityStrategyChoiceConfig;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.platform.OptionsChoiceBuilder;
import io.xpipe.app.platform.Validator;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.prefs.DataStorageAccessType;
import io.xpipe.app.secret.*;
import io.xpipe.app.storage.*;
@@ -27,7 +29,7 @@ public class SyncedIdentityStoreProvider extends IdentityStoreProvider {
@Override
public boolean allowCreation() {
return DataStorage.get().supportsSync();
return AppPrefs.get().enableGitStorage().get();
}
@Override
@@ -33,7 +33,7 @@ public class SyncConfigHubLeafProvider implements HubLeafProvider<SyncConfigStor
@Override
public boolean isApplicable(DataStoreEntryRef<SyncConfigStore> o) {
return DataStorage.get().supportsSync() && !o.get().getProvider().isSyncable(o.get());
return DataStorage.get().syncEnabled() && !o.get().getProvider().isSyncable(o.get());
}
@Override
+5 -1
View File
@@ -2081,7 +2081,8 @@ certificateRole=Role name
certificateRoleDescription=The role with which to sign the public key
testConfig=Test configuration
testConfigDescription=Verify that the configuration works
applyingVaultChanges=Applying vault changes ...
#force
applyingVaultChanges=Applying vault encryption changes ...
allowExternalApiRequests=Allow external API requests
allowExternalApiRequestsDescription=By default, the API server is only listening on localhost. If you want to accept connections from outside localhost as well, you can enable this option.
certificateRenewCommand=Certificate renewal command
@@ -2337,3 +2338,6 @@ vaultMigratedTitle=Vault migration
vaultMigratedContent=Your vault was migrated. Note that the migration removed any kind of vault users/groups, so you will have to recreate them in the settings menu.
vaultMigratedGitContent=Your vault was migrated. Note that the migration removed any kind of vault users/groups, so you will have to recreate them in the settings menu.\n\nOnce you have done this, you can also reenable the git git sync as well.
orderDisabledNotice=Reordering not possible in current sort mode
skipAllRoles=Skip all roles
skipRole=Skip role
disableRole=Disable role