packages feed

clod-0.1.0: test/Clod/AdvancedCapabilitySpec.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module      : Clod.AdvancedCapabilitySpec
-- Description : Tests for the advanced capability system
-- Copyright   : (c) Fuzz Leonard, 2025
-- License     : MIT
-- Maintainer  : cyborg@bionicfuzz.com
-- Stability   : experimental
--
-- This module contains tests for the advanced capability-based security system
-- that uses type-level programming to enforce security constraints.

module Clod.AdvancedCapabilitySpec (spec) where

import Test.Hspec
import System.Directory (createDirectoryIfMissing, doesFileExist)
import System.FilePath ((</>))
import System.IO.Temp (withSystemTempDirectory)
import qualified Data.ByteString as BS
import Control.Monad.IO.Class (liftIO)

import Clod.AdvancedCapability hiding (readFile, writeFile)
import qualified Clod.AdvancedCapability as AC

-- | Test specification for AdvancedCapability module
spec :: Spec
spec = do
  describe "Advanced capability system" $ do
    typeRestrictedFileAccessSpec
    pathTraversalProtectionSpec
    permissionConstraintsSpec
    
-- | Tests for type-restricted file access
typeRestrictedFileAccessSpec :: Spec
typeRestrictedFileAccessSpec = describe "Type-restricted file access" $ do
  it "allows access to files inside permitted directories" $ do
    withSystemTempDirectory "clod-test" $ \tmpDir -> do
      -- Create test directory and file
      let testDir = tmpDir </> "test-dir"
      let testFile = testDir </> "test-file.txt"
      createDirectoryIfMissing True testDir
      BS.writeFile testFile "Hello, World!"
      
      -- Create capability for the test directory
      let cap = createCapability @'Read [testDir]
      
      -- Use withPath to create a typed path
      withPath cap testFile $ \pathMaybe -> do
        -- pathMaybe should be Just because the path is allowed
        case pathMaybe of
          Nothing -> expectationFailure "Path should be allowed but wasn't"
          Just typedPath -> do
            -- Read file with the capability
            content <- AC.readFile cap typedPath
            content `shouldBe` "Hello, World!"
            
  it "denies access to files outside permitted directories" $ do
    withSystemTempDirectory "clod-test" $ \tmpDir -> do
      -- Create allowed and forbidden directories
      let allowedDir = tmpDir </> "allowed"
      let forbiddenDir = tmpDir </> "forbidden"
      createDirectoryIfMissing True allowedDir
      createDirectoryIfMissing True forbiddenDir
      
      -- Create files in both directories
      let allowedFile = allowedDir </> "allowed.txt"
      let forbiddenFile = forbiddenDir </> "forbidden.txt"
      BS.writeFile allowedFile "Allowed content"
      BS.writeFile forbiddenFile "Forbidden content"
      
      -- Create capability for the allowed directory only
      let cap = createCapability @'Read [allowedDir]
      
      -- Try to access forbidden file
      withPath cap forbiddenFile $ \pathMaybe -> do
        -- pathMaybe should be Nothing because the path is not allowed
        pathMaybe `shouldBe` Nothing

-- | Tests for path traversal protection
pathTraversalProtectionSpec :: Spec
pathTraversalProtectionSpec = describe "Path traversal protection" $ do
  it "prevents path traversal attacks with ../" $ do
    withSystemTempDirectory "clod-test" $ \tmpDir -> do
      -- Create test directory structure
      let safeDir = tmpDir </> "safe"
      let dataFile = safeDir </> "data.txt"
      let secretDir = tmpDir </> "secret"
      let secretFile = secretDir </> "secret.txt"
      
      createDirectoryIfMissing True safeDir
      createDirectoryIfMissing True secretDir
      BS.writeFile dataFile "Safe data"
      BS.writeFile secretFile "Secret data"
      
      -- Create capability for the safe directory only
      let cap = createCapability @'Read [safeDir]
      
      -- Try to access secret file through path traversal
      let traversalPath = safeDir </> "../secret/secret.txt"
      
      withPath cap traversalPath $ \pathMaybe -> do
        -- pathMaybe should be Nothing because path traversal is prevented
        pathMaybe `shouldBe` Nothing

-- | Tests for permission constraints
permissionConstraintsSpec :: Spec
permissionConstraintsSpec = describe "Permission constraints" $ do
  it "allows write operations with write capability" $ do
    withSystemTempDirectory "clod-test" $ \tmpDir -> do
      -- Create test directory
      let testDir = tmpDir </> "write-test"
      let testFile = testDir </> "writeable.txt"
      createDirectoryIfMissing True testDir
      
      -- Create write capability
      let writeCap = createCapability @'Write [testDir]
      
      -- Use withPath to get a typed path
      withPath writeCap testFile $ \pathMaybe -> do
        case pathMaybe of
          Nothing -> expectationFailure "Path should be allowed for writing"
          Just typedPath -> do
            -- Write to the file
            AC.writeFile writeCap typedPath "Written with capability"
            
            -- Verify content was written
            fileExists <- liftIO $ doesFileExist testFile
            fileExists `shouldBe` True
            content <- liftIO $ BS.readFile testFile
            content `shouldBe` "Written with capability"
            
  it "allows using AllPerm for both read and write" $ do
    withSystemTempDirectory "clod-test" $ \tmpDir -> do
      -- Create test directory
      let testDir = tmpDir </> "all-perm-test"
      let testFile = testDir </> "all-access.txt"
      createDirectoryIfMissing True testDir
      
      -- Create all-permission capability
      let allCap = createCapability @'All [testDir]
      
      -- Use withPath to get a typed path
      withPath allCap testFile $ \pathMaybe -> do
        case pathMaybe of
          Nothing -> expectationFailure "Path should be allowed with AllPerm"
          Just typedPath -> do
            -- Write to file
            AC.writeFile allCap typedPath "Initial content"
            
            -- Read from file
            content <- AC.readFile allCap typedPath
            content `shouldBe` "Initial content"
  
  it "allows capability restriction to more limited permissions" $ do
    withSystemTempDirectory "clod-test" $ \tmpDir -> do
      -- Create test directory
      let testDir = tmpDir </> "restriction-test"
      let testFile = testDir </> "restrict.txt"
      createDirectoryIfMissing True testDir
      BS.writeFile testFile "Original content"
      
      -- Create all-permission capability
      let allCap = createCapability @'All [testDir]
      
      -- Restrict to read-only
      let readCap = restrictCapability allCap
      
      -- Use withPath to get a typed path
      withPath readCap testFile $ \pathMaybe -> do
        case pathMaybe of
          Nothing -> expectationFailure "Path should be allowed for reading"
          Just typedPath -> do
            -- Read should be allowed
            content <- AC.readFile readCap typedPath
            content `shouldBe` "Original content"
            
            -- This would fail to compile:
            -- writeFile readCap typedPath "New content" -- Type error!