Files
oneterm/backend/internal/api/controller/base.go

526 lines
14 KiB
Go

package controller
import (
"context"
"errors"
"net/http"
"reflect"
"time"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"github.com/spf13/cast"
"golang.org/x/sync/errgroup"
"gorm.io/gorm"
"github.com/veops/oneterm/internal/acl"
"github.com/veops/oneterm/internal/model"
"github.com/veops/oneterm/internal/repository"
"github.com/veops/oneterm/internal/service"
"github.com/veops/oneterm/pkg/config"
myErrors "github.com/veops/oneterm/pkg/errors"
"github.com/veops/oneterm/pkg/remote"
)
var (
defaultHttpResponse = &HttpResponse{
Code: 0,
Message: "ok",
Data: nil,
}
)
type preHook[T any] func(*gin.Context, T)
type postHook[T any] func(*gin.Context, []T)
type deleteCheck func(*gin.Context, int)
type Controller struct {
baseService service.BaseService
historyService *service.HistoryService
}
func NewController() *Controller {
return &Controller{
baseService: service.NewBaseService(),
historyService: service.NewHistoryService(),
}
}
type HttpResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data"`
}
type ListData struct {
Count int64 `json:"count"`
List []any `json:"list"`
}
func NewHttpResponseWithData(data any) *HttpResponse {
return &HttpResponse{
Code: 0,
Message: "ok",
Data: data,
}
}
func doCreate[T model.Model](ctx *gin.Context, needAcl bool, md T, resourceType string, preHooks ...preHook[T]) (err error) {
defer repository.DeleteAllFromCacheDb(ctx, md)
currentUser, _ := acl.GetSessionFromCtx(ctx)
baseService := service.NewBaseService()
historyService := service.NewHistoryService()
if err = ctx.ShouldBindBodyWithJSON(md); err != nil {
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
for _, hook := range preHooks {
if hook == nil {
continue
}
hook(ctx, md)
if ctx.IsAborted() {
return
}
}
resourceId := 0
if needAcl {
_, ok := any(md).(*model.Node)
resourceId, err = acl.CreateGrantAcl(ctx, currentUser, resourceType, md.GetName()+lo.Ternary(ok, time.Now().Format(time.RFC3339), ""))
if err != nil {
handleRemoteErr(ctx, err)
return
}
md.SetResourceId(resourceId)
}
md.SetCreatorId(currentUser.Uid)
md.SetUpdaterId(currentUser.Uid)
if err = baseService.ExecuteInTransaction(ctx, func(tx *gorm.DB) (err error) {
if err = tx.Model(md).Create(md).Error; err != nil {
return
}
switch t := any(md).(type) {
case *model.Asset:
if err = handleAuthorization(ctx, tx, model.ACTION_CREATE, t, nil); err != nil {
handleRemoteErr(ctx, err)
return
}
case *model.Node:
if err = acl.UpdateResource(ctx, currentUser.GetUid(), resourceId, map[string]string{"name": cast.ToString(md.GetId())}); err != nil {
handleRemoteErr(ctx, err)
return
}
}
// Create history using history service
err = historyService.CreateAndSaveHistory(ctx, model.ACTION_CREATE, md, nil, currentUser.Uid)
return
}); err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrDuplicateName, Data: map[string]any{"err": err}})
return
}
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
return
}
ctx.JSON(http.StatusOK, HttpResponse{
Data: map[string]any{
"id": md.GetId(),
},
})
return
}
func doDelete[T model.Model](ctx *gin.Context, needAcl bool, md T, resourceType string, dcs ...deleteCheck) (err error) {
defer repository.DeleteAllFromCacheDb(ctx, md)
currentUser, _ := acl.GetSessionFromCtx(ctx)
baseService := service.NewBaseService()
historyService := service.NewHistoryService()
id, err := cast.ToIntE(ctx.Param("id"))
if err != nil {
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
// Use service to get model by ID
if err = baseService.GetById(ctx, id, md); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
ctx.JSON(http.StatusOK, HttpResponse{
Data: map[string]any{
"id": md.GetId(),
},
})
return
}
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
return
}
if needAcl && !hasPerm(ctx, md, resourceType, acl.DELETE) {
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.DELETE}})
return
}
for _, dc := range dcs {
if dc == nil {
continue
}
dc(ctx, id)
if ctx.IsAborted() {
return
}
}
if needAcl {
if err = acl.DeleteResource(ctx, currentUser.GetUid(), md.GetResourceId()); err != nil {
handleRemoteErr(ctx, err)
return
}
}
if err = baseService.ExecuteInTransaction(ctx, func(tx *gorm.DB) (err error) {
switch t := any(md).(type) {
case *model.Asset:
if err = handleAuthorization(ctx, tx, model.ACTION_DELETE, t, nil, nil); err != nil {
handleRemoteErr(ctx, err)
return
}
}
if err = tx.Delete(md, id).Error; err != nil {
return
}
// Create history using history service
err = historyService.CreateAndSaveHistory(ctx, model.ACTION_DELETE, md, nil, currentUser.Uid)
return
}); err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrDuplicateName, Data: map[string]any{"err": err}})
return
}
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
return
}
ctx.JSON(http.StatusOK, HttpResponse{
Data: map[string]any{
"id": md.GetId(),
},
})
return
}
func doUpdate[T model.Model](ctx *gin.Context, needAcl bool, md T, resourceType string, preHooks ...preHook[T]) (err error) {
defer repository.DeleteAllFromCacheDb(ctx, md)
currentUser, _ := acl.GetSessionFromCtx(ctx)
baseService := service.NewBaseService()
historyService := service.NewHistoryService()
id, err := cast.ToIntE(ctx.Param("id"))
if err != nil {
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
if err = ctx.ShouldBindBodyWithJSON(md); err != nil {
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
md.SetUpdaterId(currentUser.Uid)
for _, hook := range preHooks {
if hook == nil {
continue
}
hook(ctx, md)
if ctx.IsAborted() {
return
}
}
old := getEmpty(md)
if err = baseService.GetById(ctx, id, old); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
ctx.JSON(http.StatusOK, defaultHttpResponse)
return
}
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
return
}
if needAcl {
md.SetResourceId(old.GetResourceId())
if !hasPerm(ctx, md, resourceType, acl.WRITE) {
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
return
}
_, ok := any(md).(*model.Node)
if err = acl.UpdateResource(ctx, currentUser.GetUid(), old.GetResourceId(),
map[string]string{"name": lo.Ternary(ok, cast.ToString(md.GetId()), md.GetName())}); err != nil {
handleRemoteErr(ctx, err)
return
}
}
md.SetId(id)
if err = baseService.ExecuteInTransaction(ctx, func(tx *gorm.DB) (err error) {
omits := []string{"resource_id", "created_at", "deleted_at"}
selects := []string{"*"}
switch t := any(md).(type) {
case *model.Asset:
if err = handleAuthorization(ctx, tx, model.ACTION_UPDATE, t, nil); err != nil {
handleRemoteErr(ctx, err)
return
}
if cast.ToBool(ctx.Value("isAuthWithKey")) {
selects = []string{"ip", "protocols", "authorization"}
}
case *model.Account:
if cast.ToBool(ctx.Value("isAuthWithKey")) {
selects = []string{"account", "password", "phrase", "pk", "account_type"}
}
}
if err = tx.Select(selects).Omit(omits...).Save(md).Error; err != nil {
return
}
// Create history using history service
err = historyService.CreateAndSaveHistory(ctx, model.ACTION_UPDATE, md, old, currentUser.Uid)
return
}); err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrDuplicateName, Data: map[string]any{"err": err}})
return
}
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
return
}
ctx.JSON(http.StatusOK, HttpResponse{
Data: map[string]any{
"id": md.GetId(),
},
})
return
}
func doGet[T any](ctx *gin.Context, needAcl bool, dbFind *gorm.DB, resourceType string, postHooks ...postHook[T]) (err error) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if needAcl && !acl.IsAdmin(currentUser) {
if dbFind, err = handleAcl[T](ctx, dbFind, resourceType); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
return
}
}
dbCount := dbFind.Session(&gorm.Session{})
dbFind = dbFind.Session(&gorm.Session{})
count, list := int64(0), make([]T, 0)
eg := &errgroup.Group{}
eg.Go(func() error {
return dbCount.Count(&count).
Error
})
eg.Go(func() error {
pi, ps := cast.ToInt(ctx.Query("page_index")), cast.ToInt(ctx.Query("page_size"))
if _, ok := ctx.GetQuery("page_index"); ok {
dbFind = dbFind.Offset((pi - 1) * ps)
}
if _, ok := ctx.GetQuery("page_size"); ok && ps != -1 {
dbFind = dbFind.Limit(lo.Ternary(ps == 0, 20, ps))
}
return dbFind.
Order("id DESC").
Find(&list).
Error
})
if err = eg.Wait(); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
return
}
for _, hook := range postHooks {
if hook == nil {
continue
}
hook(ctx, list)
if ctx.IsAborted() {
return
}
}
if err = handlePermissions(ctx, list, resourceType); err != nil {
return
}
res := &ListData{
Count: count,
List: lo.Map(list, func(t T, _ int) any { return t }),
}
ctx.JSON(http.StatusOK, NewHttpResponseWithData(res))
return
}
func handleRemoteErr(ctx *gin.Context, err error) {
switch e := err.(type) {
case *remote.RemoteError:
if e.HttpCode == http.StatusBadRequest {
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrRemoteClient, Data: e.Resp})
return
}
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrRemoteServer, Data: e.Resp})
default:
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
}
}
func getEmpty[T model.Model](data T) T {
t := reflect.TypeOf(data).Elem()
v := reflect.New(t)
return v.Interface().(T)
}
func hasPerm[T model.Model](ctx context.Context, md T, resourceTypeName, action string) bool {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if acl.IsAdmin(currentUser) {
return true
}
if ok, _ := acl.HasPermission(ctx, currentUser.GetRid(), resourceTypeName, md.GetResourceId(), action); ok {
return true
}
pids := make([]int, 0)
switch t := any(md).(type) {
case *model.Asset:
pids, _ = repository.HandleSelfParent(ctx, t.ParentId)
case *model.Node:
pids, _ = repository.HandleSelfParent(ctx, t.Id)
}
if len(pids) > 0 {
res, _ := acl.GetRoleResources(ctx, currentUser.GetRid(), config.RESOURCE_NODE)
resId2perms := lo.SliceToMap(res, func(r *acl.Resource) (int, []string) { return r.ResourceId, r.Permissions })
resId2perms, _ = handleSelfChildPerms(ctx, resId2perms)
nodes, _ := repository.GetAllFromCacheDb(ctx, model.DefaultNode)
id2resId := lo.SliceToMap(nodes, func(n *model.Node) (int, int) { return n.Id, n.ResourceId })
if lo.ContainsBy(pids, func(pid int) bool { return lo.Contains(resId2perms[id2resId[pid]], action) }) {
return true
}
}
return false
}
func handlePermissions[T any](ctx *gin.Context, data []T, resourceTypeName string) (err error) {
if info := cast.ToBool(ctx.Query("info")); info {
return
}
currentUser, _ := acl.GetSessionFromCtx(ctx)
if !lo.Contains(config.PermResource, resourceTypeName) {
return
}
res, err := acl.GetRoleResources(ctx, currentUser.GetRid(), resourceTypeName)
if err != nil {
handleRemoteErr(ctx, err)
return
}
resId2perms := lo.SliceToMap(res, func(r *acl.Resource) (int, []string) { return r.ResourceId, r.Permissions })
switch ds := any(data).(type) {
case []*model.Node:
resId2perms, err = handleSelfChildPerms(ctx, resId2perms)
if err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
return
}
case []*model.Asset:
res, err = acl.GetRoleResources(ctx, currentUser.GetRid(), config.RESOURCE_NODE)
if err != nil {
handleRemoteErr(ctx, err)
return
}
nodeResId2perms := lo.SliceToMap(res, func(r *acl.Resource) (int, []string) { return r.ResourceId, r.Permissions })
if nodeResId2perms, err = handleSelfChildPerms(ctx, nodeResId2perms); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
return
}
var nodeId2ResId map[int]int
nodeId2ResId, err = getNodeId2ResId(ctx)
if err != nil {
return
}
for _, d := range ds {
resId2perms[d.GetResourceId()] = append(
resId2perms[d.GetResourceId()],
nodeResId2perms[nodeId2ResId[d.ParentId]]...,
)
}
}
ds := lo.Map(data, func(d T, _ int) model.Model {
x, _ := any(d).(model.Model)
return x
})
b := acl.IsAdmin(currentUser)
for _, d := range ds {
if b {
d.SetPerms(acl.AllPermissions)
continue
}
d.SetPerms(resId2perms[d.GetResourceId()])
}
return
}
func handleAcl[T any](ctx *gin.Context, dbFind *gorm.DB, resourceType string) (db *gorm.DB, err error) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
resIds, err := acl.GetRoleResourceIds(ctx, currentUser.Acl.Rid, resourceType)
if err != nil {
return
}
switch any(*new(T)).(type) {
case *model.Node:
db, err = repository.HandleNodeIds(ctx, dbFind, resIds)
case *model.Asset:
db, err = repository.HandleAssetIds(ctx, dbFind, resIds)
case *model.Account:
db, err = repository.HandleAccountIds(ctx, dbFind, resIds)
case *model.Gateway:
db = dbFind
case *model.Command:
db = dbFind
default:
db = dbFind.Where("resource_id IN ?", resIds)
}
return
}