mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-22 07:30:24 +00:00
More secret and elevation rework
This commit is contained in:
parent
1b20612cd5
commit
d29614afcc
13 changed files with 105 additions and 102 deletions
|
@ -10,7 +10,6 @@ import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
import io.xpipe.app.storage.DataStorage;
|
import io.xpipe.app.storage.DataStorage;
|
||||||
import io.xpipe.app.util.ApplicationHelper;
|
import io.xpipe.app.util.ApplicationHelper;
|
||||||
import io.xpipe.app.util.ElevationAccess;
|
|
||||||
import io.xpipe.app.util.PasswordLockSecretValue;
|
import io.xpipe.app.util.PasswordLockSecretValue;
|
||||||
import io.xpipe.core.util.InPlaceSecretValue;
|
import io.xpipe.core.util.InPlaceSecretValue;
|
||||||
import io.xpipe.core.util.ModuleHelper;
|
import io.xpipe.core.util.ModuleHelper;
|
||||||
|
@ -170,9 +169,9 @@ public class AppPrefs {
|
||||||
return disableTerminalRemotePasswordPreparation;
|
return disableTerminalRemotePasswordPreparation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final Property<ElevationAccess> elevationPolicy = map(new SimpleObjectProperty<>(ElevationAccess.ALLOW), "elevationPolicy", ElevationAccess.class);
|
public final Property<Boolean> alwaysConfirmElevation = map(new SimpleObjectProperty<>(false), "alwaysConfirmElevation", Boolean.class);
|
||||||
public ObservableValue<ElevationAccess> elevationPolicy() {
|
public ObservableValue<Boolean> alwaysConfirmElevation() {
|
||||||
return elevationPolicy;
|
return alwaysConfirmElevation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final BooleanProperty dontCachePasswords = map(new SimpleBooleanProperty(false), "dontCachePasswords", Boolean.class);
|
public final BooleanProperty dontCachePasswords = map(new SimpleBooleanProperty(false), "dontCachePasswords", Boolean.class);
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package io.xpipe.app.prefs;
|
package io.xpipe.app.prefs;
|
||||||
|
|
||||||
import io.xpipe.app.fxcomps.Comp;
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.util.ElevationAccessChoiceComp;
|
|
||||||
import io.xpipe.app.util.OptionsBuilder;
|
import io.xpipe.app.util.OptionsBuilder;
|
||||||
|
|
||||||
public class SecurityCategory extends AppPrefsCategory {
|
public class SecurityCategory extends AppPrefsCategory {
|
||||||
|
@ -16,8 +15,8 @@ public class SecurityCategory extends AppPrefsCategory {
|
||||||
var builder = new OptionsBuilder();
|
var builder = new OptionsBuilder();
|
||||||
builder.addTitle("securityPolicy")
|
builder.addTitle("securityPolicy")
|
||||||
.sub(new OptionsBuilder()
|
.sub(new OptionsBuilder()
|
||||||
.nameAndDescription("elevationPolicy")
|
.nameAndDescription("alwaysConfirmElevation")
|
||||||
.addComp(new ElevationAccessChoiceComp(prefs.elevationPolicy).minWidth(250), prefs.elevationPolicy)
|
.addToggle(prefs.alwaysConfirmElevation)
|
||||||
.nameAndDescription("dontCachePasswords")
|
.nameAndDescription("dontCachePasswords")
|
||||||
.addToggle(prefs.dontCachePasswords)
|
.addToggle(prefs.dontCachePasswords)
|
||||||
.nameAndDescription("denyTempScriptCreation")
|
.nameAndDescription("denyTempScriptCreation")
|
||||||
|
|
|
@ -4,6 +4,7 @@ import io.xpipe.app.core.AppI18n;
|
||||||
import io.xpipe.app.core.AppWindowHelper;
|
import io.xpipe.app.core.AppWindowHelper;
|
||||||
import io.xpipe.app.fxcomps.impl.SecretFieldComp;
|
import io.xpipe.app.fxcomps.impl.SecretFieldComp;
|
||||||
import io.xpipe.core.util.InPlaceSecretValue;
|
import io.xpipe.core.util.InPlaceSecretValue;
|
||||||
|
import javafx.animation.AnimationTimer;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.scene.control.Alert;
|
import javafx.scene.control.Alert;
|
||||||
|
@ -12,25 +13,54 @@ import javafx.stage.Stage;
|
||||||
|
|
||||||
public class AskpassAlert {
|
public class AskpassAlert {
|
||||||
|
|
||||||
public static SecretQueryResult queryRaw(String prompt) {
|
public static SecretQueryResult queryRaw(String prompt, InPlaceSecretValue secretValue) {
|
||||||
var prop = new SimpleObjectProperty<InPlaceSecretValue>();
|
var prop = new SimpleObjectProperty<>(secretValue);
|
||||||
var r = AppWindowHelper.showBlockingAlert(alert -> {
|
var r = AppWindowHelper.showBlockingAlert(alert -> {
|
||||||
alert.setTitle(AppI18n.get("askpassAlertTitle"));
|
alert.setTitle(AppI18n.get("askpassAlertTitle"));
|
||||||
alert.setHeaderText(prompt);
|
alert.setHeaderText(prompt);
|
||||||
alert.setAlertType(Alert.AlertType.CONFIRMATION);
|
alert.setAlertType(Alert.AlertType.CONFIRMATION);
|
||||||
|
|
||||||
var text = new SecretFieldComp(prop).createRegion();
|
var text = new SecretFieldComp(prop).createStructure().get();
|
||||||
alert.getDialogPane().setContent(new StackPane(text));
|
alert.getDialogPane().setContent(new StackPane(text));
|
||||||
var stage = (Stage) alert.getDialogPane().getScene().getWindow();
|
var stage = (Stage) alert.getDialogPane().getScene().getWindow();
|
||||||
stage.setAlwaysOnTop(true);
|
stage.setAlwaysOnTop(true);
|
||||||
|
|
||||||
|
var anim = new AnimationTimer() {
|
||||||
|
|
||||||
|
private long lastRun = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(long now) {
|
||||||
|
if (lastRun == 0) {
|
||||||
|
lastRun = now;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long elapsed = (now - lastRun) / 1_000_000;
|
||||||
|
if (elapsed < 1000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stage.requestFocus();
|
||||||
|
lastRun = now;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
alert.setOnShown(event -> {
|
alert.setOnShown(event -> {
|
||||||
stage.requestFocus();
|
stage.requestFocus();
|
||||||
|
anim.start();
|
||||||
// Wait 1 pulse before focus so that the scene can be assigned to text
|
// Wait 1 pulse before focus so that the scene can be assigned to text
|
||||||
Platform.runLater(text::requestFocus);
|
Platform.runLater(() -> {
|
||||||
|
text.requestFocus();
|
||||||
|
text.end();
|
||||||
|
});
|
||||||
event.consume();
|
event.consume();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
alert.setOnHiding(event -> {
|
||||||
|
anim.stop();
|
||||||
|
});
|
||||||
|
|
||||||
})
|
})
|
||||||
.filter(b -> b.getButtonData().isDefaultButton())
|
.filter(b -> b.getButtonData().isDefaultButton())
|
||||||
.map(t -> {
|
.map(t -> {
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
package io.xpipe.app.util;
|
|
||||||
|
|
||||||
import io.xpipe.app.core.AppI18n;
|
|
||||||
import io.xpipe.app.core.AppWindowHelper;
|
|
||||||
import io.xpipe.app.prefs.AppPrefs;
|
|
||||||
import io.xpipe.app.storage.DataStorage;
|
|
||||||
import io.xpipe.core.process.ShellControl;
|
|
||||||
|
|
||||||
public enum ElevationAccess {
|
|
||||||
|
|
||||||
ALLOW {
|
|
||||||
@Override
|
|
||||||
public boolean requestElevationUsage(ShellControl shellControl) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ASK {
|
|
||||||
@Override
|
|
||||||
public boolean requestElevationUsage(ShellControl shellControl) {
|
|
||||||
var name = shellControl.getSourceStore().flatMap(shellStore -> DataStorage.get().getStoreEntryIfPresent(shellStore))
|
|
||||||
.map(entry -> entry.getName()).orElse("a system");
|
|
||||||
return AppWindowHelper.showConfirmationAlert(
|
|
||||||
AppI18n.observable("elevationRequestTitle"),
|
|
||||||
AppI18n.observable("elevationRequestHeader", name),
|
|
||||||
AppI18n.observable("elevationRequestDescription")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
DENY {
|
|
||||||
@Override
|
|
||||||
public boolean requestElevationUsage(ShellControl shellControl) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public boolean requestElevationUsage(ShellControl shellControl) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean request(ShellControl shellControl) {
|
|
||||||
if (AppPrefs.get() == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return AppPrefs.get().elevationPolicy().getValue().requestElevationUsage(shellControl);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
package io.xpipe.app.util;
|
|
||||||
|
|
||||||
import io.xpipe.app.core.AppI18n;
|
|
||||||
import io.xpipe.app.fxcomps.SimpleComp;
|
|
||||||
import io.xpipe.app.fxcomps.impl.ChoiceComp;
|
|
||||||
import javafx.beans.property.Property;
|
|
||||||
import javafx.beans.value.ObservableValue;
|
|
||||||
import javafx.scene.layout.Region;
|
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
|
|
||||||
public class ElevationAccessChoiceComp extends SimpleComp {
|
|
||||||
|
|
||||||
private final Property<ElevationAccess> value;
|
|
||||||
|
|
||||||
public ElevationAccessChoiceComp(Property<ElevationAccess> value) {this.value = value;}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Region createSimple() {
|
|
||||||
var map = new LinkedHashMap<ElevationAccess, ObservableValue<String>>();
|
|
||||||
map.put(ElevationAccess.ALLOW, AppI18n.observable("allow"));
|
|
||||||
map.put(ElevationAccess.ASK, AppI18n.observable("ask"));
|
|
||||||
map.put(ElevationAccess.DENY, AppI18n.observable("deny"));
|
|
||||||
var c = new ChoiceComp<>(value, map, false);
|
|
||||||
return c.createRegion();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -26,8 +26,8 @@ public class SecretManager {
|
||||||
.findFirst();
|
.findFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SecretQueryProgress expectCacheablePrompt(UUID request, UUID storeId, CountDown countDown) {
|
public static SecretQueryProgress expectElevationPrompt(UUID request, UUID secretId, CountDown countDown, boolean askIfNeeded) {
|
||||||
var p = new SecretQueryProgress(request, storeId, List.of(SecretQuery.prompt(true)), SecretQuery.prompt(false), countDown);
|
var p = new SecretQueryProgress(request, secretId, List.of(askIfNeeded ? SecretQuery.elevation(secretId) : SecretQuery.prompt(true)), SecretQuery.prompt(false), countDown);
|
||||||
progress.add(p);
|
progress.add(p);
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,55 @@
|
||||||
package io.xpipe.app.util;
|
package io.xpipe.app.util;
|
||||||
|
|
||||||
|
import io.xpipe.app.prefs.AppPrefs;
|
||||||
|
import io.xpipe.core.util.SecretReference;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public interface SecretQuery {
|
public interface SecretQuery {
|
||||||
|
|
||||||
|
static SecretQuery elevation(UUID secretId) {
|
||||||
|
return new SecretQuery() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SecretQueryResult query(String prompt) {
|
||||||
|
return AskpassAlert.queryRaw(prompt, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<SecretQueryResult> retrieveCache(String prompt, SecretReference reference) {
|
||||||
|
var found = SecretQuery.super.retrieveCache(prompt, reference);
|
||||||
|
if (found.isEmpty()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
var ask = AppPrefs.get().alwaysConfirmElevation().getValue();
|
||||||
|
if (!ask) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
var inPlace = found.get().getSecret().inPlace();
|
||||||
|
var r = AskpassAlert.queryRaw(prompt, inPlace);
|
||||||
|
return r.isCancelled() ? Optional.empty() : found;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean cache() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean retryOnFail() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
static SecretQuery prompt(boolean cache) {
|
static SecretQuery prompt(boolean cache) {
|
||||||
return new SecretQuery() {
|
return new SecretQuery() {
|
||||||
@Override
|
@Override
|
||||||
public SecretQueryResult query(String prompt) {
|
public SecretQueryResult query(String prompt) {
|
||||||
return AskpassAlert.queryRaw(prompt);
|
return AskpassAlert.queryRaw(prompt, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -21,6 +64,11 @@ public interface SecretQuery {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default Optional<SecretQueryResult> retrieveCache(String prompt, SecretReference reference) {
|
||||||
|
var r = SecretManager.get(reference);
|
||||||
|
return r.map(secretValue -> new SecretQueryResult(secretValue, false));
|
||||||
|
}
|
||||||
|
|
||||||
SecretQueryResult query(String prompt);
|
SecretQueryResult query(String prompt);
|
||||||
|
|
||||||
boolean cache();
|
boolean cache();
|
||||||
|
|
|
@ -78,9 +78,14 @@ public class SecretQueryProgress {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldCache) {
|
if (shouldCache) {
|
||||||
var cached = SecretManager.get(ref);
|
var cached = sup.retrieveCache(prompt, ref);
|
||||||
if (cached.isPresent()) {
|
if (cached.isPresent()) {
|
||||||
return cached.get();
|
if (cached.get().isCancelled()) {
|
||||||
|
requestCancelled = true;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cached.get().getSecret();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -83,7 +83,7 @@ public interface SecretRetrievalStrategy {
|
||||||
return new SecretQuery() {
|
return new SecretQuery() {
|
||||||
@Override
|
@Override
|
||||||
public SecretQueryResult query(String prompt) {
|
public SecretQueryResult query(String prompt) {
|
||||||
return AskpassAlert.queryRaw(prompt);
|
return AskpassAlert.queryRaw(prompt, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -3,6 +3,7 @@ package io.xpipe.app.util;
|
||||||
import io.xpipe.beacon.ClientException;
|
import io.xpipe.beacon.ClientException;
|
||||||
import io.xpipe.beacon.ServerException;
|
import io.xpipe.beacon.ServerException;
|
||||||
import io.xpipe.core.process.ProcessControl;
|
import io.xpipe.core.process.ProcessControl;
|
||||||
|
import io.xpipe.core.process.ProcessOutputException;
|
||||||
import io.xpipe.core.process.TerminalInitScriptConfig;
|
import io.xpipe.core.process.TerminalInitScriptConfig;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
|
@ -64,7 +65,8 @@ public class TerminalLauncherManager {
|
||||||
var r = e.getResult();
|
var r = e.getResult();
|
||||||
if (r instanceof ResultFailure failure) {
|
if (r instanceof ResultFailure failure) {
|
||||||
entries.remove(request);
|
entries.remove(request);
|
||||||
throw new ServerException(failure.getThrowable());
|
var t = failure.getThrowable();
|
||||||
|
throw new ServerException(t instanceof ProcessOutputException pex ? pex.getOutput() : t.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return ((ResultSuccess) r).getTargetScript();
|
return ((ResultSuccess) r).getTargetScript();
|
||||||
|
|
|
@ -7,8 +7,8 @@ common=Common
|
||||||
key=Key
|
key=Key
|
||||||
color=Color
|
color=Color
|
||||||
roadmap=Roadmap and feature requests
|
roadmap=Roadmap and feature requests
|
||||||
elevationPolicy=Elevation policy
|
alwaysConfirmElevation=Always confirm elevation
|
||||||
elevationPolicyDescription=Controls how to handle cases when elevated access might be required to run a command on a system, e.g. with sudo.\n\nThis can be overridden by connection-specific settings.
|
alwaysConfirmElevationDescription=Controls how to handle cases when elevated access is required to run a command on a system, e.g. with sudo.\n\nBy default, any sudo credentials are cached during a session and automatically provided when needed. If this option is enabled, it will ask you to confirm the elevation access every time.
|
||||||
allow=Allow
|
allow=Allow
|
||||||
ask=Ask
|
ask=Ask
|
||||||
deny=Deny
|
deny=Deny
|
||||||
|
|
|
@ -95,6 +95,8 @@ public interface ShellDialect {
|
||||||
|
|
||||||
String getDiscardOperator();
|
String getDiscardOperator();
|
||||||
|
|
||||||
|
String nullStdin(String command);
|
||||||
|
|
||||||
String getScriptPermissionsCommand(String file);
|
String getScriptPermissionsCommand(String file);
|
||||||
|
|
||||||
ShellDialectAskpass getAskpass();
|
ShellDialectAskpass getAskpass();
|
||||||
|
|
|
@ -2,13 +2,5 @@ package io.xpipe.core.process;
|
||||||
|
|
||||||
public interface ShellSecurityPolicy {
|
public interface ShellSecurityPolicy {
|
||||||
|
|
||||||
boolean checkElevate(ShellControl shellControl);
|
|
||||||
|
|
||||||
default void elevateOrThrow(ShellControl shellControl) {
|
|
||||||
if (!checkElevate(shellControl)) {
|
|
||||||
throw new UnsupportedOperationException("Elevation is not allowed for this system");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean permitTempScriptCreation();
|
boolean permitTempScriptCreation();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue