Files
heicode-mananger/heicode/controller/resource_test.go
T

436 lines
17 KiB
Go

package controller
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/heicode/manager/common"
"github.com/heicode/manager/model"
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
func setupResourceControllerTestDB(t *testing.T) *gorm.DB {
t.Helper()
gin.SetMode(gin.TestMode)
common.UsingSQLite = true
common.UsingMySQL = false
common.UsingPostgreSQL = false
common.RedisEnabled = false
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", strings.ReplaceAll(t.Name(), "/", "_"))
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
require.NoError(t, err)
model.DB = db
model.LOG_DB = db
require.NoError(t, db.AutoMigrate(&model.ResourceBinding{}, &model.ResourceGrant{}))
t.Cleanup(func() {
sqlDB, err := db.DB()
if err == nil {
_ = sqlDB.Close()
}
})
return db
}
func performResourceRequest(handler gin.HandlerFunc, userID int, method string, path string, body string) *httptest.ResponseRecorder {
return performResourceRequestWithRoute(handler, userID, method, path, path, body)
}
func performResourceRequestWithRoute(handler gin.HandlerFunc, userID int, method string, routePath string, requestPath string, body string) *httptest.ResponseRecorder {
r := gin.New()
r.Handle(method, routePath, func(c *gin.Context) {
c.Set("id", userID)
handler(c)
})
req := httptest.NewRequest(method, requestPath, bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
return w
}
func TestCreateResourceStoresMetadataAndSecretRefOnly(t *testing.T) {
db := setupResourceControllerTestDB(t)
body := `{
"binding_scope":"https://example.com/org/repo#main",
"name":"Project repository",
"resource_type":"git",
"provider":"github",
"external_id":"https://example.com/org/repo",
"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/repo",
"metadata":{"repo_url":"https://example.com/org/repo","ref":"main","allowed_paths":["."]},
"permission_scope":{"actions":["read","write"]},
"constraints":{"environment":"dev"}
}`
w := performResourceRequest(CreateResource, 7, http.MethodPost, "/", body)
require.Equal(t, http.StatusOK, w.Code)
require.Contains(t, w.Body.String(), `"success":true`)
require.Contains(t, w.Body.String(), `"binding_scope":"https://example.com/org/repo#main"`)
require.Contains(t, w.Body.String(), `"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/repo"`)
var resource model.ResourceBinding
require.NoError(t, db.First(&resource).Error)
require.Equal(t, "git", resource.ResourceType)
require.Equal(t, "https://example.com/org/repo#main", resource.BindingScope)
require.Equal(t, "azkv://heicode-kv.vault.azure.net/secrets/repo", resource.SecretRef)
require.NotContains(t, resource.Metadata, "token")
require.NotContains(t, resource.PermissionScope, "token")
require.NotContains(t, resource.Constraints, "token")
}
func TestCreateResourceRejectsPlaintextSecretKeys(t *testing.T) {
setupResourceControllerTestDB(t)
body := `{
"name":"Cloud account",
"resource_type":"cloud_account",
"metadata":{"account_id":"sub-1","access_key":"do-not-store"},
"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/cloud-sub-1"
}`
w := performResourceRequest(CreateResource, 7, http.MethodPost, "/", body)
require.Equal(t, http.StatusOK, w.Code)
require.Contains(t, w.Body.String(), `"success":false`)
require.Contains(t, w.Body.String(), "plaintext secrets are not allowed")
}
func TestCreateResourceGrantAssignsBoundResourceToRoleAgnet(t *testing.T) {
db := setupResourceControllerTestDB(t)
resource := model.ResourceBinding{
UserId: 7,
BindingScope: "https://example.com/sk.git#main",
Name: "SK repo",
ResourceType: "sk",
Provider: "git",
SecretRef: "azkv://heicode-kv.vault.azure.net/secrets/sk-repo",
Metadata: `{"repo_url":"https://example.com/sk.git"}`,
Status: "active",
}
require.NoError(t, db.Create(&resource).Error)
body := fmt.Sprintf(`{
"binding_scope":"https://example.com/sk.git#main",
"resource_id":%d,
"role":"developer",
"agnet_id":"agnet-dev-1",
"permission_scope":{"actions":["read"]},
"constraints":{"paths":["skills/**"]}
}`, resource.Id)
w := performResourceRequest(CreateResourceGrant, 7, http.MethodPost, "/", body)
require.Equal(t, http.StatusOK, w.Code)
require.Contains(t, w.Body.String(), `"success":true`)
require.Contains(t, w.Body.String(), `"binding_scope":"https://example.com/sk.git#main"`)
require.Contains(t, w.Body.String(), `"role":"developer"`)
require.Contains(t, w.Body.String(), `"agnet_id":"agnet-dev-1"`)
var grant model.ResourceGrant
require.NoError(t, db.First(&grant).Error)
require.Equal(t, resource.Id, grant.ResourceId)
require.Equal(t, "https://example.com/sk.git#main", grant.BindingScope)
require.Equal(t, "developer", grant.Role)
require.Equal(t, "agnet-dev-1", grant.AgnetId)
}
func TestCreateResourceGrantRejectsMismatchedBindingScope(t *testing.T) {
db := setupResourceControllerTestDB(t)
resource := model.ResourceBinding{
UserId: 7,
BindingScope: "azure-vm-prod",
Name: "VM",
ResourceType: "cloud_resource",
Status: "active",
}
require.NoError(t, db.Create(&resource).Error)
body := fmt.Sprintf(`{
"binding_scope":"azure-vm-dev",
"resource_id":%d,
"role":"operator",
"agnet_id":"agnet-ops-1"
}`, resource.Id)
w := performResourceRequest(CreateResourceGrant, 7, http.MethodPost, "/", body)
require.Equal(t, http.StatusOK, w.Code)
require.Contains(t, w.Body.String(), `"success":false`)
require.Contains(t, w.Body.String(), "resource binding_scope does not match grant binding_scope")
}
func TestGenerateResourceGrantManifestIncludesActiveGrantsOnly(t *testing.T) {
db := setupResourceControllerTestDB(t)
resource := model.ResourceBinding{
UserId: 7,
BindingScope: "repo-main",
Name: "Project repository",
ResourceType: "git",
Provider: "github",
ExternalId: "https://example.com/org/repo.git",
SecretRef: "azkv://heicode-kv.vault.azure.net/secrets/repo-main",
Metadata: `{"repo_url":"https://example.com/org/repo.git"}`,
PermissionScope: `{"actions":["read","write"]}`,
Status: "active",
}
require.NoError(t, db.Create(&resource).Error)
require.NoError(t, db.Create(&model.ResourceGrant{
UserId: 7,
BindingScope: "repo-main",
ResourceId: resource.Id,
Role: "backend",
AgnetId: "agnet-backend-1",
PermissionScope: `{"actions":["read"]}`,
Constraints: `{"paths":["heicode/controller/**"]}`,
Status: "active",
}).Error)
require.NoError(t, db.Create(&model.ResourceGrant{
UserId: 7,
BindingScope: "repo-main",
ResourceId: resource.Id,
Role: "backend",
AgnetId: "agnet-backend-1",
PermissionScope: `{"actions":["write"]}`,
Status: "revoked",
}).Error)
w := performResourceRequestWithRoute(
GenerateResourceGrantManifest,
7,
http.MethodGet,
"/manifest",
"/manifest?binding_scope=repo-main&role=backend&agnet_id=agnet-backend-1",
"",
)
require.Equal(t, http.StatusOK, w.Code)
require.Contains(t, w.Body.String(), `"success":true`)
require.Contains(t, w.Body.String(), `"binding_scope":"repo-main"`)
require.Contains(t, w.Body.String(), `"agent_role":"backend"`)
require.Contains(t, w.Body.String(), `"target_agent_ref":"agnet-backend-1"`)
require.Contains(t, w.Body.String(), `"resource_type":"git"`)
require.Contains(t, w.Body.String(), `"allowed_actions":["read"]`)
require.Contains(t, w.Body.String(), `"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/repo-main"`)
require.NotContains(t, w.Body.String(), `"allowed_actions":["write"]`)
}
func TestUpsertResourceSecretWritesAzureKeyVaultAndStoresOnlySecretRef(t *testing.T) {
db := setupResourceControllerTestDB(t)
resource := model.ResourceBinding{
UserId: 7,
BindingScope: "github-org-repo-main",
Name: "GitHub",
ResourceType: "git",
Provider: "github",
Status: "active",
}
require.NoError(t, db.Create(&resource).Error)
tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodGet, r.Method)
require.Equal(t, "true", r.Header.Get("Metadata"))
require.Equal(t, "2018-02-01", r.URL.Query().Get("api-version"))
require.Equal(t, "https://vault.azure.net", r.URL.Query().Get("resource"))
_, _ = w.Write([]byte(`{"access_token":"manager-token","expires_in":"3600"}`))
}))
defer tokenServer.Close()
var writtenPath string
var writtenBody map[string]any
vaultServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPut, r.Method)
require.Equal(t, "Bearer manager-token", r.Header.Get("Authorization"))
require.Equal(t, "/secrets/users-7-bindings-github-org-repo-main-resources-1", r.URL.Path)
require.Equal(t, "7.4", r.URL.Query().Get("api-version"))
require.NoError(t, common.DecodeJson(r.Body, &writtenBody))
writtenPath = r.URL.Path
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"id":"https://heicode-kv.vault.azure.net/secrets/users-7-bindings-github-org-repo-main-resources-1/version"}`))
}))
defer vaultServer.Close()
t.Setenv("AZURE_KEY_VAULT_URL", vaultServer.URL)
t.Setenv("AZURE_MANAGED_IDENTITY_TOKEN_URL", tokenServer.URL)
body := `{"data":{"access_token":"do-not-echo","username":"octo"}}`
w := performResourceRequestWithRoute(UpsertResourceSecret, 7, http.MethodPost, "/:id/secret", "/1/secret", body)
require.Equal(t, http.StatusOK, w.Code)
require.Contains(t, w.Body.String(), `"success":true`)
require.Contains(t, w.Body.String(), `"secret_ref":"azkv://127.0.0.1`)
require.Contains(t, w.Body.String(), `/secrets/users-7-bindings-github-org-repo-main-resources-1"`)
require.NotContains(t, w.Body.String(), "do-not-echo")
require.Equal(t, "/secrets/users-7-bindings-github-org-repo-main-resources-1", writtenPath)
require.Equal(t, "application/json", writtenBody["contentType"])
require.JSONEq(t, `{"access_token":"do-not-echo","username":"octo"}`, writtenBody["value"].(string))
var stored model.ResourceBinding
require.NoError(t, db.First(&stored, resource.Id).Error)
require.Contains(t, stored.SecretRef, "azkv://127.0.0.1")
require.Contains(t, stored.SecretRef, "/secrets/users-7-bindings-github-org-repo-main-resources-1")
require.NotContains(t, stored.Metadata, "do-not-echo")
}
func TestUpsertResourceSecretRequiresAzureKeyVaultURL(t *testing.T) {
db := setupResourceControllerTestDB(t)
resource := model.ResourceBinding{
UserId: 7,
BindingScope: "github",
Name: "GitHub",
ResourceType: "git",
Status: "active",
}
require.NoError(t, db.Create(&resource).Error)
t.Setenv("AZURE_KEY_VAULT_URL", "")
w := performResourceRequestWithRoute(UpsertResourceSecret, 7, http.MethodPost, "/:id/secret", "/1/secret", `{"data":{"token":"x"}}`)
require.Equal(t, http.StatusOK, w.Code)
require.Contains(t, w.Body.String(), `"success":false`)
require.Contains(t, w.Body.String(), "AZURE_KEY_VAULT_URL is not configured")
}
func TestDiscoverAzureResourcesReadsKeyVaultSecretAndStoresCloudResources(t *testing.T) {
db := setupResourceControllerTestDB(t)
tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodGet, r.Method)
require.Equal(t, "https://vault.azure.net", r.URL.Query().Get("resource"))
_, _ = w.Write([]byte(`{"access_token":"kv-token"}`))
}))
defer tokenServer.Close()
vaultServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodGet, r.Method)
require.Equal(t, "Bearer kv-token", r.Header.Get("Authorization"))
require.Equal(t, "/secrets/azure-sub-1", r.URL.Path)
require.Equal(t, "7.4", r.URL.Query().Get("api-version"))
_, _ = w.Write([]byte(`{"value":"{\"client_secret\":\"sp-secret\"}"}`))
}))
defer vaultServer.Close()
vaultURL, err := url.Parse(vaultServer.URL)
require.NoError(t, err)
authorityServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
require.Equal(t, "/tenant-a/oauth2/v2.0/token", r.URL.Path)
require.NoError(t, r.ParseForm())
require.Equal(t, "client_credentials", r.Form.Get("grant_type"))
require.Equal(t, "client-a", r.Form.Get("client_id"))
require.Equal(t, "sp-secret", r.Form.Get("client_secret"))
require.Equal(t, "https://management.azure.com/.default", r.Form.Get("scope"))
_, _ = w.Write([]byte(`{"access_token":"arm-token"}`))
}))
defer authorityServer.Close()
armServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodGet, r.Method)
require.Equal(t, "Bearer arm-token", r.Header.Get("Authorization"))
require.Equal(t, "/subscriptions/sub-a/resources", r.URL.Path)
require.Equal(t, "2021-04-01", r.URL.Query().Get("api-version"))
_, _ = w.Write([]byte(`{
"value": [
{
"id": "/subscriptions/sub-a/resourceGroups/rg-app/providers/Microsoft.Compute/virtualMachines/vm-api-01",
"name": "vm-api-01",
"type": "Microsoft.Compute/virtualMachines",
"location": "eastus",
"tags": {"env": "test"}
}
]
}`))
}))
defer armServer.Close()
t.Setenv("AZURE_KEY_VAULT_URL", vaultServer.URL)
t.Setenv("AZURE_MANAGED_IDENTITY_TOKEN_URL", tokenServer.URL)
t.Setenv("AZURE_AUTHORITY_HOST", authorityServer.URL)
t.Setenv("AZURE_ARM_BASE_URL", armServer.URL)
account := model.ResourceBinding{
UserId: 7,
BindingScope: "azure:sub-a",
Name: "Test Azure",
ResourceType: "cloud_account",
Provider: "azure",
ExternalId: "sub-a",
SecretRef: fmt.Sprintf("azkv://%s/secrets/azure-sub-1", vaultURL.Host),
Metadata: `{"subscription_id":"sub-a","tenant_id":"tenant-a","client_id":"client-a"}`,
Status: "active",
}
require.NoError(t, db.Create(&account).Error)
w := performResourceRequestWithRoute(
DiscoverAzureResources,
7,
http.MethodPost,
"/:id/azure/discover",
"/1/azure/discover",
"",
)
require.Equal(t, http.StatusOK, w.Code)
require.Contains(t, w.Body.String(), `"success":true`)
require.Contains(t, w.Body.String(), `"resource_group":"rg-app"`)
require.Contains(t, w.Body.String(), `"type":"Microsoft.Compute/virtualMachines"`)
require.NotContains(t, w.Body.String(), "sp-secret")
var resources []model.ResourceBinding
require.NoError(t, db.Where("user_id = ? AND resource_type = ?", 7, "cloud_resource").Find(&resources).Error)
require.Len(t, resources, 1)
require.Equal(t, "vm-api-01", resources[0].Name)
require.Equal(t, "azure", resources[0].Provider)
require.Equal(t, "active", resources[0].Status)
require.Equal(t, account.SecretRef, resources[0].SecretRef)
require.Contains(t, resources[0].ExternalId, "Microsoft.Compute/virtualMachines/vm-api-01")
require.Contains(t, resources[0].Metadata, `"resource_group":"rg-app"`)
require.NotContains(t, resources[0].Metadata, "sp-secret")
}
func TestResourceSecretNameFitsAzureKeyVaultRules(t *testing.T) {
name := resourceSecretName(model.ResourceBinding{
Id: 99,
UserId: 7,
BindingScope: strings.Repeat("repo/with spaces/", 20),
Name: "fallback",
})
require.LessOrEqual(t, len(name), azureKeyVaultSecretNameMaxLen)
require.Regexp(t, `^[A-Za-z0-9-]+$`, name)
require.Contains(t, name, "users-7-bindings-")
require.Contains(t, name, "-resources-99")
}
func TestGetSecretStoreStatusReportsAzureKeyVaultReachable(t *testing.T) {
tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "true", r.Header.Get("Metadata"))
_, _ = w.Write([]byte(`{"access_token":"manager-token"}`))
}))
defer tokenServer.Close()
vaultServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodGet, r.Method)
require.Equal(t, "/secrets", r.URL.Path)
require.Equal(t, "Bearer manager-token", r.Header.Get("Authorization"))
_, _ = w.Write([]byte(`{"value":[]}`))
}))
defer vaultServer.Close()
t.Setenv("AZURE_KEY_VAULT_URL", vaultServer.URL)
t.Setenv("AZURE_MANAGED_IDENTITY_TOKEN_URL", tokenServer.URL)
r := gin.New()
r.GET("/status", GetSecretStoreStatus)
w := httptest.NewRecorder()
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/status", nil))
require.Equal(t, http.StatusOK, w.Code)
require.Contains(t, w.Body.String(), `"success":true`)
require.Contains(t, w.Body.String(), `"configured":true`)
require.Contains(t, w.Body.String(), `"reachable":true`)
require.Contains(t, w.Body.String(), `"provider":"azure_key_vault"`)
require.Contains(t, w.Body.String(), "Azure Key Vault is reachable")
}