add allow commands list
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
from nexus_sync.client.config import (
|
||||
COMMAND_ACCESS_ENV,
|
||||
FULL_ACCESS_VALUE,
|
||||
load_command_access_policy,
|
||||
)
|
||||
from nexus_sync.client.execute import (
|
||||
CommandAccessPolicy,
|
||||
execute_command,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"COMMAND_ACCESS_ENV",
|
||||
"CommandAccessPolicy",
|
||||
"FULL_ACCESS_VALUE",
|
||||
"execute_command",
|
||||
"load_command_access_policy",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import os
|
||||
from collections.abc import Mapping
|
||||
|
||||
from nexus_sync.client.execute import CommandAccessPolicy
|
||||
|
||||
COMMAND_ACCESS_ENV = "NEXUS_SYNC_ALLOWED_COMMANDS"
|
||||
FULL_ACCESS_VALUE = "full_access"
|
||||
|
||||
|
||||
def load_command_access_policy(
|
||||
env: Mapping[str, str] = os.environ,
|
||||
*,
|
||||
default: CommandAccessPolicy | None = None,
|
||||
) -> CommandAccessPolicy:
|
||||
raw_value = env.get(COMMAND_ACCESS_ENV)
|
||||
if raw_value is None or not raw_value.strip():
|
||||
return default or CommandAccessPolicy.deny_all()
|
||||
|
||||
command_names = [item.strip() for item in raw_value.split(",") if item.strip()]
|
||||
if len(command_names) == 1 and command_names[0].lower() == FULL_ACCESS_VALUE:
|
||||
return CommandAccessPolicy.allow_all()
|
||||
if any(command_name.lower() == FULL_ACCESS_VALUE for command_name in command_names):
|
||||
raise ValueError(f"{FULL_ACCESS_VALUE} cannot be mixed with explicit command names")
|
||||
|
||||
return CommandAccessPolicy.allow(command_names)
|
||||
@@ -1,5 +1,7 @@
|
||||
import platform
|
||||
import subprocess
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any, Callable, Mapping, Sequence
|
||||
|
||||
@@ -10,6 +12,27 @@ PresetBuilder = Callable[[Mapping[str, Any]], Sequence[str]]
|
||||
DEFAULT_OUTPUT_LIMIT_BYTES = 64 * 1024
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CommandAccessPolicy:
|
||||
allowed_commands: frozenset[str] = field(default_factory=frozenset)
|
||||
full_access: bool = False
|
||||
|
||||
@classmethod
|
||||
def allow(cls, command_names: Iterable[str]) -> "CommandAccessPolicy":
|
||||
return cls(allowed_commands=frozenset(command_names))
|
||||
|
||||
@classmethod
|
||||
def allow_all(cls) -> "CommandAccessPolicy":
|
||||
return cls(full_access=True)
|
||||
|
||||
@classmethod
|
||||
def deny_all(cls) -> "CommandAccessPolicy":
|
||||
return cls()
|
||||
|
||||
def allows(self, command_name: str) -> bool:
|
||||
return self.full_access or command_name in self.allowed_commands
|
||||
|
||||
|
||||
def _reject(command: Command, message: str) -> CommandResult:
|
||||
now = datetime.now(UTC)
|
||||
return CommandResult(
|
||||
@@ -45,6 +68,7 @@ DEFAULT_PRESETS: dict[str, PresetBuilder] = {
|
||||
"hostname": _hostname,
|
||||
"network_interfaces": _network_interfaces,
|
||||
}
|
||||
DEFAULT_COMMAND_ACCESS_POLICY = CommandAccessPolicy.allow_all()
|
||||
|
||||
|
||||
def execute_command(
|
||||
@@ -52,6 +76,7 @@ def execute_command(
|
||||
*,
|
||||
stdin: str | None = None,
|
||||
presets: Mapping[str, PresetBuilder] = DEFAULT_PRESETS,
|
||||
access_policy: CommandAccessPolicy = DEFAULT_COMMAND_ACCESS_POLICY,
|
||||
output_limit_bytes: int = DEFAULT_OUTPUT_LIMIT_BYTES,
|
||||
) -> CommandResult:
|
||||
if command.kind != CommandKind.EXEC:
|
||||
@@ -60,6 +85,8 @@ def execute_command(
|
||||
builder = presets.get(command.name)
|
||||
if builder is None:
|
||||
return _reject(command, f"unknown command preset: {command.name}")
|
||||
if not access_policy.allows(command.name):
|
||||
return _reject(command, f"command preset is not allowed: {command.name}")
|
||||
|
||||
try:
|
||||
argv = list(builder(command.args))
|
||||
|
||||
Reference in New Issue
Block a user