- add Manager user_context, NewAPI billing_context, and Agnet agent_runtime deployment fields - move resource binding/grant scope toward user-owned binding_scope and secret_ref-only paths - document OpenBao internal access and unified heicode.xinghanlab.com routing boundaries - fix Manager session user id preservation after external auth login
231 lines
8.2 KiB
Go
231 lines
8.2 KiB
Go
package controller
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"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":"vault://secret/resources/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":"vault://secret/resources/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, "vault://secret/resources/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":"vault://secret/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: "vault://secret/resources/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 TestUpsertResourceSecretWritesOpenBaoAndStoresOnlySecretRef(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)
|
|
|
|
var writtenPath string
|
|
var writtenBody map[string]map[string]any
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, http.MethodPost, r.Method)
|
|
require.Equal(t, "/v1/secret/data/users/7/bindings/github-org-repo-main/resources/1", r.URL.Path)
|
|
require.Equal(t, "manager-token", r.Header.Get("X-Vault-Token"))
|
|
require.NoError(t, common.DecodeJson(r.Body, &writtenBody))
|
|
writtenPath = r.URL.Path
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`{"data":{"version":1}}`))
|
|
}))
|
|
defer server.Close()
|
|
t.Setenv("OPENBAO_ADDR", server.URL)
|
|
t.Setenv("OPENBAO_TOKEN", "manager-token")
|
|
t.Setenv("OPENBAO_KV_MOUNT", "secret")
|
|
|
|
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":"vault://secret/users/7/bindings/github-org-repo-main/resources/1"`)
|
|
require.NotContains(t, w.Body.String(), "do-not-echo")
|
|
require.Equal(t, "/v1/secret/data/users/7/bindings/github-org-repo-main/resources/1", writtenPath)
|
|
require.Equal(t, "do-not-echo", writtenBody["data"]["access_token"])
|
|
|
|
var stored model.ResourceBinding
|
|
require.NoError(t, db.First(&stored, resource.Id).Error)
|
|
require.Equal(t, "vault://secret/users/7/bindings/github-org-repo-main/resources/1", stored.SecretRef)
|
|
require.NotContains(t, stored.Metadata, "do-not-echo")
|
|
}
|
|
|
|
func TestUpsertResourceSecretRequiresSecretStoreToken(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("OPENBAO_ADDR", "http://127.0.0.1:8200")
|
|
t.Setenv("OPENBAO_TOKEN", "")
|
|
t.Setenv("VAULT_TOKEN", "")
|
|
t.Setenv("OPENBAO_TOKEN_FILE", "")
|
|
t.Setenv("VAULT_TOKEN_FILE", "")
|
|
|
|
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(), "secret store token is not configured")
|
|
}
|