From a8be3980875a30a9dc69f2f6972e9797dd9cb291 Mon Sep 17 00:00:00 2001 From: gongzhiyong Date: Sat, 2 May 2026 23:28:33 +0800 Subject: [PATCH] omx(team): auto-checkpoint worker-4 [4] --- heicode/controller/resource_test.go | 140 ++++++++++++++++++++++++++++ heicode/model/main.go | 4 + heicode/router/api-router.go | 16 ++++ 3 files changed, 160 insertions(+) create mode 100644 heicode/controller/resource_test.go diff --git a/heicode/controller/resource_test.go b/heicode/controller/resource_test.go new file mode 100644 index 0000000..4aa98ba --- /dev/null +++ b/heicode/controller/resource_test.go @@ -0,0 +1,140 @@ +package controller + +import ( + "net/http" + "testing" + + "github.com/heicode/manager/common" + "github.com/heicode/manager/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupResourceControllerTestDB(t *testing.T) { + t.Helper() + db := openTokenControllerTestDB(t) + require.NoError(t, db.AutoMigrate(&model.ResourceBinding{}, &model.ResourceGrant{})) +} + +func decodeResourceBindingData(t *testing.T, raw []byte) resourceBindingResponse { + t.Helper() + var response tokenAPIResponse + require.NoError(t, common.Unmarshal(raw, &response)) + require.True(t, response.Success, response.Message) + var data resourceBindingResponse + require.NoError(t, common.Unmarshal(response.Data, &data)) + return data +} + +func decodeResourceGrantData(t *testing.T, raw []byte) resourceGrantResponse { + t.Helper() + var response tokenAPIResponse + require.NoError(t, common.Unmarshal(raw, &response)) + require.True(t, response.Success, response.Message) + var data resourceGrantResponse + require.NoError(t, common.Unmarshal(response.Data, &data)) + return data +} + +func TestCreateResourceAcceptsSecretRefOnly(t *testing.T) { + setupResourceControllerTestDB(t) + + body := map[string]any{ + "tenant_id": "tenant-a", + "project_id": "project-a", + "resource_type": "git", + "name": "main repo", + "provider": "github", + "resource_ref": "https://github.com/example/repo.git", + "metadata": map[string]any{ + "ref": "main", + }, + "permission_scope": map[string]any{ + "actions": []string{"read", "write"}, + }, + "constraints": map[string]any{ + "paths": []string{"src"}, + }, + "secret_ref": "vault://tenant-a/git/main-repo", + } + ctx, recorder := newAuthenticatedContext(t, http.MethodPost, "/api/resources/", body, 7) + + CreateResource(ctx) + + data := decodeResourceBindingData(t, recorder.Body.Bytes()) + assert.Equal(t, 7, data.UserId) + assert.Equal(t, "tenant-a", data.TenantId) + assert.Equal(t, "project-a", data.ProjectId) + assert.Equal(t, "git", data.ResourceType) + assert.Equal(t, "vault://tenant-a/git/main-repo", data.SecretRef) + assert.Equal(t, "main", data.Metadata["ref"]) + assert.NotContains(t, recorder.Body.String(), "plain-token") +} + +func TestCreateResourceRejectsPlaintextSecret(t *testing.T) { + setupResourceControllerTestDB(t) + + body := map[string]any{ + "tenant_id": "tenant-a", + "project_id": "project-a", + "resource_type": "cloud_account", + "name": "aws account", + "resource_ref": "arn:aws:organizations::123456789012:account/o-example/123456789012", + "secret": "plain-token", + } + ctx, recorder := newAuthenticatedContext(t, http.MethodPost, "/api/resources/", body, 7) + + CreateResource(ctx) + + var response tokenAPIResponse + require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response)) + assert.False(t, response.Success) + assert.Contains(t, response.Message, "secret plaintext is not accepted") + assert.NotContains(t, recorder.Body.String(), "plain-token") +} + +func TestCreateResourceGrantBindsProjectRoleAndSubAgnet(t *testing.T) { + setupResourceControllerTestDB(t) + + resource := model.ResourceBinding{ + UserId: 7, + TenantId: "tenant-a", + ProjectId: "project-a", + ResourceType: "cloud_resource", + Name: "staging vm", + Provider: "azure", + ResourceRef: "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm-a", + Metadata: "{}", + PermissionScope: "{}", + Constraints: "{}", + Status: "active", + SecretRef: "vault://tenant-a/azure/vm-a", + } + require.NoError(t, model.DB.Create(&resource).Error) + + body := map[string]any{ + "tenant_id": "tenant-a", + "project_id": "project-a", + "resource_id": resource.Id, + "role": "frontend-developer", + "sub_agnet_id": "agnet-worker-1", + "permission_scope": map[string]any{ + "actions": []string{"read", "deploy"}, + }, + "constraints": map[string]any{ + "environment": "staging", + }, + "audit_note": "grant for deployment smoke test", + } + ctx, recorder := newAuthenticatedContext(t, http.MethodPost, "/api/resource-grants/", body, 7) + + CreateResourceGrant(ctx) + + data := decodeResourceGrantData(t, recorder.Body.Bytes()) + assert.Equal(t, "tenant-a", data.TenantId) + assert.Equal(t, "project-a", data.ProjectId) + assert.Equal(t, resource.Id, data.ResourceId) + assert.Equal(t, "frontend-developer", data.Role) + assert.Equal(t, "agnet-worker-1", data.SubAgnetId) + assert.Equal(t, "staging", data.Constraints["environment"]) +} diff --git a/heicode/model/main.go b/heicode/model/main.go index c6825d6..e6fd180 100644 --- a/heicode/model/main.go +++ b/heicode/model/main.go @@ -281,6 +281,8 @@ func migrateDB() error { &CustomOAuthProvider{}, &UserOAuthBinding{}, &GitSource{}, + &ResourceBinding{}, + &ResourceGrant{}, ) if err != nil { return err @@ -330,6 +332,8 @@ func migrateDBFast() error { {&CustomOAuthProvider{}, "CustomOAuthProvider"}, {&UserOAuthBinding{}, "UserOAuthBinding"}, {&GitSource{}, "GitSource"}, + {&ResourceBinding{}, "ResourceBinding"}, + {&ResourceGrant{}, "ResourceGrant"}, } // 动态计算migration数量,确保errChan缓冲区足够大 errChan := make(chan error, len(migrations)) diff --git a/heicode/router/api-router.go b/heicode/router/api-router.go index 0f88e14..1fc9817 100644 --- a/heicode/router/api-router.go +++ b/heicode/router/api-router.go @@ -184,6 +184,22 @@ func SetApiRouter(router *gin.Engine) { gitSourceRoute.DELETE("/:id", controller.DeleteGitSource) } + resourceRoute := apiRouter.Group("/resources") + resourceRoute.Use(middleware.UserAuth()) + { + resourceRoute.GET("/", controller.ListResources) + resourceRoute.POST("/", controller.CreateResource) + resourceRoute.DELETE("/:id", controller.DeleteResource) + } + + resourceGrantRoute := apiRouter.Group("/resource-grants") + resourceGrantRoute.Use(middleware.UserAuth()) + { + resourceGrantRoute.GET("/", controller.ListResourceGrants) + resourceGrantRoute.POST("/", controller.CreateResourceGrant) + resourceGrantRoute.DELETE("/:id", controller.DeleteResourceGrant) + } + optionRoute := apiRouter.Group("/option") optionRoute.Use(middleware.RootAuth()) {