mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-03 20:32:07 -04:00
feat: Implement immutable DMS command policy
- Added pre-run checks for greeter and setup commands to enforce policy restrictions - Created cli-policy.default.json to define blocked commands and user messages for immutable environments.
This commit is contained in:
10
core/cmd/dms/assets/cli-policy.default.json
Normal file
10
core/cmd/dms/assets/cli-policy.default.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"policy_version": 1,
|
||||
"blocked_commands": [
|
||||
"greeter install",
|
||||
"greeter enable",
|
||||
"greeter sync",
|
||||
"setup"
|
||||
],
|
||||
"message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes."
|
||||
}
|
||||
@@ -24,9 +24,10 @@ var greeterCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var greeterInstallCmd = &cobra.Command{
|
||||
Use: "install",
|
||||
Short: "Install and configure DMS greeter",
|
||||
Long: "Install greetd and configure it to use DMS as the greeter interface",
|
||||
Use: "install",
|
||||
Short: "Install and configure DMS greeter",
|
||||
Long: "Install greetd and configure it to use DMS as the greeter interface",
|
||||
PreRunE: requireMutableSystemCommand,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := installGreeter(); err != nil {
|
||||
log.Fatalf("Error installing greeter: %v", err)
|
||||
@@ -35,9 +36,10 @@ var greeterInstallCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var greeterSyncCmd = &cobra.Command{
|
||||
Use: "sync",
|
||||
Short: "Sync DMS theme and settings with greeter",
|
||||
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen",
|
||||
Use: "sync",
|
||||
Short: "Sync DMS theme and settings with greeter",
|
||||
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen",
|
||||
PreRunE: requireMutableSystemCommand,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
yes, _ := cmd.Flags().GetBool("yes")
|
||||
auth, _ := cmd.Flags().GetBool("auth")
|
||||
@@ -63,9 +65,10 @@ func init() {
|
||||
}
|
||||
|
||||
var greeterEnableCmd = &cobra.Command{
|
||||
Use: "enable",
|
||||
Short: "Enable DMS greeter in greetd config",
|
||||
Long: "Configure greetd to use DMS as the greeter",
|
||||
Use: "enable",
|
||||
Short: "Enable DMS greeter in greetd config",
|
||||
Long: "Configure greetd to use DMS as the greeter",
|
||||
PreRunE: requireMutableSystemCommand,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := enableGreeter(); err != nil {
|
||||
log.Fatalf("Error enabling greeter: %v", err)
|
||||
|
||||
@@ -16,9 +16,10 @@ import (
|
||||
)
|
||||
|
||||
var setupCmd = &cobra.Command{
|
||||
Use: "setup",
|
||||
Short: "Deploy DMS configurations",
|
||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||
Use: "setup",
|
||||
Short: "Deploy DMS configurations",
|
||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||
PersistentPreRunE: requireMutableSystemCommand,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runSetup(); err != nil {
|
||||
log.Fatalf("Error during setup: %v", err)
|
||||
|
||||
271
core/cmd/dms/immutable_policy.go
Normal file
271
core/cmd/dms/immutable_policy.go
Normal file
@@ -0,0 +1,271 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
cliPolicyPackagedPath = "/usr/share/dms/cli-policy.json"
|
||||
cliPolicyAdminPath = "/etc/dms/cli-policy.json"
|
||||
)
|
||||
|
||||
var (
|
||||
immutablePolicyOnce sync.Once
|
||||
immutablePolicy immutableCommandPolicy
|
||||
immutablePolicyErr error
|
||||
)
|
||||
|
||||
//go:embed assets/cli-policy.default.json
|
||||
var defaultCLIPolicyJSON []byte
|
||||
|
||||
type immutableCommandPolicy struct {
|
||||
ImmutableSystem bool
|
||||
ImmutableReason string
|
||||
BlockedCommands []string
|
||||
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 ")
|
||||
return strings.Join(strings.Fields(normalized), " ")
|
||||
}
|
||||
|
||||
func normalizeBlockedCommands(raw []string) []string {
|
||||
normalized := make([]string, 0, len(raw))
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, cmd := range raw {
|
||||
spec := normalizeCommandSpec(cmd)
|
||||
if spec == "" || seen[spec] {
|
||||
continue
|
||||
}
|
||||
seen[spec] = true
|
||||
normalized = append(normalized, spec)
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
func commandBlockedByPolicy(commandPath string, blocked []string) bool {
|
||||
normalizedPath := normalizeCommandSpec(commandPath)
|
||||
if normalizedPath == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, entry := range blocked {
|
||||
spec := normalizeCommandSpec(entry)
|
||||
if spec == "" {
|
||||
continue
|
||||
}
|
||||
if normalizedPath == spec || strings.HasPrefix(normalizedPath, spec+" ") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if policyFile == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if policyFile.ImmutableSystem != nil {
|
||||
base.ImmutableSystem = *policyFile.ImmutableSystem
|
||||
}
|
||||
if policyFile.BlockedCommands != nil {
|
||||
base.BlockedCommands = normalizeBlockedCommands(*policyFile.BlockedCommands)
|
||||
}
|
||||
if policyFile.Message != nil {
|
||||
msg := strings.TrimSpace(*policyFile.Message)
|
||||
if msg != "" {
|
||||
base.Message = msg
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readOSReleaseMap(path string) map[string]string {
|
||||
values := make(map[string]string)
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return values
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.ToUpper(strings.TrimSpace(parts[0]))
|
||||
value := strings.Trim(strings.TrimSpace(parts[1]), "\"")
|
||||
values[key] = strings.ToLower(value)
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
func hasAnyToken(text string, tokens ...string) bool {
|
||||
if text == "" {
|
||||
return false
|
||||
}
|
||||
for _, token := range tokens {
|
||||
if strings.Contains(text, token) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func detectImmutableSystem() (bool, string) {
|
||||
if _, err := os.Stat("/run/ostree-booted"); err == nil {
|
||||
return true, "/run/ostree-booted is present"
|
||||
}
|
||||
|
||||
osRelease := readOSReleaseMap("/etc/os-release")
|
||||
if len(osRelease) == 0 {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
id := osRelease["ID"]
|
||||
idLike := osRelease["ID_LIKE"]
|
||||
variantID := osRelease["VARIANT_ID"]
|
||||
name := osRelease["NAME"]
|
||||
prettyName := osRelease["PRETTY_NAME"]
|
||||
|
||||
immutableIDs := map[string]bool{
|
||||
"bluefin": true,
|
||||
"bazzite": true,
|
||||
"silverblue": true,
|
||||
"kinoite": true,
|
||||
"sericea": true,
|
||||
"onyx": true,
|
||||
"aurora": true,
|
||||
"fedora-iot": true,
|
||||
"fedora-coreos": true,
|
||||
}
|
||||
if immutableIDs[id] {
|
||||
return true, "os-release ID=" + id
|
||||
}
|
||||
|
||||
markers := []string{"silverblue", "kinoite", "sericea", "onyx", "bazzite", "bluefin", "aurora", "ostree", "atomic"}
|
||||
if hasAnyToken(variantID, markers...) {
|
||||
return true, "os-release VARIANT_ID=" + variantID
|
||||
}
|
||||
if hasAnyToken(idLike, "ostree", "rpm-ostree") {
|
||||
return true, "os-release ID_LIKE=" + idLike
|
||||
}
|
||||
if hasAnyToken(name, markers...) || hasAnyToken(prettyName, markers...) {
|
||||
return true, "os-release identifies an atomic/ostree variant"
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func getImmutablePolicy() (*immutableCommandPolicy, error) {
|
||||
immutablePolicyOnce.Do(func() {
|
||||
detectedImmutable, reason := detectImmutableSystem()
|
||||
immutablePolicy = immutableCommandPolicy{
|
||||
ImmutableSystem: detectedImmutable,
|
||||
ImmutableReason: reason,
|
||||
BlockedCommands: []string{"greeter install", "greeter enable", "greeter sync", "setup"},
|
||||
Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.",
|
||||
}
|
||||
|
||||
var defaultPolicy cliPolicyFile
|
||||
if err := json.Unmarshal(defaultCLIPolicyJSON, &defaultPolicy); err != nil {
|
||||
immutablePolicyErr = fmt.Errorf("failed to parse embedded default CLI policy: %w", err)
|
||||
return
|
||||
}
|
||||
if defaultPolicy.BlockedCommands != nil {
|
||||
immutablePolicy.BlockedCommands = normalizeBlockedCommands(*defaultPolicy.BlockedCommands)
|
||||
}
|
||||
if defaultPolicy.Message != nil {
|
||||
msg := strings.TrimSpace(*defaultPolicy.Message)
|
||||
if msg != "" {
|
||||
immutablePolicy.Message = msg
|
||||
}
|
||||
}
|
||||
|
||||
if err := mergePolicyFile(&immutablePolicy, cliPolicyPackagedPath); err != nil {
|
||||
immutablePolicyErr = err
|
||||
return
|
||||
}
|
||||
if err := mergePolicyFile(&immutablePolicy, cliPolicyAdminPath); err != nil {
|
||||
immutablePolicyErr = err
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
if immutablePolicyErr != nil {
|
||||
return nil, immutablePolicyErr
|
||||
}
|
||||
return &immutablePolicy, nil
|
||||
}
|
||||
|
||||
func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
|
||||
policy, err := getImmutablePolicy()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !policy.ImmutableSystem {
|
||||
return nil
|
||||
}
|
||||
|
||||
commandPath := normalizeCommandSpec(cmd.CommandPath())
|
||||
if !commandBlockedByPolicy(commandPath, policy.BlockedCommands) {
|
||||
return nil
|
||||
}
|
||||
|
||||
reason := ""
|
||||
if policy.ImmutableReason != "" {
|
||||
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)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -28,7 +29,13 @@ func TestDetectResult_HasNetworkdField(t *testing.T) {
|
||||
|
||||
func TestDetectNetworkStack_Integration(t *testing.T) {
|
||||
result, err := DetectNetworkStack()
|
||||
|
||||
if err != nil && strings.Contains(err.Error(), "connect system bus") {
|
||||
t.Skipf("system D-Bus unavailable: %v", err)
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.NotEmpty(t, result.ChosenReason)
|
||||
if assert.NotNil(t, result) {
|
||||
assert.NotEmpty(t, result.ChosenReason)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user