feat: wire resource secrets to OpenBao
Manager needs a platform-owned secret handoff path so resource bindings can keep only vault references while OpenBao stores tenant-scoped credential payloads. Tested: go test ./controller ./model ./router && go vet ./controller ./model ./router Co-authored-by: OmX <omx@oh-my-codex.dev>
This commit is contained in:
@@ -59,6 +59,10 @@ type resourcePayload struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type resourceSecretPayload struct {
|
||||
Data map[string]any `json:"data"`
|
||||
}
|
||||
|
||||
type resourceResponse struct {
|
||||
Id int `json:"id"`
|
||||
UserId int `json:"user_id"`
|
||||
@@ -413,6 +417,85 @@ func DeleteResource(c *gin.Context) {
|
||||
common.ApiSuccess(c, gin.H{"deleted": true})
|
||||
}
|
||||
|
||||
func UpsertResourceSecret(c *gin.Context) {
|
||||
userId := c.GetInt("id")
|
||||
var resource model.ResourceBinding
|
||||
if err := model.DB.Where("id = ? AND user_id = ?", c.Param("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 resourceSecretPayload
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "invalid params"})
|
||||
return
|
||||
}
|
||||
if len(payload.Data) == 0 {
|
||||
common.ApiErrorMsg(c, "secret data required")
|
||||
return
|
||||
}
|
||||
client, err := newSecretStoreClientFromEnv()
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
secretPath := resourceSecretPath(resource)
|
||||
if err := client.putKV2(secretPath, payload.Data); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
resource.SecretRef = fmt.Sprintf("vault://%s/%s", client.mount, secretPath)
|
||||
if err := model.DB.Save(&resource).Error; err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, gin.H{
|
||||
"resource": resourceToResponse(resource),
|
||||
"secret_ref": resource.SecretRef,
|
||||
})
|
||||
}
|
||||
|
||||
func resourceSecretPath(resource model.ResourceBinding) string {
|
||||
projectId := resource.ProjectId
|
||||
if strings.TrimSpace(projectId) == "" {
|
||||
projectId = "_tenant"
|
||||
}
|
||||
return strings.Join([]string{
|
||||
"tenants",
|
||||
sanitizeSecretPathSegment(resource.TenantId),
|
||||
"projects",
|
||||
sanitizeSecretPathSegment(projectId),
|
||||
"resources",
|
||||
fmt.Sprintf("%d", resource.Id),
|
||||
}, "/")
|
||||
}
|
||||
|
||||
func sanitizeSecretPathSegment(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return "_"
|
||||
}
|
||||
var b strings.Builder
|
||||
for _, r := range value {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
b.WriteRune(r)
|
||||
case r >= 'A' && r <= 'Z':
|
||||
b.WriteRune(r)
|
||||
case r >= '0' && r <= '9':
|
||||
b.WriteRune(r)
|
||||
case r == '_' || r == '-' || r == '.':
|
||||
b.WriteRune(r)
|
||||
default:
|
||||
b.WriteRune('-')
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func marshalResourcePayloadJSON(payload resourcePayload) (string, string, string, error) {
|
||||
metadata, err := marshalResourceJSON(payload.Metadata)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user