mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-30 05:53:37 +00:00
216 lines
8.0 KiB
Swift
216 lines
8.0 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import OpenClaw
|
|
|
|
@Suite(.serialized)
|
|
struct ExecApprovalsStoreRefactorTests {
|
|
private var realTemporaryDirectory: URL {
|
|
let path = FileManager().temporaryDirectory.path
|
|
if path.hasPrefix("/var/") {
|
|
return URL(fileURLWithPath: "/private\(path)", isDirectory: true)
|
|
}
|
|
return FileManager().temporaryDirectory.resolvingSymlinksInPath()
|
|
}
|
|
|
|
private func withLockedEnv(
|
|
_ values: [String: String?],
|
|
_ body: () async throws -> Void) async throws
|
|
{
|
|
func restoreEnv(_ values: [String: String?]) {
|
|
for (key, value) in values {
|
|
if let value {
|
|
setenv(key, value, 1)
|
|
} else {
|
|
unsetenv(key)
|
|
}
|
|
}
|
|
}
|
|
|
|
await TestIsolationLock.shared.acquire()
|
|
var previousEnv: [String: String?] = [:]
|
|
for (key, value) in values {
|
|
previousEnv[key] = getenv(key).map { String(cString: $0) }
|
|
if let value {
|
|
setenv(key, value, 1)
|
|
} else {
|
|
unsetenv(key)
|
|
}
|
|
}
|
|
|
|
do {
|
|
try await body()
|
|
restoreEnv(previousEnv)
|
|
await TestIsolationLock.shared.release()
|
|
} catch {
|
|
restoreEnv(previousEnv)
|
|
await TestIsolationLock.shared.release()
|
|
throw error
|
|
}
|
|
}
|
|
|
|
private func withTempStateDir(
|
|
_ body: @escaping @Sendable (URL) async throws -> Void) async throws
|
|
{
|
|
let root = self.realTemporaryDirectory
|
|
.appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true)
|
|
let home = root.appendingPathComponent("home", isDirectory: true)
|
|
let stateDir = root.appendingPathComponent("state", isDirectory: true)
|
|
defer { try? FileManager().removeItem(at: root) }
|
|
try Self.seedCurrentApprovalsFile(in: stateDir)
|
|
|
|
try await self.withLockedEnv([
|
|
"OPENCLAW_HOME": home.path,
|
|
"OPENCLAW_STATE_DIR": stateDir.path,
|
|
]) {
|
|
try await body(stateDir)
|
|
}
|
|
}
|
|
|
|
private func withTempHomeAndStateDir(
|
|
_ body: @escaping @Sendable (URL, URL) async throws -> Void) async throws
|
|
{
|
|
let root = self.realTemporaryDirectory
|
|
.appendingPathComponent("openclaw-home-state-\(UUID().uuidString)", isDirectory: true)
|
|
let home = root.appendingPathComponent("home", isDirectory: true)
|
|
let stateDir = root.appendingPathComponent("state", isDirectory: true)
|
|
defer { try? FileManager().removeItem(at: root) }
|
|
|
|
try await self.withLockedEnv([
|
|
"OPENCLAW_HOME": home.path,
|
|
"OPENCLAW_STATE_DIR": stateDir.path,
|
|
]) {
|
|
try await body(home, stateDir)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `ensure file skips rewrite when unchanged`() async throws {
|
|
try await self.withTempStateDir { _ in
|
|
_ = ExecApprovalsStore.ensureFile()
|
|
let url = ExecApprovalsStore.fileURL()
|
|
let firstIdentity = try Self.fileIdentity(at: url)
|
|
|
|
_ = ExecApprovalsStore.ensureFile()
|
|
let secondIdentity = try Self.fileIdentity(at: url)
|
|
|
|
#expect(firstIdentity == secondIdentity)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `ensure file migrates default approvals into custom state dir`() async throws {
|
|
try await self.withTempHomeAndStateDir { home, stateDir in
|
|
let legacyDir = home.appendingPathComponent(".openclaw", isDirectory: true)
|
|
try FileManager().createDirectory(
|
|
at: legacyDir,
|
|
withIntermediateDirectories: true)
|
|
let legacySocket = legacyDir.appendingPathComponent("exec-approvals.sock").path
|
|
let legacyFile = legacyDir.appendingPathComponent("exec-approvals.json")
|
|
let legacyJson = """
|
|
{
|
|
"version": 1,
|
|
"socket": {
|
|
"path": "\(legacySocket)",
|
|
"token": "legacy-token"
|
|
},
|
|
"defaults": {
|
|
"security": "deny",
|
|
"ask": "always"
|
|
},
|
|
"agents": {
|
|
"main": {
|
|
"allowlist": [{ "pattern": "git status" }]
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
try Data(legacyJson.utf8).write(to: legacyFile)
|
|
|
|
let file = ExecApprovalsStore.ensureFile()
|
|
let targetURL = ExecApprovalsStore.fileURL()
|
|
|
|
#expect(targetURL.path == stateDir.appendingPathComponent("exec-approvals.json").path)
|
|
#expect(FileManager().fileExists(atPath: targetURL.path))
|
|
#expect(file.socket?.path == stateDir.appendingPathComponent("exec-approvals.sock").path)
|
|
#expect(file.socket?.token == "legacy-token")
|
|
#expect(file.defaults?.security == .deny)
|
|
#expect(file.defaults?.ask == .always)
|
|
#expect(file.agents?["main"]?.allowlist?.map(\.pattern) == ["git status"])
|
|
#expect(!FileManager().fileExists(atPath: legacyFile.path))
|
|
#expect(FileManager().fileExists(atPath: "\(legacyFile.path).migrated"))
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `update allowlist accepts basename pattern`() async throws {
|
|
try await self.withTempStateDir { _ in
|
|
let rejected = ExecApprovalsStore.updateAllowlist(
|
|
agentId: "main",
|
|
allowlist: [
|
|
ExecAllowlistEntry(pattern: "echo"),
|
|
ExecAllowlistEntry(pattern: "/bin/echo"),
|
|
])
|
|
#expect(rejected.isEmpty)
|
|
|
|
let resolved = ExecApprovalsStore.resolve(agentId: "main")
|
|
#expect(resolved.allowlist.map(\.pattern) == ["echo", "/bin/echo"])
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `update allowlist migrates legacy pattern from resolved path`() async throws {
|
|
try await self.withTempStateDir { _ in
|
|
let rejected = ExecApprovalsStore.updateAllowlist(
|
|
agentId: "main",
|
|
allowlist: [
|
|
ExecAllowlistEntry(
|
|
pattern: "echo",
|
|
lastUsedAt: nil,
|
|
lastUsedCommand: nil,
|
|
lastResolvedPath: " /usr/bin/echo "),
|
|
])
|
|
#expect(rejected.isEmpty)
|
|
|
|
let resolved = ExecApprovalsStore.resolve(agentId: "main")
|
|
#expect(resolved.allowlist.map(\.pattern) == ["/usr/bin/echo"])
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `ensure file hardens state directory permissions`() async throws {
|
|
try await self.withTempStateDir { stateDir in
|
|
try FileManager().createDirectory(at: stateDir, withIntermediateDirectories: true)
|
|
try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: stateDir.path)
|
|
|
|
_ = ExecApprovalsStore.ensureFile()
|
|
let attrs = try FileManager().attributesOfItem(atPath: stateDir.path)
|
|
let permissions = (attrs[.posixPermissions] as? NSNumber)?.intValue ?? -1
|
|
#expect(permissions & 0o777 == 0o700)
|
|
}
|
|
}
|
|
|
|
private static func fileIdentity(at url: URL) throws -> Int {
|
|
let attributes = try FileManager().attributesOfItem(atPath: url.path)
|
|
guard let identifier = (attributes[.systemFileNumber] as? NSNumber)?.intValue else {
|
|
struct MissingIdentifierError: Error {}
|
|
throw MissingIdentifierError()
|
|
}
|
|
return identifier
|
|
}
|
|
|
|
private static func seedCurrentApprovalsFile(in stateDir: URL) throws {
|
|
try FileManager().createDirectory(at: stateDir, withIntermediateDirectories: true)
|
|
let file = ExecApprovalsFile(
|
|
version: 1,
|
|
socket: ExecApprovalsSocketConfig(
|
|
path: stateDir.appendingPathComponent("exec-approvals.sock").path,
|
|
token: "test-token"),
|
|
defaults: nil,
|
|
agents: [:])
|
|
let encoder = JSONEncoder()
|
|
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
|
try encoder.encode(file)
|
|
.write(to: stateDir.appendingPathComponent("exec-approvals.json"))
|
|
}
|
|
}
|