packages feed

clod-0.1.0: test/Clod/CapabilitySpec.hs

{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module      : Clod.CapabilitySpec
-- Description : Tests for capability-based security
-- Copyright   : (c) Fuzz Leonard, 2025
-- License     : MIT
-- Maintainer  : cyborg@bionicfuzz.com
-- Stability   : experimental
--
-- This module contains tests for the capability-based security system,
-- which is a critical component for ensuring files cannot be accessed
-- outside allowed directories.

module Clod.CapabilitySpec (spec) where

import Test.Hspec
import System.Directory
import System.FilePath
import System.IO.Temp (withSystemTempDirectory)
import Data.Either (isRight, isLeft)
import qualified System.IO

import Clod.Types
import Clod.FileSystem.Operations (safeReadFile, safeWriteFile)
import Clod.TestHelpers (defaultTestConfig)

-- | Test specification for capability-based security
spec :: Spec
spec = do
  fileReadCapSpec
  fileWriteCapSpec
  gitCapSpec
  capabilityEscapePreventionSpec

-- | Tests for file read capabilities
fileReadCapSpec :: Spec
fileReadCapSpec = describe "File read capabilities" $ do
  it "allows access to files inside permitted directories" $ do
    withSystemTempDirectory "clod-test" $ \tmpDir -> do
      -- Create test files
      let subDir = tmpDir </> "allowed"
      createDirectoryIfMissing True subDir
      System.IO.writeFile (subDir </> "test.txt") "test content"
      
      -- Create capability that allows access to the subDir
      let readCap = fileReadCap [subDir]
          config = defaultTestConfig tmpDir
      
      -- Test file access
      result <- runClodM config $ safeReadFile readCap (subDir </> "test.txt")
      
      -- Should succeed
      result `shouldSatisfy` isRight
      
  it "denies access to files outside permitted directories" $ do
    withSystemTempDirectory "clod-test" $ \tmpDir -> do
      -- Create test files
      let allowedDir = tmpDir </> "allowed"
      let forbiddenDir = tmpDir </> "forbidden"
      
      createDirectoryIfMissing True allowedDir
      createDirectoryIfMissing True forbiddenDir
      
      System.IO.writeFile (allowedDir </> "allowed.txt") "allowed content"
      System.IO.writeFile (forbiddenDir </> "forbidden.txt") "forbidden content"
      
      -- Create capability that only allows access to allowedDir
      let readCap = fileReadCap [allowedDir]
          config = defaultTestConfig tmpDir
      
      -- Try to access forbidden file
      result <- runClodM config $ safeReadFile readCap (forbiddenDir </> "forbidden.txt")
      
      -- Should fail with permission error
      result `shouldSatisfy` isLeft
      case result of
        Left err -> putStrLn (show err) -- Just check that it's an error, we'll skip exact error message
        Right _ -> expectationFailure "Access was granted to a forbidden file"

-- | Tests for file write capabilities
fileWriteCapSpec :: Spec
fileWriteCapSpec = describe "File write capabilities" $ do
  it "allows writing to files inside permitted directories" $ do
    withSystemTempDirectory "clod-test" $ \tmpDir -> do
      -- Create test directory
      let writeDir = tmpDir </> "writable"
      createDirectoryIfMissing True writeDir
      
      -- Create write capability
      let writeCap = fileWriteCap [writeDir]
          config = defaultTestConfig tmpDir
      
      -- Try to write to permitted directory
      result <- runClodM config $ safeWriteFile writeCap (writeDir </> "new.txt") "new content"
      
      -- Should succeed
      result `shouldSatisfy` isRight
      
      -- Verify file was created
      fileExistsCheck <- doesFileExist (writeDir </> "new.txt")
      fileExistsCheck `shouldBe` True
      
  it "denies writing to files outside permitted directories" $ do
    withSystemTempDirectory "clod-test" $ \tmpDir -> do
      -- Create test directories
      let writeDir = tmpDir </> "writable"
      let noWriteDir = tmpDir </> "not-writable"
      
      createDirectoryIfMissing True writeDir
      createDirectoryIfMissing True noWriteDir
      
      -- Create write capability that only allows certain directories
      let writeCap = fileWriteCap [writeDir]
          config = defaultTestConfig tmpDir
      
      -- Try to write to forbidden directory
      result <- runClodM config $ safeWriteFile writeCap (noWriteDir </> "forbidden.txt") "forbidden content"
      
      -- Should fail with permission error
      result `shouldSatisfy` isLeft
      case result of
        Left err -> putStrLn (show err) -- Just check that it's an error
        Right _ -> expectationFailure "Write access was granted to a forbidden directory"

-- | Tests for Checksum Database capabilities
gitCapSpec :: Spec
gitCapSpec = describe "Database capabilities" $ do
  it "allows basic file operations with capabilities" $ do
    withSystemTempDirectory "clod-test" $ \tmpDir -> do
      -- Set up a test directory
      let dataDir = tmpDir </> "data-dir"
      createDirectoryIfMissing True dataDir
      
      -- Create a file
      System.IO.writeFile (dataDir </> "test-file.txt") "test content"
      
      -- Create capabilities
      let readCap = fileReadCap [dataDir]
          writeCap = fileWriteCap [dataDir]
          config = defaultTestConfig tmpDir
      
      -- Check file access
      readResult <- runClodM config $ safeReadFile readCap (dataDir </> "test-file.txt")
      
      -- Should succeed
      readResult `shouldSatisfy` isRight
      
      -- Write a new file with write capability
      writeResult <- runClodM config $ safeWriteFile writeCap (dataDir </> "new-file.txt") "new content"
      writeResult `shouldSatisfy` isRight
      
  it "restricts database operations outside permitted directories" $ do
    withSystemTempDirectory "clod-test" $ \tmpDir -> do
      -- Set up test directories
      let allowedDir = tmpDir </> "allowed-dir"
      let forbiddenDir = tmpDir </> "forbidden-dir"
      
      -- Create the directories
      createDirectoryIfMissing True allowedDir
      createDirectoryIfMissing True forbiddenDir
      
      -- Create files in both dirs
      System.IO.writeFile (allowedDir </> "allowed.txt") "allowed content"
      System.IO.writeFile (forbiddenDir </> "forbidden.txt") "forbidden content"
      
      -- Create restricted capabilities
      let readCap = fileReadCap [allowedDir]
          writeCap = fileWriteCap [allowedDir]
          config = defaultTestConfig tmpDir
      
      -- Try to access forbidden dir
      readResult <- runClodM config $ safeReadFile readCap (forbiddenDir </> "forbidden.txt")
      
      -- Should fail with permission error
      readResult `shouldSatisfy` isLeft
      
      -- Try to write to forbidden dir
      writeResult <- runClodM config $ safeWriteFile writeCap (forbiddenDir </> "new-file.txt") "new content"
      
      -- Should fail with permission error
      writeResult `shouldSatisfy` isLeft

-- | Tests that specifically try to escape capability restrictions
capabilityEscapePreventionSpec :: Spec
capabilityEscapePreventionSpec = describe "Capability escape prevention" $ do
  it "prevents path traversal attacks with ../" $ do
    withSystemTempDirectory "clod-test" $ \tmpDir -> do
      -- Set up test directories
      let allowedDir = tmpDir </> "allowed"
      let secretDir = tmpDir </> "secret"
      
      createDirectoryIfMissing True allowedDir
      createDirectoryIfMissing True secretDir
      
      -- Create a secret file
      System.IO.writeFile (secretDir </> "secret.txt") "highly sensitive data"
      
      -- Create a limited capability
      let readCap = fileReadCap [allowedDir]
          config = defaultTestConfig tmpDir
      
      -- Try path traversal attack using ../
      let traversalPath = allowedDir </> ".." </> "secret" </> "secret.txt"
      
      -- Verify the path does point to the secret file
      canonicalTraversalPath <- canonicalizePath traversalPath
      canonicalSecretPath <- canonicalizePath (secretDir </> "secret.txt")
      canonicalTraversalPath `shouldBe` canonicalSecretPath
      
      -- But capability should prevent access
      result <- runClodM config $ safeReadFile readCap traversalPath
      
      -- Should fail with permission error
      result `shouldSatisfy` isLeft
      
  it "prevents access via symbolic links outside allowed directories" $ do
    withSystemTempDirectory "clod-test" $ \tmpDir -> do
      -- Skip test on Windows where symlinks might not work
      let isWindows = False -- Replace with actual OS detection if needed
      
      if isWindows
        then pendingWith "Symlink tests not applicable on Windows"
        else do
          -- Set up test directories
          let allowedDir = tmpDir </> "allowed"
          let secretDir = tmpDir </> "secret"
          
          createDirectoryIfMissing True allowedDir
          createDirectoryIfMissing True secretDir
          
          -- Create a secret file
          System.IO.writeFile (secretDir </> "secret.txt") "highly sensitive data"
          
          -- Create a symlink in the allowed directory pointing to the secret
          let linkPath = allowedDir </> "link-to-secret.txt"
          
          -- Create symlink (catching exceptions as this may fail on some systems)
          createFileLink (secretDir </> "secret.txt") linkPath
          
          -- Create a limited capability
          let readCap = fileReadCap [allowedDir]
              config = defaultTestConfig tmpDir
          
          -- Try to access via symlink
          result <- runClodM config $ safeReadFile readCap linkPath
          
          -- Should fail with permission error since the target is outside
          result `shouldSatisfy` isLeft