feat(backend): implement GetAccountCredentials2 API with ACL-based permission check

This commit is contained in:
pycook
2025-08-12 17:29:19 +08:00
parent ae4e8fb51f
commit 2dc6112d6c
5 changed files with 189 additions and 14 deletions

View File

@@ -154,28 +154,69 @@ func (c *Controller) GetAccountCredentials(ctx *gin.Context) {
return
}
// Build query with authorization check
db, err := accountService.BuildQueryWithAuthorization(ctx)
account, err := accountService.GetAccountCredentials(ctx, accountId)
if err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{
Code: myErrors.ErrInternal,
Data: map[string]any{"err": err},
if err.Error() == "account not found" {
ctx.AbortWithError(http.StatusNotFound, &myErrors.ApiError{
Data: map[string]any{"err": "Account not found"},
})
} else if err.Error() == "permission denied" {
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{
Code: myErrors.ErrNoPerm,
Data: map[string]any{"perm": acl.READ},
})
} else {
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{
Code: myErrors.ErrInternal,
Data: map[string]any{"err": err.Error()},
})
}
return
}
ctx.JSON(http.StatusOK, HttpResponse{
Data: account,
})
}
// GetAccountCredentials2 godoc
//
// @Tags account
// @Summary Get account credentials with authorization check only
// @Param id path int true "Account ID"
// @Success 200 {object} HttpResponse{data=model.Account}
// @Router /account/{id}/credentials2 [get]
func (c *Controller) GetAccountCredentials2(ctx *gin.Context) {
// Get account ID from path parameter
accountId := cast.ToInt(ctx.Param("id"))
if accountId == 0 {
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{
Code: myErrors.ErrInvalidArgument,
Data: map[string]any{"err": "Invalid account ID"},
})
return
}
// Query for the specific account with all fields (including sensitive ones)
var account model.Account
if err := db.Where("id = ?", accountId).First(&account).Error; err != nil {
ctx.AbortWithError(http.StatusNotFound, &myErrors.ApiError{
Data: map[string]any{"err": "Account not found or access denied"},
})
account, err := accountService.GetAccountCredentials(ctx, accountId)
if err != nil {
if err.Error() == "account not found" {
ctx.AbortWithError(http.StatusNotFound, &myErrors.ApiError{
Data: map[string]any{"err": "Account not found"},
})
} else if err.Error() == "permission denied" {
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{
Code: myErrors.ErrNoPerm,
Data: map[string]any{"perm": acl.READ},
})
} else {
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{
Code: myErrors.ErrInternal,
Data: map[string]any{"err": err.Error()},
})
}
return
}
// Decrypt sensitive data before returning
accountService.DecryptSensitiveData([]*model.Account{&account})
ctx.JSON(http.StatusOK, HttpResponse{
Data: account,
})

View File

@@ -232,6 +232,43 @@ const docTemplate = `{
}
}
},
"/account/{id}/credentials2": {
"get": {
"tags": [
"account"
],
"summary": "Get account credentials with authorization check only",
"parameters": [
{
"type": "integer",
"description": "Account ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/controller.HttpResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/model.Account"
}
}
}
]
}
}
}
}
},
"/asset": {
"get": {
"tags": [

View File

@@ -221,6 +221,43 @@
}
}
},
"/account/{id}/credentials2": {
"get": {
"tags": [
"account"
],
"summary": "Get account credentials with authorization check only",
"parameters": [
{
"type": "integer",
"description": "Account ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/controller.HttpResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/model.Account"
}
}
}
]
}
}
}
}
},
"/asset": {
"get": {
"tags": [

View File

@@ -1245,6 +1245,27 @@ paths:
summary: Get account credentials with MFA verification
tags:
- account
/account/{id}/credentials2:
get:
parameters:
- description: Account ID
in: path
name: id
required: true
type: integer
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/controller.HttpResponse'
- properties:
data:
$ref: '#/definitions/model.Account'
type: object
summary: Get account credentials with authorization check only
tags:
- account
/asset:
get:
parameters:

View File

@@ -2,11 +2,13 @@ package service
import (
"context"
"errors"
"github.com/gin-gonic/gin"
"github.com/veops/oneterm/internal/acl"
"github.com/veops/oneterm/internal/model"
"github.com/veops/oneterm/internal/repository"
"github.com/veops/oneterm/pkg/config"
"github.com/veops/oneterm/pkg/utils"
"golang.org/x/crypto/ssh"
"gorm.io/gorm"
@@ -104,3 +106,40 @@ func (s *AccountService) BuildQueryWithAuthorization(ctx *gin.Context) (*gorm.DB
return db, nil
}
// GetAccountCredentials gets account credentials with ACL permission check
func (s *AccountService) GetAccountCredentials(ctx *gin.Context, accountId int) (*model.Account, error) {
// Get current user info
currentUser, err := acl.GetSessionFromCtx(ctx)
if err != nil {
return nil, err
}
// First get the account to check if it exists
var account model.Account
baseRepo := repository.NewBaseRepository()
if err := baseRepo.GetById(ctx, accountId, &account); err != nil {
return nil, errors.New("account not found")
}
if acl.IsAdmin(currentUser) {
// Decrypt sensitive data before returning
s.DecryptSensitiveData([]*model.Account{&account})
return &account, nil
}
// Check if user has read permission for this account resource
hasPermission, err := acl.HasPermission(ctx, currentUser.GetRid(), config.RESOURCE_ACCOUNT, account.ResourceId, acl.READ)
if err != nil {
return nil, err
}
if !hasPermission {
return nil, errors.New("permission denied")
}
// Decrypt sensitive data before returning
s.DecryptSensitiveData([]*model.Account{&account})
return &account, nil
}