More secret and elevation rework

This commit is contained in:
crschnick 2024-02-19 00:27:04 +00:00
parent 1b20612cd5
commit d29614afcc
13 changed files with 105 additions and 102 deletions

View file

@ -10,7 +10,6 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.ApplicationHelper;
import io.xpipe.app.util.ElevationAccess;
import io.xpipe.app.util.PasswordLockSecretValue;
import io.xpipe.core.util.InPlaceSecretValue;
import io.xpipe.core.util.ModuleHelper;
@ -170,9 +169,9 @@ public class AppPrefs {
return disableTerminalRemotePasswordPreparation;
}
public final Property<ElevationAccess> elevationPolicy = map(new SimpleObjectProperty<>(ElevationAccess.ALLOW), "elevationPolicy", ElevationAccess.class);
public ObservableValue<ElevationAccess> elevationPolicy() {
return elevationPolicy;
public final Property<Boolean> alwaysConfirmElevation = map(new SimpleObjectProperty<>(false), "alwaysConfirmElevation", Boolean.class);
public ObservableValue<Boolean> alwaysConfirmElevation() {
return alwaysConfirmElevation;
}
public final BooleanProperty dontCachePasswords = map(new SimpleBooleanProperty(false), "dontCachePasswords", Boolean.class);

View file

@ -1,7 +1,6 @@
package io.xpipe.app.prefs;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.util.ElevationAccessChoiceComp;
import io.xpipe.app.util.OptionsBuilder;
public class SecurityCategory extends AppPrefsCategory {
@ -16,8 +15,8 @@ public class SecurityCategory extends AppPrefsCategory {
var builder = new OptionsBuilder();
builder.addTitle("securityPolicy")
.sub(new OptionsBuilder()
.nameAndDescription("elevationPolicy")
.addComp(new ElevationAccessChoiceComp(prefs.elevationPolicy).minWidth(250), prefs.elevationPolicy)
.nameAndDescription("alwaysConfirmElevation")
.addToggle(prefs.alwaysConfirmElevation)
.nameAndDescription("dontCachePasswords")
.addToggle(prefs.dontCachePasswords)
.nameAndDescription("denyTempScriptCreation")

View file

@ -4,6 +4,7 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.app.fxcomps.impl.SecretFieldComp;
import io.xpipe.core.util.InPlaceSecretValue;
import javafx.animation.AnimationTimer;
import javafx.application.Platform;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Alert;
@ -12,25 +13,54 @@ import javafx.stage.Stage;
public class AskpassAlert {
public static SecretQueryResult queryRaw(String prompt) {
var prop = new SimpleObjectProperty<InPlaceSecretValue>();
public static SecretQueryResult queryRaw(String prompt, InPlaceSecretValue secretValue) {
var prop = new SimpleObjectProperty<>(secretValue);
var r = AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("askpassAlertTitle"));
alert.setHeaderText(prompt);
alert.setAlertType(Alert.AlertType.CONFIRMATION);
var text = new SecretFieldComp(prop).createRegion();
var text = new SecretFieldComp(prop).createStructure().get();
alert.getDialogPane().setContent(new StackPane(text));
var stage = (Stage) alert.getDialogPane().getScene().getWindow();
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 -> {
stage.requestFocus();
anim.start();
// 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();
});
alert.setOnHiding(event -> {
anim.stop();
});
})
.filter(b -> b.getButtonData().isDefaultButton())
.map(t -> {

View file

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

View file

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

View file

@ -26,8 +26,8 @@ public class SecretManager {
.findFirst();
}
public static SecretQueryProgress expectCacheablePrompt(UUID request, UUID storeId, CountDown countDown) {
var p = new SecretQueryProgress(request, storeId, List.of(SecretQuery.prompt(true)), SecretQuery.prompt(false), countDown);
public static SecretQueryProgress expectElevationPrompt(UUID request, UUID secretId, CountDown countDown, boolean askIfNeeded) {
var p = new SecretQueryProgress(request, secretId, List.of(askIfNeeded ? SecretQuery.elevation(secretId) : SecretQuery.prompt(true)), SecretQuery.prompt(false), countDown);
progress.add(p);
return p;
}

View file

@ -1,12 +1,55 @@
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 {
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) {
return new SecretQuery() {
@Override
public SecretQueryResult query(String prompt) {
return AskpassAlert.queryRaw(prompt);
return AskpassAlert.queryRaw(prompt, null);
}
@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);
boolean cache();

View file

@ -78,9 +78,14 @@ public class SecretQueryProgress {
}
if (shouldCache) {
var cached = SecretManager.get(ref);
var cached = sup.retrieveCache(prompt, ref);
if (cached.isPresent()) {
return cached.get();
if (cached.get().isCancelled()) {
requestCancelled = true;
return null;
}
return cached.get().getSecret();
}
}

View file

@ -83,7 +83,7 @@ public interface SecretRetrievalStrategy {
return new SecretQuery() {
@Override
public SecretQueryResult query(String prompt) {
return AskpassAlert.queryRaw(prompt);
return AskpassAlert.queryRaw(prompt, null);
}
@Override

View file

@ -3,6 +3,7 @@ package io.xpipe.app.util;
import io.xpipe.beacon.ClientException;
import io.xpipe.beacon.ServerException;
import io.xpipe.core.process.ProcessControl;
import io.xpipe.core.process.ProcessOutputException;
import io.xpipe.core.process.TerminalInitScriptConfig;
import lombok.Setter;
import lombok.Value;
@ -64,7 +65,8 @@ public class TerminalLauncherManager {
var r = e.getResult();
if (r instanceof ResultFailure failure) {
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();

View file

@ -7,8 +7,8 @@ common=Common
key=Key
color=Color
roadmap=Roadmap and feature requests
elevationPolicy=Elevation policy
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.
alwaysConfirmElevation=Always confirm elevation
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
ask=Ask
deny=Deny

View file

@ -95,6 +95,8 @@ public interface ShellDialect {
String getDiscardOperator();
String nullStdin(String command);
String getScriptPermissionsCommand(String file);
ShellDialectAskpass getAskpass();

View file

@ -2,13 +2,5 @@ package io.xpipe.core.process;
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();
}