458 lines
14 KiB
Go
458 lines
14 KiB
Go
package controller
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/heicode/manager/common"
|
|
"github.com/heicode/manager/model"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type resourcePayload struct {
|
|
TenantId string `json:"tenant_id"`
|
|
ProjectId string `json:"project_id"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Provider string `json:"provider"`
|
|
Target string `json:"target"`
|
|
Metadata map[string]any `json:"metadata"`
|
|
Scopes []string `json:"scopes"`
|
|
Constraints map[string]any `json:"constraints"`
|
|
SecretRef string `json:"secret_ref"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
type resourceResponse struct {
|
|
Id int `json:"id"`
|
|
UserId int `json:"user_id"`
|
|
TenantId string `json:"tenant_id"`
|
|
ProjectId string `json:"project_id"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Provider string `json:"provider"`
|
|
Target string `json:"target"`
|
|
Metadata map[string]any `json:"metadata"`
|
|
Scopes []string `json:"scopes"`
|
|
Constraints map[string]any `json:"constraints"`
|
|
SecretRef string `json:"secret_ref"`
|
|
Status string `json:"status"`
|
|
CreatedAt int64 `json:"created_at"`
|
|
UpdatedAt int64 `json:"updated_at"`
|
|
}
|
|
|
|
type resourceGrantPayload struct {
|
|
TenantId string `json:"tenant_id"`
|
|
ProjectId string `json:"project_id"`
|
|
ResourceId int `json:"resource_id"`
|
|
AgnetId string `json:"agnet_id"`
|
|
Role string `json:"role"`
|
|
Scopes []string `json:"scopes"`
|
|
Constraints map[string]any `json:"constraints"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
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"`
|
|
AgnetId string `json:"agnet_id"`
|
|
Role string `json:"role"`
|
|
Scopes []string `json:"scopes"`
|
|
Constraints map[string]any `json:"constraints"`
|
|
Status string `json:"status"`
|
|
CreatedAt int64 `json:"created_at"`
|
|
UpdatedAt int64 `json:"updated_at"`
|
|
}
|
|
|
|
var allowedResourceTypes = map[string]bool{
|
|
model.ResourceTypeGit: true,
|
|
model.ResourceTypeSK: true,
|
|
model.ResourceTypeProjectDocument: true,
|
|
model.ResourceTypeCloudAccount: true,
|
|
model.ResourceTypeCloudResource: true,
|
|
}
|
|
|
|
func normalizeResourcePayload(p resourcePayload) (resourcePayload, error) {
|
|
p.TenantId = strings.TrimSpace(p.TenantId)
|
|
p.ProjectId = strings.TrimSpace(p.ProjectId)
|
|
p.Name = strings.TrimSpace(p.Name)
|
|
p.Type = strings.TrimSpace(p.Type)
|
|
p.Provider = strings.TrimSpace(p.Provider)
|
|
p.Target = strings.TrimSpace(p.Target)
|
|
p.SecretRef = strings.TrimSpace(p.SecretRef)
|
|
p.Status = strings.TrimSpace(p.Status)
|
|
|
|
if p.TenantId == "" {
|
|
return p, errors.New("tenant_id required")
|
|
}
|
|
if p.ProjectId == "" {
|
|
return p, errors.New("project_id required")
|
|
}
|
|
if p.Name == "" {
|
|
return p, errors.New("name required")
|
|
}
|
|
if !allowedResourceTypes[p.Type] {
|
|
return p, errors.New("type must be one of git, sk, project_document, cloud_account, cloud_resource")
|
|
}
|
|
if p.Target == "" {
|
|
return p, errors.New("target required")
|
|
}
|
|
if p.Provider == "" {
|
|
p.Provider = "custom"
|
|
}
|
|
if p.Status == "" {
|
|
p.Status = "active"
|
|
}
|
|
if err := rejectSecretLikeFields("metadata", p.Metadata); err != nil {
|
|
return p, err
|
|
}
|
|
if err := rejectSecretLikeFields("constraints", p.Constraints); err != nil {
|
|
return p, err
|
|
}
|
|
p.Scopes = normalizeStringList(p.Scopes)
|
|
return p, nil
|
|
}
|
|
|
|
func normalizeResourceGrantPayload(p resourceGrantPayload) (resourceGrantPayload, error) {
|
|
p.TenantId = strings.TrimSpace(p.TenantId)
|
|
p.ProjectId = strings.TrimSpace(p.ProjectId)
|
|
p.AgnetId = strings.TrimSpace(p.AgnetId)
|
|
p.Role = strings.TrimSpace(p.Role)
|
|
p.Status = strings.TrimSpace(p.Status)
|
|
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.AgnetId == "" {
|
|
return p, errors.New("agnet_id required")
|
|
}
|
|
if p.Role == "" {
|
|
return p, errors.New("role required")
|
|
}
|
|
if p.Status == "" {
|
|
p.Status = "active"
|
|
}
|
|
if err := rejectSecretLikeFields("constraints", p.Constraints); err != nil {
|
|
return p, err
|
|
}
|
|
p.Scopes = normalizeStringList(p.Scopes)
|
|
return p, nil
|
|
}
|
|
|
|
func normalizeStringList(values []string) []string {
|
|
items := make([]string, 0, len(values))
|
|
seen := map[string]bool{}
|
|
for _, value := range values {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" || seen[value] {
|
|
continue
|
|
}
|
|
seen[value] = true
|
|
items = append(items, value)
|
|
}
|
|
return items
|
|
}
|
|
|
|
func rejectSecretLikeFields(path string, value any) error {
|
|
switch typed := value.(type) {
|
|
case nil:
|
|
return nil
|
|
case map[string]any:
|
|
for key, nested := range typed {
|
|
if isSecretLikeKey(key) {
|
|
return fmt.Errorf("%s.%s must not contain raw secret material", path, key)
|
|
}
|
|
if err := rejectSecretLikeFields(path+"."+key, nested); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case []any:
|
|
for i, nested := range typed {
|
|
if err := rejectSecretLikeFields(fmt.Sprintf("%s[%d]", path, i), nested); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isSecretLikeKey(key string) bool {
|
|
key = strings.ToLower(strings.ReplaceAll(strings.TrimSpace(key), "-", "_"))
|
|
secretKeys := []string{"token", "secret", "password", "private_key", "access_key", "refresh_token", "client_secret"}
|
|
for _, secretKey := range secretKeys {
|
|
if strings.Contains(key, secretKey) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func marshalResourcePayload(p resourcePayload) (metadata, scopes, constraints string, err error) {
|
|
metadataBytes, err := common.Marshal(defaultMap(p.Metadata))
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
scopeBytes, err := common.Marshal(p.Scopes)
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
constraintBytes, err := common.Marshal(defaultMap(p.Constraints))
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
return string(metadataBytes), string(scopeBytes), string(constraintBytes), nil
|
|
}
|
|
|
|
func marshalResourceGrantPayload(p resourceGrantPayload) (scopes, constraints string, err error) {
|
|
scopeBytes, err := common.Marshal(p.Scopes)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
constraintBytes, err := common.Marshal(defaultMap(p.Constraints))
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
return string(scopeBytes), string(constraintBytes), nil
|
|
}
|
|
|
|
func defaultMap(value map[string]any) map[string]any {
|
|
if value == nil {
|
|
return map[string]any{}
|
|
}
|
|
return value
|
|
}
|
|
|
|
func resourceToResponse(src model.Resource) resourceResponse {
|
|
metadata := map[string]any{}
|
|
constraints := map[string]any{}
|
|
var scopes []string
|
|
if src.Metadata != "" {
|
|
_ = common.UnmarshalJsonStr(src.Metadata, &metadata)
|
|
}
|
|
if src.Scopes != "" {
|
|
_ = common.UnmarshalJsonStr(src.Scopes, &scopes)
|
|
}
|
|
if src.Constraints != "" {
|
|
_ = common.UnmarshalJsonStr(src.Constraints, &constraints)
|
|
}
|
|
if scopes == nil {
|
|
scopes = []string{}
|
|
}
|
|
return resourceResponse{Id: src.Id, UserId: src.UserId, TenantId: src.TenantId, ProjectId: src.ProjectId, Name: src.Name, Type: src.Type, Provider: src.Provider, Target: src.Target, Metadata: metadata, Scopes: scopes, Constraints: constraints, SecretRef: src.SecretRef, Status: src.Status, CreatedAt: src.CreatedAt, UpdatedAt: src.UpdatedAt}
|
|
}
|
|
|
|
func resourceGrantToResponse(grant model.ResourceGrant) resourceGrantResponse {
|
|
constraints := map[string]any{}
|
|
var scopes []string
|
|
if grant.Scopes != "" {
|
|
_ = common.UnmarshalJsonStr(grant.Scopes, &scopes)
|
|
}
|
|
if grant.Constraints != "" {
|
|
_ = common.UnmarshalJsonStr(grant.Constraints, &constraints)
|
|
}
|
|
if scopes == nil {
|
|
scopes = []string{}
|
|
}
|
|
return resourceGrantResponse{Id: grant.Id, UserId: grant.UserId, TenantId: grant.TenantId, ProjectId: grant.ProjectId, ResourceId: grant.ResourceId, AgnetId: grant.AgnetId, Role: grant.Role, Scopes: scopes, Constraints: constraints, Status: grant.Status, 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.Resource
|
|
if err := query.Order("id desc").Find(&resources).Error; err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
items := make([]resourceResponse, 0, len(resources))
|
|
for _, src := range resources {
|
|
items = append(items, resourceToResponse(src))
|
|
}
|
|
common.ApiSuccess(c, gin.H{"items": items})
|
|
}
|
|
|
|
func CreateResource(c *gin.Context) {
|
|
userId := c.GetInt("id")
|
|
var payload resourcePayload
|
|
if err := c.ShouldBindJSON(&payload); err != nil {
|
|
common.ApiErrorMsg(c, "invalid params")
|
|
return
|
|
}
|
|
payload, err := normalizeResourcePayload(payload)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
metadata, scopes, constraints, err := marshalResourcePayload(payload)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
resource := model.Resource{UserId: userId, TenantId: payload.TenantId, ProjectId: payload.ProjectId, Name: payload.Name, Type: payload.Type, Provider: payload.Provider, Target: payload.Target, Metadata: metadata, Scopes: scopes, Constraints: constraints, SecretRef: payload.SecretRef, Status: payload.Status}
|
|
if err := model.DB.Create(&resource).Error; err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
common.ApiSuccess(c, resourceToResponse(resource))
|
|
}
|
|
|
|
func UpdateResource(c *gin.Context) {
|
|
userId := c.GetInt("id")
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil || id <= 0 {
|
|
common.ApiErrorMsg(c, "invalid resource id")
|
|
return
|
|
}
|
|
var resource model.Resource
|
|
if err := model.DB.Where("id = ? AND user_id = ?", id, userId).First(&resource).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
common.ApiErrorMsg(c, "resource not found")
|
|
return
|
|
}
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
var payload resourcePayload
|
|
if err := c.ShouldBindJSON(&payload); err != nil {
|
|
common.ApiErrorMsg(c, "invalid params")
|
|
return
|
|
}
|
|
payload, err = normalizeResourcePayload(payload)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
metadata, scopes, constraints, err := marshalResourcePayload(payload)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
resource.TenantId = payload.TenantId
|
|
resource.ProjectId = payload.ProjectId
|
|
resource.Name = payload.Name
|
|
resource.Type = payload.Type
|
|
resource.Provider = payload.Provider
|
|
resource.Target = payload.Target
|
|
resource.Metadata = metadata
|
|
resource.Scopes = scopes
|
|
resource.Constraints = constraints
|
|
resource.SecretRef = payload.SecretRef
|
|
resource.Status = payload.Status
|
|
if err := model.DB.Save(&resource).Error; err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
common.ApiSuccess(c, resourceToResponse(resource))
|
|
}
|
|
|
|
func DeleteResource(c *gin.Context) {
|
|
userId := c.GetInt("id")
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil || id <= 0 {
|
|
common.ApiErrorMsg(c, "invalid resource id")
|
|
return
|
|
}
|
|
res := model.DB.Where("id = ? AND user_id = ?", id, userId).Delete(&model.Resource{})
|
|
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 {
|
|
common.ApiErrorMsg(c, "invalid params")
|
|
return
|
|
}
|
|
payload, err := normalizeResourceGrantPayload(payload)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
var resource model.Resource
|
|
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
|
|
}
|
|
scopes, constraints, err := marshalResourceGrantPayload(payload)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
grant := model.ResourceGrant{UserId: userId, TenantId: payload.TenantId, ProjectId: payload.ProjectId, ResourceId: payload.ResourceId, AgnetId: payload.AgnetId, Role: payload.Role, Scopes: scopes, Constraints: constraints, Status: payload.Status}
|
|
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")
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil || id <= 0 {
|
|
common.ApiErrorMsg(c, "invalid grant id")
|
|
return
|
|
}
|
|
res := model.DB.Where("id = ? AND user_id = ?", 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})
|
|
}
|