network/e2e/namespace_isolation_test.go
2026-01-22 17:13:08 +02:00

476 lines
16 KiB
Go

//go:build e2e
package e2e
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNamespaceIsolation_Deployments(t *testing.T) {
// Setup two test environments with different namespaces
envA, err := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix()))
require.NoError(t, err, "Failed to create namespace A environment")
envB, err := LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix()))
require.NoError(t, err, "Failed to create namespace B environment")
tarballPath := filepath.Join("../testdata/tarballs/react-vite.tar.gz")
// Create deployment in namespace-a
deploymentNameA := "test-app-ns-a"
deploymentIDA := CreateTestDeployment(t, envA, deploymentNameA, tarballPath)
defer func() {
if !envA.SkipCleanup {
DeleteDeployment(t, envA, deploymentIDA)
}
}()
// Create deployment in namespace-b
deploymentNameB := "test-app-ns-b"
deploymentIDB := CreateTestDeployment(t, envB, deploymentNameB, tarballPath)
defer func() {
if !envB.SkipCleanup {
DeleteDeployment(t, envB, deploymentIDB)
}
}()
t.Run("Namespace-A cannot list Namespace-B deployments", func(t *testing.T) {
req, _ := http.NewRequest("GET", envA.GatewayURL+"/v1/deployments/list", nil)
req.Header.Set("Authorization", "Bearer "+envA.APIKey)
resp, err := envA.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
var result map[string]interface{}
bodyBytes, _ := io.ReadAll(resp.Body)
require.NoError(t, json.Unmarshal(bodyBytes, &result), "Should decode JSON")
deployments, ok := result["deployments"].([]interface{})
require.True(t, ok, "Deployments should be an array")
// Should only see namespace-a deployments
for _, d := range deployments {
dep, ok := d.(map[string]interface{})
if !ok {
continue
}
assert.NotEqual(t, deploymentNameB, dep["name"], "Should not see namespace-b deployment")
}
t.Logf("✓ Namespace A cannot see Namespace B deployments")
})
t.Run("Namespace-A cannot access Namespace-B deployment by ID", func(t *testing.T) {
req, _ := http.NewRequest("GET", envA.GatewayURL+"/v1/deployments/get?id="+deploymentIDB, nil)
req.Header.Set("Authorization", "Bearer "+envA.APIKey)
resp, err := envA.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
// Should return 404 or 403
assert.Contains(t, []int{http.StatusNotFound, http.StatusForbidden}, resp.StatusCode,
"Should block cross-namespace access")
t.Logf("✓ Namespace A cannot access Namespace B deployment (status: %d)", resp.StatusCode)
})
t.Run("Namespace-A cannot delete Namespace-B deployment", func(t *testing.T) {
req, _ := http.NewRequest("DELETE", envA.GatewayURL+"/v1/deployments/delete?id="+deploymentIDB, nil)
req.Header.Set("Authorization", "Bearer "+envA.APIKey)
resp, err := envA.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
assert.Contains(t, []int{http.StatusNotFound, http.StatusForbidden}, resp.StatusCode,
"Should block cross-namespace deletion")
// Verify deployment still exists for namespace-b
req2, _ := http.NewRequest("GET", envB.GatewayURL+"/v1/deployments/get?id="+deploymentIDB, nil)
req2.Header.Set("Authorization", "Bearer "+envB.APIKey)
resp2, err := envB.HTTPClient.Do(req2)
require.NoError(t, err, "Should execute request")
defer resp2.Body.Close()
assert.Equal(t, http.StatusOK, resp2.StatusCode, "Deployment should still exist in namespace B")
t.Logf("✓ Namespace A cannot delete Namespace B deployment")
})
}
func TestNamespaceIsolation_SQLiteDatabases(t *testing.T) {
envA, err := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix()))
require.NoError(t, err, "Should create test environment for namespace-a")
envB, err := LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix()))
require.NoError(t, err, "Should create test environment for namespace-b")
// Create database in namespace-a
dbNameA := "users-db-a"
CreateSQLiteDB(t, envA, dbNameA)
defer func() {
if !envA.SkipCleanup {
DeleteSQLiteDB(t, envA, dbNameA)
}
}()
// Create database in namespace-b
dbNameB := "users-db-b"
CreateSQLiteDB(t, envB, dbNameB)
defer func() {
if !envB.SkipCleanup {
DeleteSQLiteDB(t, envB, dbNameB)
}
}()
t.Run("Namespace-A cannot list Namespace-B databases", func(t *testing.T) {
req, _ := http.NewRequest("GET", envA.GatewayURL+"/v1/db/sqlite/list", nil)
req.Header.Set("Authorization", "Bearer "+envA.APIKey)
resp, err := envA.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
var result map[string]interface{}
bodyBytes, _ := io.ReadAll(resp.Body)
require.NoError(t, json.Unmarshal(bodyBytes, &result), "Should decode JSON")
databases, ok := result["databases"].([]interface{})
require.True(t, ok, "Databases should be an array")
for _, db := range databases {
database, ok := db.(map[string]interface{})
if !ok {
continue
}
assert.NotEqual(t, dbNameB, database["database_name"], "Should not see namespace-b database")
}
t.Logf("✓ Namespace A cannot see Namespace B databases")
})
t.Run("Namespace-A cannot query Namespace-B database", func(t *testing.T) {
reqBody := map[string]interface{}{
"database_name": dbNameB,
"query": "SELECT * FROM users",
}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", envA.GatewayURL+"/v1/db/sqlite/query", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+envA.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := envA.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
assert.Equal(t, http.StatusNotFound, resp.StatusCode, "Should block cross-namespace query")
t.Logf("✓ Namespace A cannot query Namespace B database")
})
t.Run("Namespace-A cannot backup Namespace-B database", func(t *testing.T) {
reqBody := map[string]string{"database_name": dbNameB}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", envA.GatewayURL+"/v1/db/sqlite/backup", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+envA.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := envA.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
assert.Equal(t, http.StatusNotFound, resp.StatusCode, "Should block cross-namespace backup")
t.Logf("✓ Namespace A cannot backup Namespace B database")
})
}
func TestNamespaceIsolation_IPFSContent(t *testing.T) {
envA, err := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix()))
require.NoError(t, err, "Should create test environment for namespace-a")
envB, err := LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix()))
require.NoError(t, err, "Should create test environment for namespace-b")
// Upload file in namespace-a
cidA := UploadTestFile(t, envA, "test-file-a.txt", "Content from namespace A")
defer func() {
if !envA.SkipCleanup {
UnpinFile(t, envA, cidA)
}
}()
t.Run("Namespace-B cannot GET Namespace-A IPFS content", func(t *testing.T) {
// This tests application-level access control
// IPFS content is globally accessible by CID, but our handlers should enforce namespace
req, _ := http.NewRequest("GET", envB.GatewayURL+"/v1/storage/get?cid="+cidA, nil)
req.Header.Set("Authorization", "Bearer "+envB.APIKey)
resp, err := envB.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
// Should return 403 or 404 (namespace doesn't own this CID)
assert.Contains(t, []int{http.StatusNotFound, http.StatusForbidden}, resp.StatusCode,
"Should block cross-namespace IPFS GET")
t.Logf("✓ Namespace B cannot GET Namespace A IPFS content (status: %d)", resp.StatusCode)
})
t.Run("Namespace-B cannot PIN Namespace-A IPFS content", func(t *testing.T) {
reqBody := map[string]string{
"cid": cidA,
"name": "stolen-content",
}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", envB.GatewayURL+"/v1/storage/pin", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+envB.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := envB.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
assert.Contains(t, []int{http.StatusNotFound, http.StatusForbidden}, resp.StatusCode,
"Should block cross-namespace PIN")
t.Logf("✓ Namespace B cannot PIN Namespace A IPFS content (status: %d)", resp.StatusCode)
})
t.Run("Namespace-B cannot UNPIN Namespace-A IPFS content", func(t *testing.T) {
reqBody := map[string]string{"cid": cidA}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", envB.GatewayURL+"/v1/storage/unpin", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+envB.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := envB.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
assert.Contains(t, []int{http.StatusNotFound, http.StatusForbidden}, resp.StatusCode,
"Should block cross-namespace UNPIN")
t.Logf("✓ Namespace B cannot UNPIN Namespace A IPFS content (status: %d)", resp.StatusCode)
})
t.Run("Namespace-A can list only their own IPFS pins", func(t *testing.T) {
req, _ := http.NewRequest("GET", envA.GatewayURL+"/v1/storage/pins", nil)
req.Header.Set("Authorization", "Bearer "+envA.APIKey)
resp, err := envA.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode, "Should list pins successfully")
var pins []map[string]interface{}
bodyBytes, _ := io.ReadAll(resp.Body)
require.NoError(t, json.Unmarshal(bodyBytes, &pins), "Should decode pins")
// Should see their own pin
foundOwn := false
for _, pin := range pins {
if cid, ok := pin["cid"].(string); ok && cid == cidA {
foundOwn = true
break
}
}
assert.True(t, foundOwn, "Should see own pins")
t.Logf("✓ Namespace A can list only their own pins")
})
}
func TestNamespaceIsolation_OlricCache(t *testing.T) {
envA, err := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix()))
require.NoError(t, err, "Should create test environment for namespace-a")
envB, err := LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix()))
require.NoError(t, err, "Should create test environment for namespace-b")
dmap := "test-cache"
keyA := "user-session-123"
valueA := `{"user_id": "alice", "token": "secret-token-a"}`
t.Run("Namespace-A sets cache key", func(t *testing.T) {
reqBody := map[string]interface{}{
"dmap": dmap,
"key": keyA,
"value": valueA,
"ttl": "300s",
}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", envA.GatewayURL+"/v1/cache/put", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+envA.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := envA.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode, "Should set cache key successfully")
t.Logf("✓ Namespace A set cache key")
})
t.Run("Namespace-B cannot GET Namespace-A cache key", func(t *testing.T) {
reqBody := map[string]interface{}{
"dmap": dmap,
"key": keyA,
}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", envB.GatewayURL+"/v1/cache/get", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+envB.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := envB.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
// Should return 404 (key doesn't exist in namespace-b)
assert.Equal(t, http.StatusNotFound, resp.StatusCode, "Should not find key in different namespace")
t.Logf("✓ Namespace B cannot GET Namespace A cache key")
})
t.Run("Namespace-B cannot DELETE Namespace-A cache key", func(t *testing.T) {
reqBody := map[string]string{
"dmap": dmap,
"key": keyA,
}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", envB.GatewayURL+"/v1/cache/delete", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+envB.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := envB.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
// Should return 404 or success (key doesn't exist in their namespace)
assert.Contains(t, []int{http.StatusOK, http.StatusNotFound}, resp.StatusCode)
// Verify key still exists for namespace-a
reqBody2 := map[string]interface{}{
"dmap": dmap,
"key": keyA,
}
bodyBytes2, _ := json.Marshal(reqBody2)
req2, _ := http.NewRequest("POST", envA.GatewayURL+"/v1/cache/get", bytes.NewReader(bodyBytes2))
req2.Header.Set("Authorization", "Bearer "+envA.APIKey)
req2.Header.Set("Content-Type", "application/json")
resp2, err := envA.HTTPClient.Do(req2)
require.NoError(t, err, "Should execute request")
defer resp2.Body.Close()
assert.Equal(t, http.StatusOK, resp2.StatusCode, "Key should still exist in namespace A")
var result map[string]interface{}
bodyBytes3, _ := io.ReadAll(resp2.Body)
require.NoError(t, json.Unmarshal(bodyBytes3, &result), "Should decode result")
// Parse expected JSON string for comparison
var expectedValue map[string]interface{}
json.Unmarshal([]byte(valueA), &expectedValue)
assert.Equal(t, expectedValue, result["value"], "Value should match")
t.Logf("✓ Namespace B cannot DELETE Namespace A cache key")
})
t.Run("Namespace-B can set same key name in their namespace", func(t *testing.T) {
// Same key name, different namespace should be allowed
valueB := `{"user_id": "bob", "token": "secret-token-b"}`
reqBody := map[string]interface{}{
"dmap": dmap,
"key": keyA, // Same key name as namespace-a
"value": valueB,
"ttl": "300s",
}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", envB.GatewayURL+"/v1/cache/put", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+envB.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := envB.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode, "Should set key in namespace B")
// Verify namespace-a still has their value
reqBody2 := map[string]interface{}{
"dmap": dmap,
"key": keyA,
}
bodyBytes2, _ := json.Marshal(reqBody2)
req2, _ := http.NewRequest("POST", envA.GatewayURL+"/v1/cache/get", bytes.NewReader(bodyBytes2))
req2.Header.Set("Authorization", "Bearer "+envA.APIKey)
req2.Header.Set("Content-Type", "application/json")
resp2, _ := envA.HTTPClient.Do(req2)
defer resp2.Body.Close()
var resultA map[string]interface{}
bodyBytesA, _ := io.ReadAll(resp2.Body)
require.NoError(t, json.Unmarshal(bodyBytesA, &resultA), "Should decode result A")
// Parse expected JSON string for comparison
var expectedValueA map[string]interface{}
json.Unmarshal([]byte(valueA), &expectedValueA)
assert.Equal(t, expectedValueA, resultA["value"], "Namespace A value should be unchanged")
// Verify namespace-b has their different value
reqBody3 := map[string]interface{}{
"dmap": dmap,
"key": keyA,
}
bodyBytes3, _ := json.Marshal(reqBody3)
req3, _ := http.NewRequest("POST", envB.GatewayURL+"/v1/cache/get", bytes.NewReader(bodyBytes3))
req3.Header.Set("Authorization", "Bearer "+envB.APIKey)
req3.Header.Set("Content-Type", "application/json")
resp3, _ := envB.HTTPClient.Do(req3)
defer resp3.Body.Close()
var resultB map[string]interface{}
bodyBytesB, _ := io.ReadAll(resp3.Body)
require.NoError(t, json.Unmarshal(bodyBytesB, &resultB), "Should decode result B")
// Parse expected JSON string for comparison
var expectedValueB map[string]interface{}
json.Unmarshal([]byte(valueB), &expectedValueB)
assert.Equal(t, expectedValueB, resultB["value"], "Namespace B value should be different")
t.Logf("✓ Namespace B can set same key name independently")
t.Logf(" - Namespace A value: %s", valueA)
t.Logf(" - Namespace B value: %s", valueB)
})
}