mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-27 21:47:50 -04:00
settings: enforce administrator-managed lock settings (#3897)
port 2.0
(cherry picked from commit 404c9672d7)
This commit is contained in:
1 parent
4e0985fbbb
commit
5ef5a7391b
18 files changed
+526
-56
No files matched your search
@@ -125,7 +125,7 @@ func dmsExtraEnv(string) []string {
|
||||
env = append(env, gpu.EGLVendorEnv()...)
|
||||
policyEnv, err := clipolicy.ShellEnv(afero.NewOsFs())
|
||||
if err != nil {
|
||||
log.Errorf("CLI policy unreadable, enforcing every shell switch: %v", err)
|
||||
log.Errorf("CLI policy unreadable, enforcing every shell switch and ignoring its lock settings: %v", err)
|
||||
}
|
||||
env = append(env, policyEnv...)
|
||||
// systemd user services miss the login shell PATH, so tools in ~/.local/bin
|
||||
|
||||
@@ -4,8 +4,10 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/duxlabs-dev/dux-shell/core/internal/log"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
@@ -25,6 +27,8 @@ type File struct {
|
||||
DisableCustomLockPAM *bool `json:"disable_custom_lock_pam"`
|
||||
DisableUserPlugins *bool `json:"disable_user_plugins"`
|
||||
DisableSettingsIPCSet *bool `json:"disable_settings_ipc_set"`
|
||||
|
||||
LockSettings map[string]json.RawMessage `json:"lock_settings"`
|
||||
}
|
||||
|
||||
// shellSwitches reach the shell as DMS_POLICY_<KEY> in its launch environment.
|
||||
@@ -39,6 +43,22 @@ var shellSwitches = []struct {
|
||||
{"disable_settings_ipc_set", func(p *File) *bool { return p.DisableSettingsIPCSet }},
|
||||
}
|
||||
|
||||
// lockSettingKeys are the shell settings lock_settings may manage.
|
||||
var lockSettingKeys = []string{
|
||||
"customPowerActionLock",
|
||||
"lockBeforeSuspend",
|
||||
"loginctlLockIntegration",
|
||||
"lockAtStartup",
|
||||
"acLockTimeout",
|
||||
"batteryLockTimeout",
|
||||
"fadeToLockEnabled",
|
||||
"fadeToLockGracePeriod",
|
||||
"enableFprint",
|
||||
"maxFprintTries",
|
||||
"enableU2f",
|
||||
"u2fMode",
|
||||
}
|
||||
|
||||
// LoadFile returns nil without an error when the file does not exist.
|
||||
func LoadFile(fs afero.Fs, path string) (*File, error) {
|
||||
data, err := afero.ReadFile(fs, path)
|
||||
@@ -104,12 +124,50 @@ func LogindUnlockDisabled(fs afero.Fs) (bool, error) {
|
||||
return flag(policies, func(p *File) *bool { return p.DisableLogindUnlock }), nil
|
||||
}
|
||||
|
||||
// ShellEnv returns every shell switch as DMS_POLICY_<KEY>=1 or 0, so a value
|
||||
// lockSettings merges lock_settings per key, AdminPath over PackagedPath,
|
||||
// without null values and keys outside lockSettingKeys.
|
||||
func lockSettings(policies []*File) map[string]json.RawMessage {
|
||||
settings := make(map[string]json.RawMessage)
|
||||
for _, policy := range policies {
|
||||
for key, value := range policy.LockSettings {
|
||||
if !slices.Contains(lockSettingKeys, key) {
|
||||
log.Warnf("CLI policy: ignoring lock_settings key %q, which is not a managed lock setting", key)
|
||||
continue
|
||||
}
|
||||
if string(value) == "null" {
|
||||
continue
|
||||
}
|
||||
settings[key] = value
|
||||
}
|
||||
}
|
||||
return settings
|
||||
}
|
||||
|
||||
// LockSettings returns the managed lock settings as raw JSON by key. A policy
|
||||
// file that fails to load manages none, and its error is returned.
|
||||
func LockSettings(fs afero.Fs) (map[string]json.RawMessage, error) {
|
||||
policies, err := load(fs)
|
||||
return lockSettings(policies), err
|
||||
}
|
||||
|
||||
// LockSettingBool returns a managed boolean lock setting, or nil when it is
|
||||
// not managed or not a boolean, which the shell ignores too.
|
||||
func LockSettingBool(settings map[string]json.RawMessage, key string) *bool {
|
||||
var value *bool
|
||||
if err := json.Unmarshal(settings[key], &value); err != nil {
|
||||
return nil
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// ShellEnv returns every shell switch as DMS_POLICY_<KEY>=1 or 0 and the
|
||||
// managed lock settings as DMS_POLICY_LOCK_SETTINGS=<JSON object>, so a value
|
||||
// inherited by the shell never counts. A policy file that fails to load turns
|
||||
// every switch on, and its error is returned with them.
|
||||
// every switch on and manages no lock setting, and its error is returned with
|
||||
// them.
|
||||
func ShellEnv(fs afero.Fs) ([]string, error) {
|
||||
policies, err := load(fs)
|
||||
env := make([]string, 0, len(shellSwitches))
|
||||
env := make([]string, 0, len(shellSwitches)+1)
|
||||
for _, s := range shellSwitches {
|
||||
value := "0"
|
||||
if err != nil || flag(policies, s.value) {
|
||||
@@ -117,5 +175,8 @@ func ShellEnv(fs afero.Fs) ([]string, error) {
|
||||
}
|
||||
env = append(env, "DMS_POLICY_"+strings.ToUpper(s.key)+"="+value)
|
||||
}
|
||||
// The values come from json.Unmarshal, so they marshal.
|
||||
settings, _ := json.Marshal(lockSettings(policies))
|
||||
env = append(env, "DMS_POLICY_LOCK_SETTINGS="+string(settings))
|
||||
return env, err
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package clipolicy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -124,6 +125,7 @@ func TestShellEnv(t *testing.T) {
|
||||
want = append(want, other+"=0")
|
||||
}
|
||||
}
|
||||
want = append(want, "DMS_POLICY_LOCK_SETTINGS={}")
|
||||
|
||||
env, err := ShellEnv(fs)
|
||||
require.NoError(t, err)
|
||||
@@ -133,11 +135,12 @@ func TestShellEnv(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellEnvMalformedPolicyTurnsEverySwitchOn(t *testing.T) {
|
||||
func TestShellEnvMalformedPolicyTurnsEverySwitchOnAndIgnoresLockSettings(t *testing.T) {
|
||||
fs := afero.NewMemMapFs()
|
||||
require.NoError(t, afero.WriteFile(fs, PackagedPath, []byte(`{"lock_settings": {"lockBeforeSuspend": true}}`), 0o644))
|
||||
require.NoError(t, afero.WriteFile(fs, AdminPath, []byte("{not json"), 0o644))
|
||||
|
||||
var want []string
|
||||
want := []string{"DMS_POLICY_LOCK_SETTINGS={}"}
|
||||
for _, name := range shellSwitchEnv {
|
||||
want = append(want, name+"=1")
|
||||
}
|
||||
@@ -146,3 +149,83 @@ func TestShellEnvMalformedPolicyTurnsEverySwitchOn(t *testing.T) {
|
||||
require.ErrorContains(t, err, "failed to parse "+AdminPath)
|
||||
assert.ElementsMatch(t, want, env)
|
||||
}
|
||||
|
||||
func TestLockSettings(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
packaged string
|
||||
admin string
|
||||
want string
|
||||
wantErr string
|
||||
}{
|
||||
{name: "no policy files", want: `{}`},
|
||||
{name: "no lock settings", admin: `{"disable_registries": true}`, want: `{}`},
|
||||
{name: "packaged policy", packaged: `{"lock_settings": {"lockBeforeSuspend": true, "customPowerActionLock": ""}}`, want: `{"lockBeforeSuspend": true, "customPowerActionLock": ""}`},
|
||||
{name: "admin policy overrides packaged per key", packaged: `{"lock_settings": {"lockBeforeSuspend": true, "acLockTimeout": 300}}`, admin: `{"lock_settings": {"lockBeforeSuspend": false}}`, want: `{"lockBeforeSuspend": false, "acLockTimeout": 300}`},
|
||||
{name: "admin policy without lock settings keeps packaged", packaged: `{"lock_settings": {"enableFprint": false}}`, admin: `{"disable_registries": true}`, want: `{"enableFprint": false}`},
|
||||
{name: "null leaves a key unset", packaged: `{"lock_settings": {"u2fMode": "and"}}`, admin: `{"lock_settings": {"u2fMode": null, "lockAtStartup": null}}`, want: `{"u2fMode": "and"}`},
|
||||
{
|
||||
name: "only lock settings are managed",
|
||||
admin: `{"lock_settings": {"customPowerActionLock": "", "lockBeforeSuspend": true, "loginctlLockIntegration": true, "lockAtStartup": true, "acLockTimeout": 300, "batteryLockTimeout": 120, "fadeToLockEnabled": true, "fadeToLockGracePeriod": 5, "enableFprint": true, "maxFprintTries": 3, "enableU2f": true, "u2fMode": "and", "lockScreenShowPowerActions": false, "lockPamPath": "/tmp/pam", "unknownSetting": 1}}`,
|
||||
want: `{"customPowerActionLock": "", "lockBeforeSuspend": true, "loginctlLockIntegration": true, "lockAtStartup": true, "acLockTimeout": 300, "batteryLockTimeout": 120, "fadeToLockEnabled": true, "fadeToLockGracePeriod": 5, "enableFprint": true, "maxFprintTries": 3, "enableU2f": true, "u2fMode": "and"}`,
|
||||
},
|
||||
{name: "values are passed as written", admin: `{"lock_settings": {"lockBeforeSuspend": "yes", "acLockTimeout": [1]}}`, want: `{"lockBeforeSuspend": "yes", "acLockTimeout": [1]}`},
|
||||
{name: "malformed admin policy manages nothing", packaged: `{"lock_settings": {"lockBeforeSuspend": true}}`, admin: "{not json", want: `{}`, wantErr: "failed to parse " + AdminPath},
|
||||
{name: "lock settings not an object", packaged: `{"lock_settings": {"lockBeforeSuspend": true}}`, admin: `{"lock_settings": ["lockBeforeSuspend"]}`, want: `{}`, wantErr: "failed to parse " + AdminPath},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fs := afero.NewMemMapFs()
|
||||
if tt.packaged != "" {
|
||||
require.NoError(t, afero.WriteFile(fs, PackagedPath, []byte(tt.packaged), 0o644))
|
||||
}
|
||||
if tt.admin != "" {
|
||||
require.NoError(t, afero.WriteFile(fs, AdminPath, []byte(tt.admin), 0o644))
|
||||
}
|
||||
|
||||
settings, err := LockSettings(fs)
|
||||
if tt.wantErr != "" {
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
got, err := json.Marshal(settings)
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, tt.want, string(got))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellEnvLockSettings(t *testing.T) {
|
||||
fs := afero.NewMemMapFs()
|
||||
require.NoError(t, afero.WriteFile(fs, PackagedPath, []byte(`{"lock_settings": {"lockBeforeSuspend": false, "acLockTimeout": 300}}`), 0o644))
|
||||
require.NoError(t, afero.WriteFile(fs, AdminPath, []byte(`{"lock_settings": {"lockBeforeSuspend": true, "customPowerActionLock": "", "lockScreenShowWeather": false}}`), 0o644))
|
||||
|
||||
env, err := ShellEnv(fs)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, env, `DMS_POLICY_LOCK_SETTINGS={"acLockTimeout":300,"customPowerActionLock":"","lockBeforeSuspend":true}`)
|
||||
}
|
||||
|
||||
func TestLockSettingBool(t *testing.T) {
|
||||
on, off := true, false
|
||||
tests := []struct {
|
||||
name string
|
||||
settings string
|
||||
want *bool
|
||||
}{
|
||||
{name: "not managed", settings: `{"lockAtStartup": true}`},
|
||||
{name: "managed on", settings: `{"lockBeforeSuspend": true}`, want: &on},
|
||||
{name: "managed off", settings: `{"lockBeforeSuspend": false}`, want: &off},
|
||||
{name: "null", settings: `{"lockBeforeSuspend": null}`},
|
||||
{name: "not a boolean", settings: `{"lockBeforeSuspend": "true"}`},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var settings map[string]json.RawMessage
|
||||
require.NoError(t, json.Unmarshal([]byte(tt.settings), &settings))
|
||||
assert.Equal(t, tt.want, LockSettingBool(settings, "lockBeforeSuspend"))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,15 @@
|
||||
package loginctl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
errLockBeforeSuspendManaged = errors.New("lock before suspend is managed by administrator policy")
|
||||
errSleepInhibitorManaged = errors.New("the sleep inhibitor is managed by administrator policy")
|
||||
)
|
||||
|
||||
func (m *Manager) Lock() error {
|
||||
if m.sessionObj == nil {
|
||||
return fmt.Errorf("session object not available")
|
||||
@@ -85,8 +91,21 @@ func (m *Manager) Terminate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) SetLockBeforeSuspend(enabled bool) {
|
||||
// applyLockPolicy starts from the managed settings, before the shell connects.
|
||||
func (m *Manager) applyLockPolicy() {
|
||||
if managed := m.lockPolicy.LockBeforeSuspend; managed != nil {
|
||||
m.lockBeforeSuspend.Store(*managed)
|
||||
}
|
||||
enabled, managed := m.lockPolicy.sleepInhibitor()
|
||||
m.sleepInhibitorEnabled.Store(enabled || !managed)
|
||||
}
|
||||
|
||||
func (m *Manager) SetLockBeforeSuspend(enabled bool) error {
|
||||
if managed := m.lockPolicy.LockBeforeSuspend; managed != nil && *managed != enabled {
|
||||
return errLockBeforeSuspendManaged
|
||||
}
|
||||
m.lockBeforeSuspend.Store(enabled)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) markLockerReady() {
|
||||
@@ -107,7 +126,10 @@ func (m *Manager) markLockerReady() {
|
||||
m.prelockedReady.Store(true)
|
||||
}
|
||||
|
||||
func (m *Manager) SetSleepInhibitorEnabled(enabled bool) {
|
||||
func (m *Manager) SetSleepInhibitorEnabled(enabled bool) error {
|
||||
if managedValue, managed := m.lockPolicy.sleepInhibitor(); managed && managedValue != enabled {
|
||||
return errSleepInhibitorManaged
|
||||
}
|
||||
m.sleepInhibitorEnabled.Store(enabled)
|
||||
if enabled {
|
||||
// Re-acquire inhibitor if enabled
|
||||
@@ -116,4 +138,5 @@ func (m *Manager) SetSleepInhibitorEnabled(enabled bool) {
|
||||
// Release inhibitor if disabled
|
||||
m.releaseSleepInhibitor()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -100,7 +100,10 @@ func handleSetLockBeforeSuspend(conn *ipc.ConnWriter, req ipc.Request, manager *
|
||||
return
|
||||
}
|
||||
|
||||
manager.SetLockBeforeSuspend(enabled)
|
||||
if err := manager.SetLockBeforeSuspend(enabled); err != nil {
|
||||
conn.RespondError(req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
conn.Respond(req.ID, models.SuccessResult{Success: true, Message: "lock before suspend set"})
|
||||
}
|
||||
|
||||
@@ -111,7 +114,10 @@ func handleSetSleepInhibitorEnabled(conn *ipc.ConnWriter, req ipc.Request, manag
|
||||
return
|
||||
}
|
||||
|
||||
manager.SetSleepInhibitorEnabled(enabled)
|
||||
if err := manager.SetSleepInhibitorEnabled(enabled); err != nil {
|
||||
conn.RespondError(req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
conn.Respond(req.ID, models.SuccessResult{Success: true, Message: "sleep inhibitor setting updated"})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -424,3 +425,85 @@ func TestHandleRequest(t *testing.T) {
|
||||
assert.Empty(t, resp.Error)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleSetLockBeforeSuspend(t *testing.T) {
|
||||
on, off := true, false
|
||||
tests := []struct {
|
||||
name string
|
||||
managed *bool
|
||||
request bool
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "unmanaged", request: true, want: true},
|
||||
{name: "managed, same value", managed: &on, request: true, want: true},
|
||||
{name: "managed on, request off", managed: &on, request: false, want: true, wantErr: true},
|
||||
{name: "managed off, request on", managed: &off, request: true, want: false, wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
manager := &Manager{lockPolicy: LockPolicy{LockBeforeSuspend: tt.managed}}
|
||||
manager.applyLockPolicy()
|
||||
|
||||
mc := newMockNetConn()
|
||||
conn := ipc.NewConnWriter(mc)
|
||||
HandleRequest(conn, ipc.Request{ID: 7, Method: "loginctl.setLockBeforeSuspend", Params: map[string]any{"enabled": tt.request}}, manager)
|
||||
|
||||
var resp ipc.Response[models.SuccessResult]
|
||||
require.NoError(t, json.NewDecoder(mc.writeBuf).Decode(&resp))
|
||||
if tt.wantErr {
|
||||
assert.Equal(t, "lock before suspend is managed by administrator policy", resp.Error)
|
||||
} else {
|
||||
assert.Empty(t, resp.Error)
|
||||
}
|
||||
assert.Equal(t, tt.want, manager.lockBeforeSuspend.Load())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleSetSleepInhibitorEnabled(t *testing.T) {
|
||||
on, off := true, false
|
||||
tests := []struct {
|
||||
name string
|
||||
policy LockPolicy
|
||||
request bool
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "unmanaged", request: false, want: false},
|
||||
{name: "both managed on, request off", policy: LockPolicy{LockBeforeSuspend: &on, LoginctlLockIntegration: &on}, request: false, want: true, wantErr: true},
|
||||
{name: "both managed on, request on", policy: LockPolicy{LockBeforeSuspend: &on, LoginctlLockIntegration: &on}, request: true, want: true},
|
||||
{name: "lock before suspend managed off", policy: LockPolicy{LockBeforeSuspend: &off}, request: true, want: false, wantErr: true},
|
||||
{name: "integration managed off", policy: LockPolicy{LoginctlLockIntegration: &off}, request: true, want: false, wantErr: true},
|
||||
{name: "lock before suspend managed on, integration left to the user", policy: LockPolicy{LockBeforeSuspend: &on}, request: false, want: false},
|
||||
{name: "integration managed on, lock before suspend left to the user", policy: LockPolicy{LoginctlLockIntegration: &on}, request: false, want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
manager := &Manager{lockPolicy: tt.policy}
|
||||
manager.applyLockPolicy()
|
||||
inhibit, err := os.CreateTemp(t.TempDir(), "inhibit")
|
||||
require.NoError(t, err)
|
||||
held := manager.sleepInhibitorEnabled.Load()
|
||||
if held {
|
||||
manager.inhibitFile = inhibit
|
||||
}
|
||||
|
||||
mc := newMockNetConn()
|
||||
conn := ipc.NewConnWriter(mc)
|
||||
HandleRequest(conn, ipc.Request{ID: 7, Method: "loginctl.setSleepInhibitorEnabled", Params: map[string]any{"enabled": tt.request}}, manager)
|
||||
|
||||
var resp ipc.Response[models.SuccessResult]
|
||||
require.NoError(t, json.NewDecoder(mc.writeBuf).Decode(&resp))
|
||||
if tt.wantErr {
|
||||
assert.Equal(t, "the sleep inhibitor is managed by administrator policy", resp.Error)
|
||||
assert.Equal(t, held, manager.inhibitFile != nil, "a refused request leaves the inhibitor as it was")
|
||||
} else {
|
||||
assert.Empty(t, resp.Error)
|
||||
}
|
||||
assert.Equal(t, tt.want, manager.sleepInhibitorEnabled.Load())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
func NewManager(keepLockOnUnlock bool) (*Manager, error) {
|
||||
func NewManager(keepLockOnUnlock bool, lockPolicy LockPolicy) (*Manager, error) {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to system bus: %w", err)
|
||||
@@ -27,8 +27,9 @@ func NewManager(keepLockOnUnlock bool) (*Manager, error) {
|
||||
signals: make(chan *dbus.Signal, 256),
|
||||
|
||||
keepLockOnUnlock: keepLockOnUnlock,
|
||||
lockPolicy: lockPolicy,
|
||||
}
|
||||
m.sleepInhibitorEnabled.Store(true)
|
||||
m.applyLockPolicy()
|
||||
|
||||
if err := m.initialize(); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -126,3 +126,28 @@ func TestStateChangedMeaningfully(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_ApplyLockPolicy(t *testing.T) {
|
||||
on, off := true, false
|
||||
tests := []struct {
|
||||
name string
|
||||
policy LockPolicy
|
||||
lockBeforeSuspend bool
|
||||
sleepInhibitorEnabled bool
|
||||
}{
|
||||
{name: "unmanaged", sleepInhibitorEnabled: true},
|
||||
{name: "lock before suspend managed on", policy: LockPolicy{LockBeforeSuspend: &on}, lockBeforeSuspend: true, sleepInhibitorEnabled: true},
|
||||
{name: "both managed on", policy: LockPolicy{LockBeforeSuspend: &on, LoginctlLockIntegration: &on}, lockBeforeSuspend: true, sleepInhibitorEnabled: true},
|
||||
{name: "lock before suspend managed off", policy: LockPolicy{LockBeforeSuspend: &off}},
|
||||
{name: "integration managed off", policy: LockPolicy{LockBeforeSuspend: &on, LoginctlLockIntegration: &off}, lockBeforeSuspend: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
manager := &Manager{lockPolicy: tt.policy}
|
||||
manager.applyLockPolicy()
|
||||
assert.Equal(t, tt.lockBeforeSuspend, manager.lockBeforeSuspend.Load())
|
||||
assert.Equal(t, tt.sleepInhibitorEnabled, manager.sleepInhibitorEnabled.Load())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -70,4 +70,25 @@ type Manager struct {
|
||||
sleepInhibitorEnabled atomic.Bool
|
||||
fallbackDelay time.Duration
|
||||
keepLockOnUnlock bool
|
||||
lockPolicy LockPolicy
|
||||
}
|
||||
|
||||
// LockPolicy holds the lock settings an administrator policy manages; nil
|
||||
// leaves a setting to the shell.
|
||||
type LockPolicy struct {
|
||||
LockBeforeSuspend *bool
|
||||
LoginctlLockIntegration *bool
|
||||
}
|
||||
|
||||
// sleepInhibitor returns the inhibitor state the shell asks for,
|
||||
// loginctlLockIntegration && lockBeforeSuspend, when the managed settings
|
||||
// decide it on their own.
|
||||
func (p LockPolicy) sleepInhibitor() (enabled, managed bool) {
|
||||
switch {
|
||||
case p.LockBeforeSuspend != nil && !*p.LockBeforeSuspend, p.LoginctlLockIntegration != nil && !*p.LoginctlLockIntegration:
|
||||
return false, true
|
||||
case p.LockBeforeSuspend != nil && p.LoginctlLockIntegration != nil:
|
||||
return true, true
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
@@ -115,7 +115,14 @@ func InitializeLoginctlManager() error {
|
||||
log.Errorf("CLI policy unreadable, keeping locks through logind Unlock: %v", err)
|
||||
keepLockOnUnlock = true
|
||||
}
|
||||
manager, err := loginctl.NewManager(keepLockOnUnlock)
|
||||
lockSettings, err := clipolicy.LockSettings(afero.NewOsFs())
|
||||
if err != nil {
|
||||
log.Errorf("CLI policy unreadable, ignoring its lock settings: %v", err)
|
||||
}
|
||||
manager, err := loginctl.NewManager(keepLockOnUnlock, loginctl.LockPolicy{
|
||||
LockBeforeSuspend: clipolicy.LockSettingBool(lockSettings, "lockBeforeSuspend"),
|
||||
LoginctlLockIntegration: clipolicy.LockSettingBool(lockSettings, "loginctlLockIntegration"),
|
||||
})
|
||||
if err != nil {
|
||||
log.Warnf("Failed to initialize loginctl manager: %v", err)
|
||||
return err
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import qs.Services
|
||||
import "settings/ManagedSettings.js" as ManagedSettings
|
||||
|
||||
// cli-policy.json switches, set by `dms run` on each launch (core/internal/clipolicy).
|
||||
// cli-policy.json switches and lock settings, set by `dms run` on each launch (core/internal/clipolicy).
|
||||
Singleton {
|
||||
readonly property bool disableLockIpcUnlock: Quickshell.env("DMS_POLICY_DISABLE_LOCK_IPC_UNLOCK") === "1"
|
||||
readonly property bool disableLogindUnlock: Quickshell.env("DMS_POLICY_DISABLE_LOGIND_UNLOCK") === "1"
|
||||
readonly property bool disableCustomLockPam: Quickshell.env("DMS_POLICY_DISABLE_CUSTOM_LOCK_PAM") === "1"
|
||||
readonly property bool disableUserPlugins: Quickshell.env("DMS_POLICY_DISABLE_USER_PLUGINS") === "1"
|
||||
readonly property bool disableSettingsIpcSet: Quickshell.env("DMS_POLICY_DISABLE_SETTINGS_IPC_SET") === "1"
|
||||
readonly property var lockSettings: ManagedSettings.parse(Quickshell.env("DMS_POLICY_LOCK_SETTINGS"), Log.scoped("AdminPolicy").warn)
|
||||
}
|
||||
@@ -762,32 +762,9 @@ Singleton {
|
||||
|
||||
function migrateFromUndefinedToV1(settings) {
|
||||
if (typeof SettingsData !== "undefined") {
|
||||
if (settings.acMonitorTimeout !== undefined) {
|
||||
SettingsData.set("acMonitorTimeout", settings.acMonitorTimeout);
|
||||
}
|
||||
if (settings.acLockTimeout !== undefined) {
|
||||
SettingsData.set("acLockTimeout", settings.acLockTimeout);
|
||||
}
|
||||
if (settings.acSuspendTimeout !== undefined) {
|
||||
SettingsData.set("acSuspendTimeout", settings.acSuspendTimeout);
|
||||
}
|
||||
if (settings.batteryMonitorTimeout !== undefined) {
|
||||
SettingsData.set("batteryMonitorTimeout", settings.batteryMonitorTimeout);
|
||||
}
|
||||
if (settings.batteryLockTimeout !== undefined) {
|
||||
SettingsData.set("batteryLockTimeout", settings.batteryLockTimeout);
|
||||
}
|
||||
if (settings.batterySuspendTimeout !== undefined) {
|
||||
SettingsData.set("batterySuspendTimeout", settings.batterySuspendTimeout);
|
||||
}
|
||||
if (settings.lockBeforeSuspend !== undefined) {
|
||||
SettingsData.set("lockBeforeSuspend", settings.lockBeforeSuspend);
|
||||
}
|
||||
if (settings.loginctlLockIntegration !== undefined) {
|
||||
SettingsData.set("loginctlLockIntegration", settings.loginctlLockIntegration);
|
||||
}
|
||||
if (settings.launchPrefix !== undefined) {
|
||||
SettingsData.set("launchPrefix", settings.launchPrefix);
|
||||
for (const key of ["acMonitorTimeout", "acLockTimeout", "acSuspendTimeout", "batteryMonitorTimeout", "batteryLockTimeout", "batterySuspendTimeout", "lockBeforeSuspend", "loginctlLockIntegration", "launchPrefix"]) {
|
||||
if (settings[key] !== undefined && !SettingsData.isManaged(key))
|
||||
SettingsData.set(key, settings[key]);
|
||||
}
|
||||
}
|
||||
if (typeof CacheData !== "undefined") {
|
||||
|
||||
@@ -1739,6 +1739,10 @@ Singleton {
|
||||
})
|
||||
|
||||
function set(key, value) {
|
||||
if (isManaged(key)) {
|
||||
log.warn("Refusing to change a setting managed by administrator policy:", key);
|
||||
return;
|
||||
}
|
||||
if (key === "cornerRadius") {
|
||||
setCornerRadius(value);
|
||||
return;
|
||||
@@ -1754,6 +1758,10 @@ Singleton {
|
||||
return key in Spec.SPEC;
|
||||
}
|
||||
|
||||
function isManaged(key) {
|
||||
return key in AdminPolicy.lockSettings;
|
||||
}
|
||||
|
||||
function specDefault(key) {
|
||||
return SpecUtil.cloneDef(Spec.SPEC[key]?.def);
|
||||
}
|
||||
@@ -1764,7 +1772,7 @@ Singleton {
|
||||
|
||||
function resetToDefault(keys) {
|
||||
for (const key of keys) {
|
||||
if (key in Spec.SPEC)
|
||||
if (key in Spec.SPEC && !isManaged(key))
|
||||
Spec.set(root, key, null, saveSettings, _hooks);
|
||||
}
|
||||
}
|
||||
@@ -1876,7 +1884,7 @@ Singleton {
|
||||
const prevFrameEnabled = frameEnabled;
|
||||
const prevFrameMode = frameMode;
|
||||
unsavedChanges = loadedSettings !== JSON.stringify(obj);
|
||||
Store.parse(root, obj);
|
||||
Store.parse(root, Object.assign(obj, AdminPolicy.lockSettings));
|
||||
|
||||
// set() enforces this pair, but a hand-edited settings bypass set() entirely.
|
||||
if (frameEnabled)
|
||||
@@ -2031,6 +2039,8 @@ Singleton {
|
||||
const file = _settingsFiles.get(path);
|
||||
const fileSettings = file.getSettings();
|
||||
for (const setting in fileSettings) {
|
||||
if (isManaged(setting))
|
||||
continue;
|
||||
if (!(setting in settings)) {
|
||||
delete fileSettings[setting];
|
||||
continue;
|
||||
@@ -2045,7 +2055,7 @@ Singleton {
|
||||
}
|
||||
|
||||
for (const setting in settings) {
|
||||
if (!savedSettings.has(setting)) {
|
||||
if (!savedSettings.has(setting) && !isManaged(setting)) {
|
||||
splitSettings[defaultSettingsFile.filePath][setting] = settings[setting];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
.pragma library
|
||||
.import "SettingsSpec.js" as SpecModule
|
||||
|
||||
function parse(text, warn) {
|
||||
const settings = {};
|
||||
if (!text)
|
||||
return settings;
|
||||
let policy;
|
||||
try {
|
||||
policy = JSON.parse(text);
|
||||
} catch (e) {
|
||||
warn("Ignoring unreadable managed settings:", e.message);
|
||||
return settings;
|
||||
}
|
||||
for (const key in policy) {
|
||||
const spec = SpecModule.SPEC[key];
|
||||
if (!spec || spec.persist === false) {
|
||||
warn("Ignoring managed setting that does not exist:", key);
|
||||
continue;
|
||||
}
|
||||
const value = policy[key];
|
||||
if (typeof value !== typeof spec.def || Array.isArray(value) !== Array.isArray(spec.def)) {
|
||||
warn("Ignoring managed setting of the wrong type:", key, JSON.stringify(value));
|
||||
continue;
|
||||
}
|
||||
settings[key] = value;
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
@@ -1104,6 +1104,8 @@ Item {
|
||||
function set(key: string, value: string): string {
|
||||
if (AdminPolicy.disableSettingsIpcSet)
|
||||
return "SETTINGS_SET_DISABLED";
|
||||
if (SettingsData.isManaged(key))
|
||||
return "SETTINGS_SET_MANAGED";
|
||||
if (!(key in SettingsData)) {
|
||||
log.warn("Cannot set property, not found:", key);
|
||||
return "SETTINGS_INVALID_KEY";
|
||||
|
||||
@@ -34,6 +34,7 @@ Item {
|
||||
property string u2fPendingApplyPath: ""
|
||||
property bool u2fShowCustom: false
|
||||
|
||||
readonly property string managedNote: I18n.tr("Managed by administrator policy", "settings note: the administrator lock policy sets this value")
|
||||
readonly property string authAutoLabel: I18n.tr("Auto", "automatic PAM authentication source option")
|
||||
readonly property string authCustomLabel: I18n.tr("Custom", "custom PAM authentication source option") + "…"
|
||||
|
||||
@@ -416,6 +417,8 @@ Item {
|
||||
settingKey: "lockAtStartup"
|
||||
tags: ["lock", "screen", "startup", "start", "boot", "login", "automatic"]
|
||||
text: I18n.tr("Lock at startup")
|
||||
description: SettingsData.isManaged("lockAtStartup") ? root.managedNote : ""
|
||||
enabled: !SettingsData.isManaged("lockAtStartup")
|
||||
checked: SettingsData.lockAtStartup
|
||||
onToggled: checked => SettingsData.set("lockAtStartup", checked)
|
||||
}
|
||||
@@ -424,6 +427,8 @@ Item {
|
||||
settingKey: "lockBeforeSuspend"
|
||||
tags: ["lock", "screen", "suspend", "hibernate", "sleep", "automatic"]
|
||||
text: I18n.tr("Lock before suspend")
|
||||
description: SettingsData.isManaged("lockBeforeSuspend") ? root.managedNote : ""
|
||||
enabled: !SettingsData.isManaged("lockBeforeSuspend")
|
||||
checked: SettingsData.lockBeforeSuspend
|
||||
visible: SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration
|
||||
onToggled: checked => SettingsData.set("lockBeforeSuspend", checked)
|
||||
@@ -499,10 +504,10 @@ Item {
|
||||
settingKey: "enableFprint"
|
||||
tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"]
|
||||
text: I18n.tr("Fingerprint", "lock screen fingerprint authentication toggle")
|
||||
description: root.lockFprintControlledByPrimary ? I18n.tr("Managed by the primary PAM source", "factor managed by PAM source status") : root.lockFingerprintDescription()
|
||||
descriptionColor: root.lockFprintControlledByPrimary || SettingsData.lockFingerprintReason === "ready" ? Theme.surfaceVariantText : Theme.warning
|
||||
description: SettingsData.isManaged("enableFprint") ? root.managedNote : root.lockFprintControlledByPrimary ? I18n.tr("Managed by the primary PAM source", "factor managed by PAM source status") : root.lockFingerprintDescription()
|
||||
descriptionColor: SettingsData.isManaged("enableFprint") || root.lockFprintControlledByPrimary || SettingsData.lockFingerprintReason === "ready" ? Theme.surfaceVariantText : Theme.warning
|
||||
checked: SettingsData.enableFprint || root.primaryPamHasFprint
|
||||
enabled: root.lockFprintToggleAvailable && !root.lockFprintControlledByPrimary
|
||||
enabled: root.lockFprintToggleAvailable && !root.lockFprintControlledByPrimary && !SettingsData.isManaged("enableFprint")
|
||||
onToggled: checked => SettingsData.set("enableFprint", checked)
|
||||
}
|
||||
|
||||
@@ -510,10 +515,10 @@ Item {
|
||||
settingKey: "enableU2f"
|
||||
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "fido", "authentication", "hardware"]
|
||||
text: I18n.tr("Security key")
|
||||
description: root.lockU2fControlledByPrimary ? I18n.tr("Managed by the primary PAM source", "factor managed by PAM source status") : root.lockU2fDescription()
|
||||
descriptionColor: root.lockU2fControlledByPrimary || SettingsData.lockU2fReason === "ready" ? Theme.surfaceVariantText : Theme.warning
|
||||
description: SettingsData.isManaged("enableU2f") ? root.managedNote : root.lockU2fControlledByPrimary ? I18n.tr("Managed by the primary PAM source", "factor managed by PAM source status") : root.lockU2fDescription()
|
||||
descriptionColor: SettingsData.isManaged("enableU2f") || root.lockU2fControlledByPrimary || SettingsData.lockU2fReason === "ready" ? Theme.surfaceVariantText : Theme.warning
|
||||
checked: SettingsData.enableU2f || root.primaryPamHasU2f
|
||||
enabled: root.lockU2fToggleAvailable && !root.lockU2fControlledByPrimary
|
||||
enabled: root.lockU2fToggleAvailable && !root.lockU2fControlledByPrimary && !SettingsData.isManaged("enableU2f")
|
||||
onToggled: checked => SettingsData.set("enableU2f", checked)
|
||||
}
|
||||
|
||||
@@ -521,7 +526,8 @@ Item {
|
||||
settingKey: "u2fMode"
|
||||
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "mode", "factor", "second"]
|
||||
text: I18n.tr("Security key mode", "lock screen U2F security key mode setting")
|
||||
description: I18n.tr("'Alternative' lets the key unlock on its own. 'Second factor' requires password or fingerprint first, then the key.", "lock screen U2F security key mode setting")
|
||||
description: SettingsData.isManaged("u2fMode") ? root.managedNote : I18n.tr("'Alternative' lets the key unlock on its own. 'Second factor' requires password or fingerprint first, then the key.", "lock screen U2F security key mode setting")
|
||||
enabled: !SettingsData.isManaged("u2fMode")
|
||||
visible: SettingsData.enableU2f && !root.lockU2fControlledByPrimary
|
||||
options: [I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method"), I18n.tr("Second factor (AND)", "U2F mode option: key required after password or fingerprint")]
|
||||
currentValue: SettingsData.u2fMode === "and" ? I18n.tr("Second factor (AND)", "U2F mode option: key required after password or fingerprint") : I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method")
|
||||
@@ -852,9 +858,9 @@ Item {
|
||||
settingKey: "loginctlLockIntegration"
|
||||
tags: ["lock", "screen", "loginctl", "dbus", "integration", "external"]
|
||||
text: I18n.tr("loginctl integration")
|
||||
description: I18n.tr("Disable when using an external lock screen")
|
||||
description: SettingsData.isManaged("loginctlLockIntegration") ? root.managedNote : I18n.tr("Disable when using an external lock screen")
|
||||
checked: SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration
|
||||
enabled: SessionService.loginctlAvailable
|
||||
enabled: SessionService.loginctlAvailable && !SettingsData.isManaged("loginctlLockIntegration")
|
||||
onToggled: checked => {
|
||||
if (!SessionService.loginctlAvailable)
|
||||
return;
|
||||
|
||||
@@ -15,6 +15,7 @@ Item {
|
||||
readonly property var timeoutOptions: timeoutValues.map(seconds => seconds === 0 ? I18n.tr("Never", "timeout option meaning the action never happens") : I18n.duration(seconds))
|
||||
readonly property var gracePeriodValues: [1, 2, 3, 4, 5, 10, 15, 20, 30]
|
||||
readonly property var gracePeriodOptions: gracePeriodValues.map(seconds => I18n.duration(seconds))
|
||||
readonly property string managedNote: I18n.tr("Managed by administrator policy", "settings note: the administrator lock policy sets this value")
|
||||
|
||||
function sourceKey(suffix) {
|
||||
return (onAc ? "ac" : "battery") + suffix;
|
||||
@@ -70,6 +71,8 @@ Item {
|
||||
settingKey: "lockTimeout"
|
||||
tags: ["lock", "timeout", "idle", "automatic", "security"]
|
||||
text: I18n.tr("Automatically lock after")
|
||||
description: SettingsData.isManaged(root.sourceKey("LockTimeout")) ? root.managedNote : ""
|
||||
enabled: !SettingsData.isManaged(root.sourceKey("LockTimeout"))
|
||||
resetKeys: [root.sourceKey("LockTimeout")]
|
||||
options: root.timeoutOptions
|
||||
currentValue: root.timeoutText(root.sourceKey("LockTimeout"))
|
||||
@@ -126,6 +129,8 @@ Item {
|
||||
settingKey: "lockBeforeSuspend"
|
||||
tags: ["lock", "suspend", "hibernate", "sleep", "security"]
|
||||
text: I18n.tr("Lock before suspend")
|
||||
description: SettingsData.isManaged("lockBeforeSuspend") ? root.managedNote : ""
|
||||
enabled: !SettingsData.isManaged("lockBeforeSuspend")
|
||||
checked: SettingsData.lockBeforeSuspend
|
||||
visible: SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration
|
||||
onToggled: checked => SettingsData.set("lockBeforeSuspend", checked)
|
||||
@@ -143,7 +148,8 @@ Item {
|
||||
settingKey: "fadeToLockEnabled"
|
||||
tags: ["fade", "lock", "screen", "idle", "grace period"]
|
||||
text: I18n.tr("Fade to lock screen")
|
||||
description: I18n.tr("Fades the screen out first. Input during the fade cancels it", "idle fade toggle description")
|
||||
description: SettingsData.isManaged("fadeToLockEnabled") ? root.managedNote : I18n.tr("Fades the screen out first. Input during the fade cancels it", "idle fade toggle description")
|
||||
enabled: !SettingsData.isManaged("fadeToLockEnabled")
|
||||
checked: SettingsData.fadeToLockEnabled
|
||||
onToggled: checked => SettingsData.set("fadeToLockEnabled", checked)
|
||||
}
|
||||
@@ -152,6 +158,8 @@ Item {
|
||||
settingKey: "fadeToLockGracePeriod"
|
||||
tags: ["fade", "grace", "period", "timeout", "lock"]
|
||||
text: I18n.tr("Lock fade grace period")
|
||||
description: SettingsData.isManaged("fadeToLockGracePeriod") ? root.managedNote : ""
|
||||
enabled: !SettingsData.isManaged("fadeToLockGracePeriod")
|
||||
visible: SettingsData.fadeToLockEnabled
|
||||
options: root.gracePeriodOptions
|
||||
currentValue: root.gracePeriodText("fadeToLockGracePeriod")
|
||||
@@ -453,6 +461,8 @@ Item {
|
||||
tags: ["power", "command", "override", modelData.label]
|
||||
text: modelData.label
|
||||
leftIconName: modelData.icon
|
||||
description: SettingsData.isManaged(modelData.key) ? root.managedNote : ""
|
||||
enabled: !SettingsData.isManaged(modelData.key)
|
||||
value: SettingsData[modelData.key] || ""
|
||||
placeholderText: modelData.placeholder
|
||||
onValueEdited: value => SettingsData.set(modelData.key, value.trim())
|
||||
|
||||
@@ -58,12 +58,16 @@ test("the user plugin switch keeps the user tree out of a scan and a resync", ()
|
||||
}
|
||||
});
|
||||
|
||||
test("the settings IPC switch refuses set and keeps get", () => {
|
||||
function settingsIpcHandlers() {
|
||||
const source = readFileSync(new URL("../DMSShellIPC.qml", import.meta.url), "utf8");
|
||||
const block = source.slice(source.indexOf(" function get(key: string)"), source.indexOf(' target: "settings"'));
|
||||
const handlers = [...block.matchAll(/^ {8}function \w+\([^)]*\)(?:: \w+)? \{\n[\s\S]*?^ {8}\}$/gm)].map(match => match[0].replace(/\((.*?)\)(?:: \w+)? \{/, (_, params) => "(" + params.replace(/: \w+/g, "") + ") {"));
|
||||
return [...block.matchAll(/^ {8}function \w+\([^)]*\)(?:: \w+)? \{\n[\s\S]*?^ {8}\}$/gm)].map(match => match[0].replace(/\((.*?)\)(?:: \w+)? \{/, (_, params) => "(" + params.replace(/: \w+/g, "") + ") {"));
|
||||
}
|
||||
|
||||
test("the settings IPC switch refuses set and keeps get", () => {
|
||||
const handlers = settingsIpcHandlers();
|
||||
for (const disableSettingsIpcSet of [false, true]) {
|
||||
const SettingsData = { lockBeforeSuspend: false, saveSettings() {} };
|
||||
const SettingsData = { lockBeforeSuspend: false, saveSettings() {}, isManaged: () => false };
|
||||
const context = vm.createContext({ AdminPolicy: { disableSettingsIpcSet }, SettingsData, log: { warn() {} } });
|
||||
for (const handler of handlers)
|
||||
vm.runInContext(handler, context);
|
||||
@@ -72,3 +76,122 @@ test("the settings IPC switch refuses set and keeps get", () => {
|
||||
assert.equal(context.get("lockBeforeSuspend"), String(!disableSettingsIpcSet));
|
||||
}
|
||||
});
|
||||
|
||||
const ManagedSettings = loadScript(new URL("../Common/settings/ManagedSettings.js", import.meta.url));
|
||||
const SettingsStore = loadScript(new URL("../Common/settings/SettingsStore.js", import.meta.url));
|
||||
const SPEC = loadScript(new URL("../Common/settings/SettingsSpec.js", import.meta.url)).SPEC;
|
||||
|
||||
function settingsDataContext(globals, names) {
|
||||
const context = vm.createContext(globals);
|
||||
context.root = context;
|
||||
for (const name of names)
|
||||
vm.runInContext(functionSource("../Common/SettingsData.qml", name), context);
|
||||
return context;
|
||||
}
|
||||
|
||||
test("managed lock settings keep only values of the default's type", () => {
|
||||
const warnings = [];
|
||||
const settings = ManagedSettings.parse(JSON.stringify({
|
||||
lockBeforeSuspend: true,
|
||||
customPowerActionLock: "",
|
||||
acLockTimeout: 300,
|
||||
u2fMode: null,
|
||||
enableFprint: "yes",
|
||||
maxFprintTries: [3],
|
||||
notASetting: true
|
||||
}), (...args) => warnings.push(args[1]));
|
||||
assert.deepEqual({ ...settings }, { lockBeforeSuspend: true, customPowerActionLock: "", acLockTimeout: 300 });
|
||||
assert.deepEqual(warnings.sort(), ["enableFprint", "maxFprintTries", "notASetting", "u2fMode"]);
|
||||
assert.deepEqual({ ...ManagedSettings.parse("", () => assert.fail("no policy, no warning")) }, {});
|
||||
});
|
||||
|
||||
test("managed settings win over every settings file, on the first load and on each reload", () => {
|
||||
const version = Number(readFileSync(new URL("../Common/SettingsData.qml", import.meta.url), "utf8").match(/settingsConfigVersion: (\d+)/)[1]);
|
||||
let files = { configVersion: version, lockBeforeSuspend: false, customPowerActionLock: "true", acLockTimeout: 60 };
|
||||
const context = settingsDataContext({
|
||||
AdminPolicy: { lockSettings: { lockBeforeSuspend: true, customPowerActionLock: "" } },
|
||||
SettingsData: { Stage: { Partial: 2, Ready: 3 } },
|
||||
Store: SettingsStore,
|
||||
settingsConfigVersion: version,
|
||||
_hasLoaded: false,
|
||||
_settingsStage: 3,
|
||||
_parseError: false,
|
||||
frameEnabled: false,
|
||||
frameMode: "",
|
||||
_getSettingsObjectFromFiles: () => JSON.parse(JSON.stringify(files)),
|
||||
_mergeSessionState() {},
|
||||
Qt: { callLater() {} },
|
||||
checkIconThemeDrift() {},
|
||||
applyStoredTheme() {},
|
||||
updateCompositorCursor() {},
|
||||
updateFrameCompositorLayout() {},
|
||||
loadPluginSettings() {},
|
||||
clearIslandBars() {}
|
||||
}, ["_loadSettings"]);
|
||||
for (const userValue of [false, true]) {
|
||||
files = Object.assign({}, files, { lockBeforeSuspend: userValue, acLockTimeout: files.acLockTimeout * 2 });
|
||||
assert.equal(context._loadSettings(), false, "managed values are no unsaved change");
|
||||
assert.equal(context.lockBeforeSuspend, true);
|
||||
assert.equal(context.customPowerActionLock, "");
|
||||
assert.equal(context.acLockTimeout, files.acLockTimeout);
|
||||
}
|
||||
});
|
||||
|
||||
test("a save leaves managed keys out of the settings files, and a user's own value in place", () => {
|
||||
const main = "/home/u/.config/dms/settings.json";
|
||||
const overlay = "/home/u/.config/dms/config.d/lock.json";
|
||||
const fileSettings = { [main]: { lockBeforeSuspend: false, acLockTimeout: 60 }, [overlay]: { customPowerActionLock: "true", fadeToLockEnabled: false } };
|
||||
const context = settingsDataContext({
|
||||
AdminPolicy: { lockSettings: { lockBeforeSuspend: true, customPowerActionLock: "", batteryLockTimeout: 300 } },
|
||||
_settingsFilesPaths: [main, overlay],
|
||||
_settingsFiles: new Map(Object.keys(fileSettings).map(path => [path, { getSettings: () => Object.assign({}, fileSettings[path]) }])),
|
||||
defaultSettingsFile: { filePath: main }
|
||||
}, ["isManaged", "_splitSettingsByFile"]);
|
||||
const split = context._splitSettingsByFile({ configVersion: 42, lockBeforeSuspend: true, customPowerActionLock: "", batteryLockTimeout: 300, acLockTimeout: 120, fadeToLockEnabled: true, lockAtStartup: true });
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(split)), {
|
||||
[main]: { lockBeforeSuspend: false, acLockTimeout: 120, configVersion: 42, lockAtStartup: true },
|
||||
[overlay]: { customPowerActionLock: "true", fadeToLockEnabled: true }
|
||||
});
|
||||
});
|
||||
|
||||
test("set and reset refuse managed keys and change the others", () => {
|
||||
const changes = [];
|
||||
const warnings = [];
|
||||
const context = settingsDataContext({
|
||||
AdminPolicy: { lockSettings: { lockBeforeSuspend: true } },
|
||||
Spec: { SPEC, set: (root, key, value) => changes.push([key, value]) },
|
||||
log: { warn: (...args) => warnings.push(args.at(-1)) },
|
||||
saveSettings() {},
|
||||
_hooks: {},
|
||||
lockBeforeSuspend: true
|
||||
}, ["isManaged", "set", "resetToDefault"]);
|
||||
context.set("lockBeforeSuspend", false);
|
||||
context.set("lockAtStartup", true);
|
||||
context.resetToDefault(["lockBeforeSuspend", "acLockTimeout"]);
|
||||
assert.deepEqual(changes, [["lockAtStartup", true], ["acLockTimeout", null]]);
|
||||
assert.deepEqual(warnings, ["lockBeforeSuspend"]);
|
||||
});
|
||||
|
||||
test("settings IPC set refuses a managed key, also with the settings IPC switch off", () => {
|
||||
const handlers = settingsIpcHandlers();
|
||||
const managed = new Set(["lockBeforeSuspend"]);
|
||||
for (const disableSettingsIpcSet of [false, true]) {
|
||||
const SettingsData = { lockBeforeSuspend: true, lockAtStartup: false, saveSettings() {}, isManaged: key => managed.has(key) };
|
||||
const context = vm.createContext({ AdminPolicy: { disableSettingsIpcSet }, SettingsData, log: { warn() {} } });
|
||||
for (const handler of handlers)
|
||||
vm.runInContext(handler, context);
|
||||
assert.equal(context.set("lockBeforeSuspend", "false"), disableSettingsIpcSet ? "SETTINGS_SET_DISABLED" : "SETTINGS_SET_MANAGED");
|
||||
assert.equal(SettingsData.lockBeforeSuspend, true);
|
||||
assert.equal(context.set("lockAtStartup", "true"), disableSettingsIpcSet ? "SETTINGS_SET_DISABLED" : "SETTINGS_SET_SUCCESS");
|
||||
assert.equal(SettingsData.lockAtStartup, !disableSettingsIpcSet);
|
||||
}
|
||||
});
|
||||
|
||||
test("the legacy session migration leaves managed lock settings alone", () => {
|
||||
const managed = new Set(["acLockTimeout", "lockBeforeSuspend"]);
|
||||
const changes = [];
|
||||
const context = vm.createContext({ SettingsData: { isManaged: key => managed.has(key), set: (key, value) => changes.push([key, value]) } });
|
||||
vm.runInContext(functionSource("../Common/SessionData.qml", "migrateFromUndefinedToV1"), context);
|
||||
context.migrateFromUndefinedToV1({ acMonitorTimeout: 30, acLockTimeout: 60, lockBeforeSuspend: false, loginctlLockIntegration: false, launchPrefix: "uwsm app --" });
|
||||
assert.deepEqual(changes, [["acMonitorTimeout", 30], ["loginctlLockIntegration", false], ["launchPrefix", "uwsm app --"]]);
|
||||
});
|
||||
Reference in new issue
Block a user