mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-30 15:07:49 -04:00
registries: let the CLI policy turn the registries off (#3703)
* policy: load the CLI policy file from an internal package The plugin and theme registries need to read the policy too, and internal packages cannot import package main. * registries: let the CLI policy turn the registries off With disable_registries set in /usr/share/dms/cli-policy.json or /etc/dms/cli-policy.json (the /etc file wins), the plugin and theme registries start without sources, so nothing is cloned or pulled. Installed plugins and themes keep working.
This commit is contained in:
1 parent
9ec48c4f13
commit
eeea1a633c
12 files changed
+293
-39
No files matched your search
@@ -12,7 +12,7 @@ import (
|
||||
var registryCmd = &cobra.Command{
|
||||
Use: "registry",
|
||||
Short: "Manage plugin and theme registries",
|
||||
Long: "Manage the registries DMS fetches plugins and themes from. The official registry is always active; additional registries can be added by name and git URL.",
|
||||
Long: "Manage the registries DMS fetches plugins and themes from. The official registry is always active and additional registries can be added by name and git URL, unless the CLI policy sets disable_registries.",
|
||||
}
|
||||
|
||||
var registryListCmd = &cobra.Command{
|
||||
|
||||
@@ -9,15 +9,12 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipolicy"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
cliPolicyPackagedPath = "/usr/share/dms/cli-policy.json"
|
||||
cliPolicyAdminPath = "/etc/dms/cli-policy.json"
|
||||
)
|
||||
|
||||
var (
|
||||
immutablePolicyOnce sync.Once
|
||||
immutablePolicy immutableCommandPolicy
|
||||
@@ -34,13 +31,6 @@ type immutableCommandPolicy struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
type cliPolicyFile struct {
|
||||
PolicyVersion int `json:"policy_version"`
|
||||
ImmutableSystem *bool `json:"immutable_system"`
|
||||
BlockedCommands *[]string `json:"blocked_commands"`
|
||||
Message *string `json:"message"`
|
||||
}
|
||||
|
||||
func normalizeCommandSpec(raw string) string {
|
||||
normalized := strings.ToLower(strings.TrimSpace(raw))
|
||||
normalized = strings.TrimPrefix(normalized, "dms ")
|
||||
@@ -82,25 +72,8 @@ func commandBlockedByPolicy(commandPath string, blocked []string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func loadPolicyFile(path string) (*cliPolicyFile, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to read %s: %w", path, err)
|
||||
}
|
||||
|
||||
var policy cliPolicyFile
|
||||
if err := json.Unmarshal(data, &policy); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse %s: %w", path, err)
|
||||
}
|
||||
|
||||
return &policy, nil
|
||||
}
|
||||
|
||||
func mergePolicyFile(base *immutableCommandPolicy, path string) error {
|
||||
policyFile, err := loadPolicyFile(path)
|
||||
policyFile, err := clipolicy.LoadFile(afero.NewOsFs(), path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -218,7 +191,7 @@ func getImmutablePolicy() (*immutableCommandPolicy, error) {
|
||||
Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.",
|
||||
}
|
||||
|
||||
var defaultPolicy cliPolicyFile
|
||||
var defaultPolicy clipolicy.File
|
||||
if err := json.Unmarshal(defaultCLIPolicyJSON, &defaultPolicy); err != nil {
|
||||
immutablePolicyErr = fmt.Errorf("failed to parse embedded default CLI policy: %w", err)
|
||||
return
|
||||
@@ -233,11 +206,11 @@ func getImmutablePolicy() (*immutableCommandPolicy, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := mergePolicyFile(&immutablePolicy, cliPolicyPackagedPath); err != nil {
|
||||
if err := mergePolicyFile(&immutablePolicy, clipolicy.PackagedPath); err != nil {
|
||||
immutablePolicyErr = err
|
||||
return
|
||||
}
|
||||
if err := mergePolicyFile(&immutablePolicy, cliPolicyAdminPath); err != nil {
|
||||
if err := mergePolicyFile(&immutablePolicy, clipolicy.AdminPath); err != nil {
|
||||
immutablePolicyErr = err
|
||||
return
|
||||
}
|
||||
@@ -268,7 +241,7 @@ func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
|
||||
reason = "Detected immutable system: " + policy.ImmutableReason + "\n"
|
||||
}
|
||||
|
||||
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
|
||||
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, clipolicy.PackagedPath, clipolicy.AdminPath)
|
||||
}
|
||||
|
||||
// preRunPrivileged combines the immutable-system check with a privesc tool
|
||||
|
||||
@@ -3,10 +3,12 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipolicy"
|
||||
)
|
||||
|
||||
func TestDefaultImmutablePolicyAllowsSyncButBlocksEnable(t *testing.T) {
|
||||
var policyFile cliPolicyFile
|
||||
var policyFile clipolicy.File
|
||||
if err := json.Unmarshal(defaultCLIPolicyJSON, &policyFile); err != nil {
|
||||
t.Fatalf("failed to parse embedded CLI policy: %v", err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package clipolicy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
const (
|
||||
PackagedPath = "/usr/share/dms/cli-policy.json"
|
||||
AdminPath = "/etc/dms/cli-policy.json"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
PolicyVersion int `json:"policy_version"`
|
||||
ImmutableSystem *bool `json:"immutable_system"`
|
||||
BlockedCommands *[]string `json:"blocked_commands"`
|
||||
Message *string `json:"message"`
|
||||
DisableRegistries *bool `json:"disable_registries"`
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to read %s: %w", path, err)
|
||||
}
|
||||
|
||||
var policy File
|
||||
if err := json.Unmarshal(data, &policy); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse %s: %w", path, err)
|
||||
}
|
||||
|
||||
return &policy, nil
|
||||
}
|
||||
|
||||
// RegistriesDisabled reports whether the policy turns off the plugin and theme
|
||||
// registries. An explicit value in AdminPath overrides PackagedPath.
|
||||
func RegistriesDisabled(fs afero.Fs) (bool, error) {
|
||||
disabled := false
|
||||
for _, path := range []string{PackagedPath, AdminPath} {
|
||||
policy, err := LoadFile(fs, path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if policy != nil && policy.DisableRegistries != nil {
|
||||
disabled = *policy.DisableRegistries
|
||||
}
|
||||
}
|
||||
return disabled, nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package clipolicy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRegistriesDisabled(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
packaged string
|
||||
admin string
|
||||
want bool
|
||||
wantErr string
|
||||
}{
|
||||
{name: "no policy files"},
|
||||
{name: "packaged policy", packaged: `{"disable_registries": true}`, want: true},
|
||||
{name: "admin policy overrides packaged", packaged: `{"disable_registries": true}`, admin: `{"disable_registries": false}`},
|
||||
{name: "admin policy", admin: `{"disable_registries": true}`, want: true},
|
||||
{name: "admin policy without the key keeps packaged", packaged: `{"disable_registries": true}`, admin: `{"immutable_system": true}`, want: true},
|
||||
{name: "malformed packaged policy", packaged: "{not json", admin: `{"disable_registries": false}`, wantErr: "failed to parse " + PackagedPath},
|
||||
{name: "malformed admin policy", packaged: `{"disable_registries": false}`, admin: "{not json", 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))
|
||||
}
|
||||
|
||||
disabled, err := RegistriesDisabled(fs)
|
||||
if tt.wantErr != "" {
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, disabled)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -178,10 +178,14 @@ func NewRegistry() (*Registry, error) {
|
||||
}
|
||||
|
||||
func NewRegistryWithFs(fs afero.Fs) (*Registry, error) {
|
||||
sources, err := registries.Active(fs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Registry{
|
||||
fs: fs,
|
||||
cacheDir: getCacheDir(),
|
||||
registries: registries.Load(fs),
|
||||
registries: sources,
|
||||
git: &realGitClient{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipolicy"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/registries"
|
||||
"github.com/go-git/go-git/v6"
|
||||
"github.com/go-git/go-git/v6/plumbing"
|
||||
@@ -313,6 +314,72 @@ func TestList(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func setupDisabledRegistry(t *testing.T) (*Registry, afero.Fs) {
|
||||
t.Helper()
|
||||
t.Setenv("XDG_CONFIG_HOME", "/xdg")
|
||||
fs := afero.NewMemMapFs()
|
||||
require.NoError(t, registries.Add(fs, "extra", "https://example.com/extra.git"))
|
||||
require.NoError(t, afero.WriteFile(fs, clipolicy.AdminPath, []byte(`{"disable_registries": true}`), 0o644))
|
||||
registry, err := NewRegistryWithFs(fs)
|
||||
require.NoError(t, err)
|
||||
return registry, fs
|
||||
}
|
||||
|
||||
func TestRegistriesDisabledByPolicy(t *testing.T) {
|
||||
t.Run("does not clone", func(t *testing.T) {
|
||||
registry, _ := setupDisabledRegistry(t)
|
||||
|
||||
var cloned []string
|
||||
registry.git = &mockGitClient{
|
||||
cloneFunc: func(path string, url string) error {
|
||||
cloned = append(cloned, url)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
plugins, err := registry.List()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, plugins)
|
||||
assert.Empty(t, cloned)
|
||||
})
|
||||
|
||||
t.Run("does not pull a cached clone", func(t *testing.T) {
|
||||
registry, fs := setupDisabledRegistry(t)
|
||||
|
||||
origins := make(map[string]string)
|
||||
for _, src := range registries.Load(fs) {
|
||||
dir := filepath.Join(registry.cacheDir, src.Name)
|
||||
require.NoError(t, fs.MkdirAll(dir, 0o755))
|
||||
origins[dir] = src.URL
|
||||
}
|
||||
require.Len(t, origins, 2)
|
||||
|
||||
var pulled []string
|
||||
registry.git = &mockGitClient{
|
||||
originFunc: func(path string) (string, error) {
|
||||
return origins[path], nil
|
||||
},
|
||||
pullFunc: func(path string) error {
|
||||
pulled = append(pulled, path)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
plugins, err := registry.List()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, plugins)
|
||||
assert.Empty(t, pulled)
|
||||
})
|
||||
|
||||
t.Run("malformed policy fails construction", func(t *testing.T) {
|
||||
fs := afero.NewMemMapFs()
|
||||
require.NoError(t, afero.WriteFile(fs, clipolicy.AdminPath, []byte("{not json"), 0o644))
|
||||
|
||||
_, err := NewRegistryWithFs(fs)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
t.Run("clones repository when cache doesn't exist", func(t *testing.T) {
|
||||
registry, fs, tmpDir := setupTestRegistry(t)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipolicy"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
@@ -63,6 +64,19 @@ func Load(fs afero.Fs) []Source {
|
||||
return sources
|
||||
}
|
||||
|
||||
// Active returns the registries to fetch from: none when the CLI policy
|
||||
// disables them, otherwise Load.
|
||||
func Active(fs afero.Fs) ([]Source, error) {
|
||||
disabled, err := clipolicy.RegistriesDisabled(fs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if disabled {
|
||||
return nil, nil
|
||||
}
|
||||
return Load(fs), nil
|
||||
}
|
||||
|
||||
func saveExtras(fs afero.Fs, extras []Source) error {
|
||||
path, err := configPath()
|
||||
if err != nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ package registries
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipolicy"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -77,3 +78,23 @@ func TestLoadIgnoresInvalidConfig(t *testing.T) {
|
||||
assert.Equal(t, "ok", sources[1].Name)
|
||||
assert.Equal(t, officialURL, sources[0].URL, "official cannot be spoofed from config")
|
||||
}
|
||||
|
||||
func TestActive(t *testing.T) {
|
||||
fs := setupFs(t)
|
||||
require.NoError(t, Add(fs, "extra", "https://example.com/extra.git"))
|
||||
|
||||
sources, err := Active(fs)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, sources, 2)
|
||||
assert.Equal(t, Load(fs), sources, "registries are on without a policy")
|
||||
|
||||
require.NoError(t, afero.WriteFile(fs, clipolicy.AdminPath, []byte(`{"disable_registries": true}`), 0o644))
|
||||
sources, err = Active(fs)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, sources, "the extra registry is off too")
|
||||
assert.Len(t, Load(fs), 2, "Load still reports the configured registries")
|
||||
|
||||
require.NoError(t, afero.WriteFile(fs, clipolicy.AdminPath, []byte("{not json"), 0o644))
|
||||
_, err = Active(fs)
|
||||
assert.Error(t, err, "a malformed policy is not ignored")
|
||||
}
|
||||
@@ -27,7 +27,10 @@ func HandleList(conn *ipc.ConnWriter, req ipc.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
feedback := plugins.FetchFeedback()
|
||||
var feedback map[string]plugins.Feedback
|
||||
if len(pluginList) > 0 {
|
||||
feedback = plugins.FetchFeedback()
|
||||
}
|
||||
|
||||
result := make([]PluginInfo, len(pluginList))
|
||||
for i, p := range pluginList {
|
||||
|
||||
@@ -211,10 +211,14 @@ func NewRegistry() (*Registry, error) {
|
||||
}
|
||||
|
||||
func NewRegistryWithFs(fs afero.Fs) (*Registry, error) {
|
||||
sources, err := registries.Active(fs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Registry{
|
||||
fs: fs,
|
||||
cacheDir: getCacheDir(),
|
||||
registries: registries.Load(fs),
|
||||
registries: sources,
|
||||
git: &realGitClient{},
|
||||
}, nil
|
||||
}
|
||||
@@ -405,6 +409,9 @@ func (r *Registry) GetThemeSourcePath(themeID string) string {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
if len(r.registries) == 0 {
|
||||
return ""
|
||||
}
|
||||
// Fallback to first registry (legacy path semantics).
|
||||
return filepath.Join(r.cacheDirFor(r.registries[0]), "themes", themeID, "theme.json")
|
||||
}
|
||||
@@ -416,6 +423,9 @@ func (r *Registry) GetThemeDir(themeID string) string {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
if len(r.registries) == 0 {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(r.cacheDirFor(r.registries[0]), "themes", themeID)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipolicy"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/registries"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
@@ -136,3 +137,59 @@ func TestUpdateMultiRegistry(t *testing.T) {
|
||||
t.Fatalf("expected theme source under official registry, got %q", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistriesDisabledByPolicy(t *testing.T) {
|
||||
t.Setenv("XDG_CONFIG_HOME", "/xdg")
|
||||
fs := afero.NewMemMapFs()
|
||||
if err := registries.Add(fs, "extra", "https://example.com/extra.git"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := afero.WriteFile(fs, clipolicy.AdminPath, []byte(`{"disable_registries": true}`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r, err := NewRegistryWithFs(fs)
|
||||
if err != nil {
|
||||
t.Fatalf("NewRegistryWithFs: %v", err)
|
||||
}
|
||||
var cloned []string
|
||||
r.git = &stubGitClient{
|
||||
cloneFunc: func(path string, url string) error {
|
||||
cloned = append(cloned, url)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
list, err := r.List()
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(list) != 0 {
|
||||
t.Fatalf("expected no themes, got %d", len(list))
|
||||
}
|
||||
if len(cloned) != 0 {
|
||||
t.Fatalf("expected no registry clone, got %v", cloned)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRegistryWithFsMalformedPolicy(t *testing.T) {
|
||||
fs := afero.NewMemMapFs()
|
||||
if err := afero.WriteFile(fs, clipolicy.AdminPath, []byte("{not json"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := NewRegistryWithFs(fs); err == nil {
|
||||
t.Fatal("expected an error for a malformed policy file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestThemePathsWithoutRegistries(t *testing.T) {
|
||||
r := &Registry{fs: afero.NewMemMapFs(), cacheDir: "/test-cache"}
|
||||
|
||||
if dir := r.GetThemeDir("one"); dir != "" {
|
||||
t.Fatalf("expected no theme dir, got %q", dir)
|
||||
}
|
||||
if path := r.GetThemeSourcePath("one"); path != "" {
|
||||
t.Fatalf("expected no theme source, got %q", path)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user