375 lines
12 KiB
Go
375 lines
12 KiB
Go
package controller
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/heicode/manager/common"
|
|
"github.com/heicode/manager/model"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
var allowedResourceTypes = map[string]bool{
|
|
"git": true,
|
|
"sk": true,
|
|
"project_document": true,
|
|
"cloud_account": true,
|
|
"cloud_resource": true,
|
|
}
|
|
|
|
type resourceBindingPayload struct {
|
|
TenantId string `json:"tenant_id"`
|
|
ProjectId string `json:"project_id"`
|
|
ResourceType string `json:"resource_type"`
|
|
Name string `json:"name"`
|
|
Provider string `json:"provider"`
|
|
ResourceRef string `json:"resource_ref"`
|
|
Metadata map[string]any `json:"metadata"`
|
|
PermissionScope map[string]any `json:"permission_scope"`
|
|
Constraints map[string]any `json:"constraints"`
|
|
Status string `json:"status"`
|
|
SecretRef string `json:"secret_ref"`
|
|
Secret string `json:"secret"`
|
|
}
|
|
|
|
type resourceGrantPayload struct {
|
|
TenantId string `json:"tenant_id"`
|
|
ProjectId string `json:"project_id"`
|
|
ResourceId int `json:"resource_id"`
|
|
Role string `json:"role"`
|
|
SubAgnetId string `json:"sub_agnet_id"`
|
|
PermissionScope map[string]any `json:"permission_scope"`
|
|
Constraints map[string]any `json:"constraints"`
|
|
Status string `json:"status"`
|
|
AuditNote string `json:"audit_note"`
|
|
}
|
|
|
|
type resourceBindingResponse struct {
|
|
Id int `json:"id"`
|
|
UserId int `json:"user_id"`
|
|
TenantId string `json:"tenant_id"`
|
|
ProjectId string `json:"project_id"`
|
|
ResourceType string `json:"resource_type"`
|
|
Name string `json:"name"`
|
|
Provider string `json:"provider"`
|
|
ResourceRef string `json:"resource_ref"`
|
|
Metadata map[string]any `json:"metadata"`
|
|
PermissionScope map[string]any `json:"permission_scope"`
|
|
Constraints map[string]any `json:"constraints"`
|
|
Status string `json:"status"`
|
|
SecretRef string `json:"secret_ref,omitempty"`
|
|
CreatedAt int64 `json:"created_at"`
|
|
UpdatedAt int64 `json:"updated_at"`
|
|
}
|
|
|
|
type resourceGrantResponse struct {
|
|
Id int `json:"id"`
|
|
UserId int `json:"user_id"`
|
|
TenantId string `json:"tenant_id"`
|
|
ProjectId string `json:"project_id"`
|
|
ResourceId int `json:"resource_id"`
|
|
Role string `json:"role"`
|
|
SubAgnetId string `json:"sub_agnet_id"`
|
|
PermissionScope map[string]any `json:"permission_scope"`
|
|
Constraints map[string]any `json:"constraints"`
|
|
Status string `json:"status"`
|
|
AuditNote string `json:"audit_note"`
|
|
CreatedAt int64 `json:"created_at"`
|
|
UpdatedAt int64 `json:"updated_at"`
|
|
}
|
|
|
|
func normalizeResourceBindingPayload(p resourceBindingPayload) (resourceBindingPayload, error) {
|
|
p.TenantId = strings.TrimSpace(p.TenantId)
|
|
p.ProjectId = strings.TrimSpace(p.ProjectId)
|
|
p.ResourceType = strings.TrimSpace(p.ResourceType)
|
|
p.Name = strings.TrimSpace(p.Name)
|
|
p.Provider = strings.TrimSpace(p.Provider)
|
|
p.ResourceRef = strings.TrimSpace(p.ResourceRef)
|
|
p.Status = strings.TrimSpace(p.Status)
|
|
p.SecretRef = strings.TrimSpace(p.SecretRef)
|
|
p.Secret = strings.TrimSpace(p.Secret)
|
|
if p.TenantId == "" {
|
|
return p, errors.New("tenant_id required")
|
|
}
|
|
if p.ProjectId == "" {
|
|
return p, errors.New("project_id required")
|
|
}
|
|
if !allowedResourceTypes[p.ResourceType] {
|
|
return p, errors.New("resource_type must be one of git, sk, project_document, cloud_account, cloud_resource")
|
|
}
|
|
if p.Name == "" {
|
|
return p, errors.New("name required")
|
|
}
|
|
if p.ResourceRef == "" {
|
|
return p, errors.New("resource_ref required")
|
|
}
|
|
if p.Secret != "" {
|
|
return p, errors.New("secret plaintext is not accepted; store credentials in Secret Store and pass secret_ref")
|
|
}
|
|
if p.Provider == "" {
|
|
p.Provider = "custom"
|
|
}
|
|
if p.Status == "" {
|
|
p.Status = "active"
|
|
}
|
|
if p.Metadata == nil {
|
|
p.Metadata = map[string]any{}
|
|
}
|
|
if p.PermissionScope == nil {
|
|
p.PermissionScope = map[string]any{}
|
|
}
|
|
if p.Constraints == nil {
|
|
p.Constraints = map[string]any{}
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func normalizeResourceGrantPayload(p resourceGrantPayload) (resourceGrantPayload, error) {
|
|
p.TenantId = strings.TrimSpace(p.TenantId)
|
|
p.ProjectId = strings.TrimSpace(p.ProjectId)
|
|
p.Role = strings.TrimSpace(p.Role)
|
|
p.SubAgnetId = strings.TrimSpace(p.SubAgnetId)
|
|
p.Status = strings.TrimSpace(p.Status)
|
|
p.AuditNote = strings.TrimSpace(p.AuditNote)
|
|
if p.TenantId == "" {
|
|
return p, errors.New("tenant_id required")
|
|
}
|
|
if p.ProjectId == "" {
|
|
return p, errors.New("project_id required")
|
|
}
|
|
if p.ResourceId <= 0 {
|
|
return p, errors.New("resource_id required")
|
|
}
|
|
if p.Role == "" {
|
|
return p, errors.New("role required")
|
|
}
|
|
if p.Status == "" {
|
|
p.Status = "active"
|
|
}
|
|
if p.PermissionScope == nil {
|
|
p.PermissionScope = map[string]any{}
|
|
}
|
|
if p.Constraints == nil {
|
|
p.Constraints = map[string]any{}
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func marshalResourceMaps(metadata map[string]any, permissionScope map[string]any, constraints map[string]any) (string, string, string, error) {
|
|
metadataBytes, err := common.Marshal(metadata)
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
permissionBytes, err := common.Marshal(permissionScope)
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
constraintBytes, err := common.Marshal(constraints)
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
return string(metadataBytes), string(permissionBytes), string(constraintBytes), nil
|
|
}
|
|
|
|
func parseMapField(raw string) map[string]any {
|
|
out := map[string]any{}
|
|
if raw != "" {
|
|
_ = common.UnmarshalJsonStr(raw, &out)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func resourceBindingToResponse(resource model.ResourceBinding) resourceBindingResponse {
|
|
return resourceBindingResponse{
|
|
Id: resource.Id,
|
|
UserId: resource.UserId,
|
|
TenantId: resource.TenantId,
|
|
ProjectId: resource.ProjectId,
|
|
ResourceType: resource.ResourceType,
|
|
Name: resource.Name,
|
|
Provider: resource.Provider,
|
|
ResourceRef: resource.ResourceRef,
|
|
Metadata: parseMapField(resource.Metadata),
|
|
PermissionScope: parseMapField(resource.PermissionScope),
|
|
Constraints: parseMapField(resource.Constraints),
|
|
Status: resource.Status,
|
|
SecretRef: resource.SecretRef,
|
|
CreatedAt: resource.CreatedAt,
|
|
UpdatedAt: resource.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func resourceGrantToResponse(grant model.ResourceGrant) resourceGrantResponse {
|
|
return resourceGrantResponse{
|
|
Id: grant.Id,
|
|
UserId: grant.UserId,
|
|
TenantId: grant.TenantId,
|
|
ProjectId: grant.ProjectId,
|
|
ResourceId: grant.ResourceId,
|
|
Role: grant.Role,
|
|
SubAgnetId: grant.SubAgnetId,
|
|
PermissionScope: parseMapField(grant.PermissionScope),
|
|
Constraints: parseMapField(grant.Constraints),
|
|
Status: grant.Status,
|
|
AuditNote: grant.AuditNote,
|
|
CreatedAt: grant.CreatedAt,
|
|
UpdatedAt: grant.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func ListResources(c *gin.Context) {
|
|
userId := c.GetInt("id")
|
|
query := model.DB.Where("user_id = ?", userId)
|
|
if tenantId := strings.TrimSpace(c.Query("tenant_id")); tenantId != "" {
|
|
query = query.Where("tenant_id = ?", tenantId)
|
|
}
|
|
if projectId := strings.TrimSpace(c.Query("project_id")); projectId != "" {
|
|
query = query.Where("project_id = ?", projectId)
|
|
}
|
|
var resources []model.ResourceBinding
|
|
if err := query.Order("id desc").Find(&resources).Error; err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
items := make([]resourceBindingResponse, 0, len(resources))
|
|
for _, resource := range resources {
|
|
items = append(items, resourceBindingToResponse(resource))
|
|
}
|
|
common.ApiSuccess(c, gin.H{"items": items})
|
|
}
|
|
|
|
func CreateResource(c *gin.Context) {
|
|
userId := c.GetInt("id")
|
|
var payload resourceBindingPayload
|
|
if err := c.ShouldBindJSON(&payload); err != nil {
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "message": "invalid params"})
|
|
return
|
|
}
|
|
payload, err := normalizeResourceBindingPayload(payload)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
metadata, permissionScope, constraints, err := marshalResourceMaps(payload.Metadata, payload.PermissionScope, payload.Constraints)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
resource := model.ResourceBinding{
|
|
UserId: userId,
|
|
TenantId: payload.TenantId,
|
|
ProjectId: payload.ProjectId,
|
|
ResourceType: payload.ResourceType,
|
|
Name: payload.Name,
|
|
Provider: payload.Provider,
|
|
ResourceRef: payload.ResourceRef,
|
|
Metadata: metadata,
|
|
PermissionScope: permissionScope,
|
|
Constraints: constraints,
|
|
Status: payload.Status,
|
|
SecretRef: payload.SecretRef,
|
|
}
|
|
if err := model.DB.Create(&resource).Error; err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
common.ApiSuccess(c, resourceBindingToResponse(resource))
|
|
}
|
|
|
|
func DeleteResource(c *gin.Context) {
|
|
userId := c.GetInt("id")
|
|
res := model.DB.Where("id = ? AND user_id = ?", c.Param("id"), userId).Delete(&model.ResourceBinding{})
|
|
if res.Error != nil {
|
|
common.ApiError(c, res.Error)
|
|
return
|
|
}
|
|
if res.RowsAffected == 0 {
|
|
common.ApiErrorMsg(c, "resource not found")
|
|
return
|
|
}
|
|
common.ApiSuccess(c, gin.H{"deleted": true})
|
|
}
|
|
|
|
func ListResourceGrants(c *gin.Context) {
|
|
userId := c.GetInt("id")
|
|
query := model.DB.Where("user_id = ?", userId)
|
|
if tenantId := strings.TrimSpace(c.Query("tenant_id")); tenantId != "" {
|
|
query = query.Where("tenant_id = ?", tenantId)
|
|
}
|
|
if projectId := strings.TrimSpace(c.Query("project_id")); projectId != "" {
|
|
query = query.Where("project_id = ?", projectId)
|
|
}
|
|
var grants []model.ResourceGrant
|
|
if err := query.Order("id desc").Find(&grants).Error; err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
items := make([]resourceGrantResponse, 0, len(grants))
|
|
for _, grant := range grants {
|
|
items = append(items, resourceGrantToResponse(grant))
|
|
}
|
|
common.ApiSuccess(c, gin.H{"items": items})
|
|
}
|
|
|
|
func CreateResourceGrant(c *gin.Context) {
|
|
userId := c.GetInt("id")
|
|
var payload resourceGrantPayload
|
|
if err := c.ShouldBindJSON(&payload); err != nil {
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "message": "invalid params"})
|
|
return
|
|
}
|
|
payload, err := normalizeResourceGrantPayload(payload)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
var resource model.ResourceBinding
|
|
if err := model.DB.Where("id = ? AND user_id = ? AND tenant_id = ? AND project_id = ?", payload.ResourceId, userId, payload.TenantId, payload.ProjectId).First(&resource).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
common.ApiErrorMsg(c, "resource not found")
|
|
return
|
|
}
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
_, permissionScope, constraints, err := marshalResourceMaps(map[string]any{}, payload.PermissionScope, payload.Constraints)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
grant := model.ResourceGrant{
|
|
UserId: userId,
|
|
TenantId: payload.TenantId,
|
|
ProjectId: payload.ProjectId,
|
|
ResourceId: payload.ResourceId,
|
|
Role: payload.Role,
|
|
SubAgnetId: payload.SubAgnetId,
|
|
PermissionScope: permissionScope,
|
|
Constraints: constraints,
|
|
Status: payload.Status,
|
|
AuditNote: payload.AuditNote,
|
|
}
|
|
if err := model.DB.Create(&grant).Error; err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
common.ApiSuccess(c, resourceGrantToResponse(grant))
|
|
}
|
|
|
|
func DeleteResourceGrant(c *gin.Context) {
|
|
userId := c.GetInt("id")
|
|
res := model.DB.Where("id = ? AND user_id = ?", c.Param("id"), userId).Delete(&model.ResourceGrant{})
|
|
if res.Error != nil {
|
|
common.ApiError(c, res.Error)
|
|
return
|
|
}
|
|
if res.RowsAffected == 0 {
|
|
common.ApiErrorMsg(c, "resource grant not found")
|
|
return
|
|
}
|
|
common.ApiSuccess(c, gin.H{"deleted": true})
|
|
}
|