feat(api): permission

This commit is contained in:
ttk
2024-09-20 17:32:13 +08:00
parent 1ea2304ed1
commit 03eec7abbc
15 changed files with 537 additions and 277 deletions

View File

@@ -134,15 +134,14 @@ func RunApi() error {
share.POST("", authAdmin(), c.CreateShare)
share.DELETE("/:id", authAdmin(), c.DeleteShare)
share.GET("", authAdmin(), c.GetShare)
share.GET("/connect/:uuid", c.ConnectShare)
}
// r.GET("/api/oneterm/v1/share/connect/:uuid", Error2Resp(), c.ConnectShare)
r.GET("/api/oneterm/v1/share/connect/:uuid", Error2Resp(), c.ConnectShare)
authorization := v1.Group("/authorization")
authorization := v1.Group("/authorization", authAdmin())
{
authorization.POST("", c.CreateAuthorization)
authorization.POST("", c.UpsertAuthorization)
authorization.DELETE("/:id", c.DeleteAccount)
authorization.PUT("/:id", c.UpdateAuthorization)
authorization.GET("", c.GetAuthorizations)
}
}

View File

@@ -2,8 +2,10 @@ package controller
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
@@ -12,12 +14,17 @@ import (
"gorm.io/gorm"
"github.com/veops/oneterm/acl"
redis "github.com/veops/oneterm/cache"
"github.com/veops/oneterm/conf"
mysql "github.com/veops/oneterm/db"
"github.com/veops/oneterm/model"
"github.com/veops/oneterm/util"
)
const (
kFmtAccountIds = "accountIds-%d"
)
var (
accountPreHooks = []preHook[*model.Account]{
func(ctx *gin.Context, data *model.Account) {
@@ -145,7 +152,7 @@ func (c *Controller) GetAccounts(ctx *gin.Context) {
}
if info && !acl.IsAdmin(currentUser) {
ids, err := getAccountIdsByAuthorization(ctx)
ids, err := GetAccountIdsByAuthorization(ctx)
if err != nil {
return
}
@@ -157,29 +164,29 @@ func (c *Controller) GetAccounts(ctx *gin.Context) {
doGet[*model.Account](ctx, !info, db, acl.GetResourceTypeName(conf.RESOURCE_ACCOUNT), accountPostHooks...)
}
func getAccountIdsByAuthorization(ctx *gin.Context) (ids []int, err error) {
assetIds, err := getAssertIdsByAuthorization(ctx)
func GetAccountIdsByAuthorization(ctx *gin.Context) (ids []int, err error) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
k := fmt.Sprintf(kFmtAccountIds, currentUser.GetUid())
if err = redis.Get(ctx, k, &ids); err == nil {
return
}
assetIds, err := GetAssetIdsByAuthorization(ctx)
if err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
assets := make([]*model.Asset, 0)
if err = mysql.DB.Model(&model.Asset{}).Where("id IN ?", assetIds).Find(&assets).Error; err != nil {
ss := make([][]int, 0)
if err = mysql.DB.Model(&model.Asset{}).Where("id IN ?", assetIds).Pluck("JSON_KEYS(authorization)", &ss).Error; err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
authorizationIds, _ := ctx.Value("authorizationIds").([]*model.AuthorizationIds)
parentNodeIds, _ := ctx.Value("parentNodeIds").([]int)
for _, a := range assets {
if lo.Contains(parentNodeIds, a.Id) {
ids = append(ids, lo.Keys(a.Authorization)...)
}
accountIds := lo.Uniq(
lo.Map(lo.Filter(authorizationIds, func(item *model.AuthorizationIds, _ int) bool {
return item.AssetId != nil && *item.AssetId == a.Id && item.AccountId != nil
}),
func(item *model.AuthorizationIds, _ int) int { return *item.AccountId }))
ids = append(ids, accountIds...)
}
ids = lo.Uniq(lo.Flatten(ss))
_, _, accountIds := getIdsByAuthorizationIds(ctx)
ids = lo.Uniq(append(ids, accountIds...))
redis.SetEx(ctx, k, ids, time.Minute)
return
}

View File

@@ -1,9 +1,10 @@
package controller
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
@@ -11,6 +12,7 @@ import (
"go.uber.org/zap"
"github.com/veops/oneterm/acl"
redis "github.com/veops/oneterm/cache"
"github.com/veops/oneterm/conf"
mysql "github.com/veops/oneterm/db"
"github.com/veops/oneterm/logger"
@@ -18,6 +20,13 @@ import (
"github.com/veops/oneterm/schedule"
)
const (
kFmtAssetIds = "assetIds-%d"
kAuthorizationIds = "authorizationIds"
kParentNodeIds = "parentNodeIds"
kAccountIds = "accountIds"
)
var (
assetPreHooks = []preHook[*model.Asset]{
func(ctx *gin.Context, data *model.Asset) {
@@ -92,7 +101,7 @@ func (c *Controller) GetAssets(ctx *gin.Context) {
db = db.Where("id IN ?", lo.Map(strings.Split(q, ","), func(s string, _ int) int { return cast.ToInt(s) }))
}
if q, ok := ctx.GetQuery("parent_id"); ok {
parentIds, err := handleParentId(cast.ToInt(q))
parentIds, err := handleParentId(ctx, cast.ToInt(q))
if err != nil {
logger.L().Error("parent id found failed", zap.Error(err))
return
@@ -101,7 +110,7 @@ func (c *Controller) GetAssets(ctx *gin.Context) {
}
if info && !acl.IsAdmin(currentUser) {
ids, err := getAssertIdsByAuthorization(ctx)
ids, err := GetAssetIdsByAuthorization(ctx)
if err != nil {
return
}
@@ -147,29 +156,33 @@ func assetPostHookAuth(ctx *gin.Context, data []*model.Asset) {
return
}
authorizationIds, _ := ctx.Value("authorizationIds").([]*model.AuthorizationIds)
parentNodeIds, _ := ctx.Value("parentNodeIds").([]int)
parentNodeIds, _, accountIds := getIdsByAuthorizationIds(ctx)
for _, a := range data {
if lo.Contains(parentNodeIds, a.Id) {
continue
}
accountIds := lo.Uniq(
ids := lo.Uniq(
lo.Map(lo.Filter(authorizationIds, func(item *model.AuthorizationIds, _ int) bool {
return item.AssetId != nil && *item.AssetId == a.Id && item.AccountId != nil
}),
func(item *model.AuthorizationIds, _ int) int { return *item.AccountId }))
for k := range a.Authorization {
if !lo.Contains(accountIds, k) {
if !lo.Contains(ids, k) && !lo.Contains(accountIds, k) {
delete(a.Authorization, k)
}
}
}
}
func handleParentId(parentId int) (pids []int, err error) {
func handleParentId(ctx context.Context, parentId int) (pids []int, err error) {
nodes := make([]*model.NodeIdPid, 0)
if err = redis.Get(ctx, kFmtAllNodes, &nodes); err != nil {
if err = mysql.DB.Model(&model.Node{}).Find(&nodes).Error; err != nil {
return
}
redis.SetEx(ctx, kFmtAllNodes, nodes, time.Hour)
}
g := make(map[int][]int)
for _, n := range nodes {
g[n.ParentId] = append(g[n.ParentId], n.Id)
@@ -186,33 +199,69 @@ func handleParentId(parentId int) (pids []int, err error) {
return
}
func getAssertIdsByAuthorization(ctx *gin.Context) (ids []int, err error) {
authorizationResourceIds, err := getAutorizationResourceIds(ctx)
func GetAssetIdsByAuthorization(ctx *gin.Context) (ids []int, err error) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
authIds, err := getAuthorizationIds(ctx)
if err != nil {
return
}
ctx.Set(kAuthorizationIds, authIds)
k := fmt.Sprintf(kFmtAssetIds, currentUser.GetUid())
if err = redis.Get(ctx, k, &ids); err == nil {
return
}
parentNodeIds, ids, accountIds := getIdsByAuthorizationIds(ctx)
tmp, err := handleSelfChild(ctx, parentNodeIds)
if err != nil {
return
}
parentNodeIds = append(parentNodeIds, tmp...)
ctx.Set(kParentNodeIds, parentNodeIds)
ctx.Set(kAccountIds, accountIds)
tmp, err = getAssetIdsByNodeAccount(ctx, parentNodeIds, accountIds)
if err != nil {
return
}
ids = lo.Uniq(append(ids, tmp...))
redis.SetEx(ctx, k, ids, time.Minute)
return
}
func getIdsByAuthorizationIds(ctx context.Context) (parentNodeIds, assetIds, accountIds []int) {
authIds, _ := ctx.Value(kAuthorizationIds).([]*model.AuthorizationIds)
for _, a := range authIds {
if a.NodeId != nil && a.AssetId == nil && a.AccountId == nil {
parentNodeIds = append(parentNodeIds, *a.NodeId)
}
if a.AssetId != nil && a.NodeId == nil && a.AccountId == nil {
assetIds = append(assetIds, *a.AssetId)
}
if a.AccountId != nil && a.AssetId == nil && a.NodeId == nil {
accountIds = append(accountIds, *a.AccountId)
}
}
return
}
func getAuthorizationIds(ctx *gin.Context) (authIds []*model.AuthorizationIds, err error) {
resourceIds, err := getAutorizationResourceIds(ctx)
if err != nil {
handleRemoteErr(ctx, err)
return
}
authIds := make([]*model.AuthorizationIds, 0)
if err = mysql.DB.Model(authIds).Where("resource_id IN ?", authorizationResourceIds).Find(&ids).Error; err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
ctx.Set("authorizationIds", authIds)
parentNodeIds := make([]int, 0)
for _, a := range authIds {
if a.NodeId != nil {
parentNodeIds = append(parentNodeIds, *a.NodeId)
} else if a.AssetId != nil {
ids = append(ids, *a.AssetId)
}
}
ctx.Set("parentNodeIds", parentNodeIds)
tmp := make([]int, 0)
if err = mysql.DB.Model(&model.Asset{}).Where("parent_id IN?", parentNodeIds).Pluck("id", &tmp).Error; err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
ids = append(ids, tmp...)
err = mysql.DB.Model(authIds).Where("resource_id IN ?", resourceIds).Find(&authIds).Error
return
}
func getAssetIdsByNodeAccount(ctx context.Context, parentNodeIds, accountIds []int) (assetIds []int, err error) {
err = mysql.DB.Model(&model.Asset{}).Where("parent_id IN?", parentNodeIds).Or("JSON_KEYS(authorization) IN ?", accountIds).Pluck("id", &assetIds).Error
return
}

View File

@@ -1,9 +1,11 @@
package controller
import (
"errors"
"fmt"
"net/http"
"reflect"
"sort"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
@@ -13,12 +15,104 @@ import (
"gorm.io/gorm"
"github.com/veops/oneterm/acl"
redis "github.com/veops/oneterm/cache"
"github.com/veops/oneterm/conf"
mysql "github.com/veops/oneterm/db"
"github.com/veops/oneterm/logger"
"github.com/veops/oneterm/model"
gsession "github.com/veops/oneterm/session"
)
const (
kFmtAuthorizationIds = "AuthorizationIds-%d"
kFmtHasAuthorization = "HasAuthorization-%d-%d-%d"
)
// UpsertAuthorization godoc
//
// @Tags authorization
// @Param authorization body model.Authorization true "authorization"
// @Success 200 {object} HttpResponse
// @Router /authorization [post]
func (c *Controller) UpsertAuthorization(ctx *gin.Context) {
auth := &model.Authorization{}
err := ctx.ShouldBindBodyWithJSON(auth)
if err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
if err := mysql.DB.Transaction(func(tx *gorm.DB) error {
auth := &model.Authorization{}
if err = tx.Model(auth).
Where(fmt.Sprintf("node_id %s AND asset_id %s AND account_id %s",
lo.Ternary(auth.NodeId == nil, "IS NULL", fmt.Sprintf("=%d", auth.NodeId)),
lo.Ternary(auth.AssetId == nil, "IS NULL", fmt.Sprintf("=%d", auth.AssetId)),
lo.Ternary(auth.AccountId == nil, "IS NULL", fmt.Sprintf("=%d", auth.AccountId)),
)).
FirstOrCreate(auth).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
}
action := lo.Ternary(auth.Id > 0, model.ACTION_UPDATE, model.ACTION_CREATE)
return handleAuthorization(ctx, tx, action, nil, auth)
}); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
ctx.JSON(http.StatusOK, HttpResponse{
Data: map[string]any{
"id": auth.GetId(),
},
})
}
// DeleteAccount godoc
//
// @Tags authorization
// @Param id path int true "authorization id"
// @Success 200 {object} HttpResponse
// @Router /authorization/:id [delete]
func (c *Controller) DeleteAuthorization(ctx *gin.Context) {
auth := &model.Authorization{
Id: cast.ToInt(ctx.Param("id")),
}
if err := handleAuthorization(ctx, mysql.DB, model.ACTION_DELETE, nil, auth); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
ctx.JSON(http.StatusOK, HttpResponse{
Data: map[string]any{
"id": auth.GetId(),
},
})
}
// GetAuthorizations godoc
//
// @Tags authorization
// @Param page_index query int true "page_index"
// @Param page_size query int true "page_size"
// @Param node_id query int false "node id"
// @Param asset_id query int false "asset id"
// @Param account_id query int false "account id"
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Account}}
// @Router /authorization [get]
func (c *Controller) GetAuthorizations(ctx *gin.Context) {
db := mysql.DB.Model(&model.Authorization{})
for _, k := range []string{"node_id", "asset_id", "account_id"} {
q, ok := ctx.GetQuery(k)
if ok {
db = db.Where(fmt.Sprintf("%s IN ?", k), lo.Map(strings.Split(q, ","), func(s string, _ int) int { return cast.ToInt(s) }))
} else {
db = db.Where(fmt.Sprintf("%s IS NULL", k))
}
}
doGet[*model.Authorization](ctx, false, db, acl.GetResourceTypeName(conf.RESOURCE_AUTHORIZATION))
}
func getAuthsByAsset(t *model.Asset) (data []*model.Authorization, err error) {
db := mysql.DB.Model(data)
for accountId := range t.Authorization {
@@ -57,7 +151,8 @@ func handleAuthorization(ctx *gin.Context, tx *gorm.DB, action int, asset *model
}
}
for _, auth := range lo.Filter(auths, func(item *model.Authorization, _ int) bool { return item != nil }) {
for _, a := range lo.Filter(auths, func(item *model.Authorization, _ int) bool { return item != nil }) {
auth := a
switch action {
case model.ACTION_CREATE:
eg.Go(func() (err error) {
@@ -103,29 +198,14 @@ func handleAuthorization(ctx *gin.Context, tx *gorm.DB, action int, asset *model
return
}
func sameAuthorization(old, new model.Map[int, model.Slice[int]]) bool {
if len(old) != len(new) {
return false
}
ks := lo.Uniq(append(lo.Keys(old), lo.Keys(new)...))
for _, k := range ks {
if len(old[k]) != len(new[k]) {
return false
}
o, n := make([]int, 0, len(old[k])), make([]int, 0, len(new[k]))
copy(o, old[k])
copy(n, new[k])
sort.Ints(o)
sort.Ints(n)
if !reflect.DeepEqual(o, n) {
return false
}
}
return true
}
func getAutorizationResourceIds(ctx *gin.Context) (resourceIds []int, err error) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
k := fmt.Sprintf(kFmtAuthorizationIds, currentUser.GetUid())
if err = redis.Get(ctx, k, &resourceIds); err == nil {
return
}
var rs []*acl.Resource
rs, err = acl.GetRoleResources(ctx, currentUser.Acl.Rid, conf.RESOURCE_AUTHORIZATION)
if err != nil {
@@ -133,97 +213,63 @@ func getAutorizationResourceIds(ctx *gin.Context) (resourceIds []int, err error)
}
resourceIds = lo.Map(rs, func(r *acl.Resource, _ int) int { return r.ResourceId })
redis.SetEx(ctx, k, resourceIds, time.Minute)
return
}
func hasAuthorization(ctx *gin.Context, assetId, accountId int) (ok bool) {
if cast.ToString(ctx.Value("shareId")) != "" {
func hasAuthorization(ctx *gin.Context, sess *gsession.Session) (ok bool) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if sess.ShareId != 0 {
return true
}
ids, err := getAutorizationResourceIds(ctx)
k := fmt.Sprintf(kFmtHasAuthorization, currentUser.GetUid(), sess.AccountId, sess.AssetId)
if err := redis.Get(ctx, k, &ok); err == nil {
return
}
defer func() {
redis.SetEx(ctx, k, ok, time.Minute)
}()
if ok = acl.IsAdmin(currentUser); ok {
return
}
if sess.Session.Asset == nil {
if err := mysql.DB.Model(sess.Session.Asset).First(&sess.Session.Asset, sess.AssetId).Error; err != nil {
return
}
}
authIds, err := getAuthorizationIds(ctx)
if err != nil {
return
}
if _, ok = lo.Find(authIds, func(item *model.AuthorizationIds) bool {
return item.NodeId == nil && item.AssetId != nil && *item.AssetId == sess.AssetId && item.AccountId != nil && *item.AccountId == sess.AccountId
}); ok {
return
}
ctx.Set(kAuthorizationIds, authIds)
parentNodeIds, assetIds, accountIds := getIdsByAuthorizationIds(ctx)
tmp, err := handleSelfChild(ctx, parentNodeIds)
if err != nil {
logger.L().Error("", zap.Error(err))
return
}
cnt := int64(0)
err = mysql.DB.Model(&model.Authorization{}).
Where("asset_id =? AND account_id =? AND resource_id IN (?)", assetId, accountId, ids).
Count(&cnt).Error
parentNodeIds = append(parentNodeIds, tmp...)
if ok = lo.Contains(parentNodeIds, sess.Session.Asset.ParentId) || lo.Contains(assetIds, sess.AssetId) || lo.Contains(accountIds, sess.AccountId); ok {
return
}
ids, err := getAssetIdsByNodeAccount(ctx, parentNodeIds, accountIds)
if err != nil {
logger.L().Error("", zap.Error(err))
return
}
return cnt > 0
}
// CreateAccount godoc
//
// @Tags authorization
// @Param authorization body model.Authorization true "authorization"
// @Success 200 {object} HttpResponse
// @Router /authorization [post]
func (c *Controller) CreateAuthorization(ctx *gin.Context) {
auth := &model.Authorization{}
err := ctx.ShouldBindBodyWithJSON(auth)
if err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
if err := handleAuthorization(ctx, mysql.DB, model.ACTION_CREATE, nil, auth); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
ctx.JSON(http.StatusOK, HttpResponse{
Data: map[string]any{
"id": auth.GetId(),
},
})
}
// DeleteAccount godoc
//
// @Tags authorization
// @Param id path int true "authorization id"
// @Success 200 {object} HttpResponse
// @Router /authorization/:id [delete]
func (c *Controller) DeleteAuthorization(ctx *gin.Context) {
auth := &model.Authorization{
Id: cast.ToInt(ctx.Param("id")),
}
if err := handleAuthorization(ctx, mysql.DB, model.ACTION_DELETE, nil, auth); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
ctx.JSON(http.StatusOK, HttpResponse{
Data: map[string]any{
"id": auth.GetId(),
},
})
}
// UpdateAccount godoc
//
// @Tags authorization
// @Param id path int true "authorization id"
// @Param authorization body model.Authorization true "authorization"
// @Success 200 {object} HttpResponse
// @Router /authorization/:id [put]
func (c *Controller) UpdateAuthorization(ctx *gin.Context) {
auth := &model.Authorization{}
err := ctx.ShouldBindBodyWithJSON(auth)
if err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
auth.Id = cast.ToInt(ctx.Param("id"))
if err := handleAuthorization(ctx, mysql.DB, model.ACTION_UPDATE, nil, auth); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
ctx.JSON(http.StatusOK, HttpResponse{
Data: map[string]any{
"id": auth.GetId(),
},
})
return lo.Contains(ids, sess.AssetId)
}

View File

@@ -269,6 +269,7 @@ func DoConnect(ctx *gin.Context, ws *websocket.Conn) (sess *gsession.Session, er
Uid: currentUser.GetUid(),
UserName: currentUser.GetUserName(),
AssetId: assetId,
Asset: asset,
AssetInfo: fmt.Sprintf("%s(%s)", asset.Name, asset.Ip),
AccountId: accountId,
AccountInfo: fmt.Sprintf("%s(%s)", account.Name, account.Account),
@@ -305,7 +306,7 @@ func DoConnect(ctx *gin.Context, ws *websocket.Conn) (sess *gsession.Session, er
ctx.AbortWithError(http.StatusBadRequest, err)
return
}
if !acl.IsAdmin(currentUser) && !hasAuthorization(ctx, assetId, accountId) {
if !hasAuthorization(ctx, sess) {
err = &ApiError{Code: ErrUnauthorized}
ctx.AbortWithError(http.StatusForbidden, err)
return

View File

@@ -18,6 +18,7 @@ import (
mysql "github.com/veops/oneterm/db"
"github.com/veops/oneterm/logger"
"github.com/veops/oneterm/model"
gsession "github.com/veops/oneterm/session"
)
// GetFileHistory godoc
@@ -60,8 +61,14 @@ func (c *Controller) GetFileHistory(ctx *gin.Context) {
// @Success 200 {object} HttpResponse
// @Router /file/ls/:asset_id/:account_id [post]
func (c *Controller) FileLS(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if !acl.IsAdmin(currentUser) && !hasAuthorization(ctx, cast.ToInt(ctx.Param("account_id")), cast.ToInt(ctx.Param("account_id"))) {
sess := &gsession.Session{
Session: &model.Session{
AssetId: cast.ToInt(ctx.Param("asset_id")),
AccountId: cast.ToInt(ctx.Param("account_id")),
},
}
if !hasAuthorization(ctx, sess) {
ctx.AbortWithError(http.StatusForbidden, &ApiError{Code: ErrNoPerm, Data: map[string]any{}})
return
}
@@ -101,7 +108,15 @@ func (c *Controller) FileLS(ctx *gin.Context) {
// @Router /file/mkdir/:asset_id/:account_id [post]
func (c *Controller) FileMkdir(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if !acl.IsAdmin(currentUser) && !hasAuthorization(ctx, cast.ToInt(ctx.Param("account_id")), cast.ToInt(ctx.Param("account_id"))) {
sess := &gsession.Session{
Session: &model.Session{
AssetId: cast.ToInt(ctx.Param("asset_id")),
AccountId: cast.ToInt(ctx.Param("account_id")),
},
}
if !hasAuthorization(ctx, sess) {
ctx.AbortWithError(http.StatusForbidden, &ApiError{Code: ErrNoPerm, Data: map[string]any{}})
return
}
@@ -141,7 +156,15 @@ func (c *Controller) FileMkdir(ctx *gin.Context) {
// @Router /file/upload/:asset_id/:account_id [post]
func (c *Controller) FileUpload(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if !acl.IsAdmin(currentUser) && !hasAuthorization(ctx, cast.ToInt(ctx.Param("account_id")), cast.ToInt(ctx.Param("account_id"))) {
sess := &gsession.Session{
Session: &model.Session{
AssetId: cast.ToInt(ctx.Param("asset_id")),
AccountId: cast.ToInt(ctx.Param("account_id")),
},
}
if !hasAuthorization(ctx, sess) {
ctx.AbortWithError(http.StatusForbidden, &ApiError{Code: ErrNoPerm, Data: map[string]any{}})
return
}
@@ -202,7 +225,15 @@ func (c *Controller) FileUpload(ctx *gin.Context) {
// @Router /file/download/:asset_id/:account_id [get]
func (c *Controller) FileDownload(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if !acl.IsAdmin(currentUser) && !hasAuthorization(ctx, cast.ToInt(ctx.Param("account_id")), cast.ToInt(ctx.Param("account_id"))) {
sess := &gsession.Session{
Session: &model.Session{
AssetId: cast.ToInt(ctx.Param("asset_id")),
AccountId: cast.ToInt(ctx.Param("account_id")),
},
}
if !hasAuthorization(ctx, sess) {
ctx.AbortWithError(http.StatusForbidden, &ApiError{Code: ErrNoPerm, Data: map[string]any{}})
return
}

View File

@@ -1,9 +1,11 @@
package controller
import (
"context"
"errors"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
@@ -12,11 +14,16 @@ import (
"gorm.io/gorm"
"github.com/veops/oneterm/acl"
redis "github.com/veops/oneterm/cache"
mysql "github.com/veops/oneterm/db"
"github.com/veops/oneterm/logger"
"github.com/veops/oneterm/model"
)
const (
kFmtAllNodes = "allNodes"
)
var (
nodePreHooks = []preHook[*model.Node]{nodePreHookCheckCycle}
nodePostHooks = []postHook[*model.Node]{nodePostHookCountAsset, nodePostHookHasChild}
@@ -30,6 +37,7 @@ var (
// @Success 200 {object} HttpResponse
// @Router /node [post]
func (c *Controller) CreateNode(ctx *gin.Context) {
redis.RC.Del(ctx, kFmtAllNodes)
doCreate(ctx, false, &model.Node{}, "")
}
@@ -40,6 +48,7 @@ func (c *Controller) CreateNode(ctx *gin.Context) {
// @Success 200 {object} HttpResponse
// @Router /node/:id [delete]
func (c *Controller) DeleteNode(ctx *gin.Context) {
redis.RC.Del(ctx, kFmtAllNodes)
doDelete(ctx, false, &model.Node{}, nodeDcs...)
}
@@ -51,6 +60,7 @@ func (c *Controller) DeleteNode(ctx *gin.Context) {
// @Success 200 {object} HttpResponse
// @Router /node/:id [put]
func (c *Controller) UpdateNode(ctx *gin.Context) {
redis.RC.Del(ctx, kFmtAllNodes)
doUpdate(ctx, false, &model.Node{}, nodePreHooks...)
}
@@ -77,7 +87,7 @@ func (c *Controller) GetNodes(ctx *gin.Context) {
db = db.Where("id IN ?", lo.Map(strings.Split(q, ","), func(s string, _ int) int { return cast.ToInt(s) }))
}
if id, ok := ctx.GetQuery("no_self_child"); ok {
ids, err := handleNoSelfChild(cast.ToInt(id))
ids, err := handleNoSelfChild(ctx, cast.ToInt(id))
if err != nil {
return
}
@@ -85,7 +95,7 @@ func (c *Controller) GetNodes(ctx *gin.Context) {
}
if id, ok := ctx.GetQuery("self_parent"); ok {
ids, err := handleSelfParent(cast.ToInt(id))
ids, err := handleSelfParent(ctx, cast.ToInt(id))
if err != nil {
return
}
@@ -200,11 +210,14 @@ func nodeDelHook(ctx *gin.Context, id int) {
ctx.AbortWithError(http.StatusBadRequest, err)
}
func handleNoSelfChild(id int) (ids []int, err error) {
func handleNoSelfChild(ctx context.Context, id int) (ids []int, err error) {
nodes := make([]*model.NodeIdPid, 0)
if err = redis.Get(ctx, kFmtAllNodes, &nodes); err != nil {
if err = mysql.DB.Model(&model.Node{}).Find(&nodes).Error; err != nil {
return
}
redis.SetEx(ctx, kFmtAllNodes, nodes, time.Hour)
}
g := make(map[int][]int)
for _, n := range nodes {
g[n.ParentId] = append(g[n.ParentId], n.Id)
@@ -221,11 +234,14 @@ func handleNoSelfChild(id int) (ids []int, err error) {
return
}
func handleSelfParent(id int) (ids []int, err error) {
func handleSelfParent(ctx context.Context, id int) (ids []int, err error) {
nodes := make([]*model.NodeIdPid, 0)
if err = redis.Get(ctx, kFmtAllNodes, &nodes); err != nil {
if err = mysql.DB.Model(&model.Node{}).Find(&nodes).Error; err != nil {
return
}
redis.SetEx(ctx, kFmtAllNodes, nodes, time.Hour)
}
g := make(map[int][]int)
for _, n := range nodes {
g[n.ParentId] = append(g[n.ParentId], n.Id)
@@ -244,11 +260,15 @@ func handleSelfParent(id int) (ids []int, err error) {
return
}
func handleSelfChild(ins []int) (ids []int, err error) {
func handleSelfChild(ctx context.Context, parentIds []int) (ids []int, err error) {
// TODO: cache
nodes := make([]*model.NodeIdPid, 0)
if err = redis.Get(ctx, kFmtAllNodes, &nodes); err != nil {
if err = mysql.DB.Model(&model.Node{}).Find(&nodes).Error; err != nil {
return
}
redis.SetEx(ctx, kFmtAllNodes, nodes, time.Hour)
}
g := make(map[int][]int)
for _, n := range nodes {
g[n.ParentId] = append(g[n.ParentId], n.Id)
@@ -259,7 +279,7 @@ func handleSelfChild(ins []int) (ids []int, err error) {
ids = append(ids, x)
}
for _, y := range g[x] {
dfs(y, b || lo.Contains(ins, x))
dfs(y, b || lo.Contains(parentIds, x))
}
}
dfs(0, false)

View File

@@ -1,4 +1,4 @@
// Package docs Code generated by swaggo/swag at 2024-09-19 17:52:48.7357174 +0800 CST m=+7.810953701. DO NOT EDIT
// Package docs Code generated by swaggo/swag at 2024-09-20 17:24:49.219072 +0800 CST m=+7.114523601. DO NOT EDIT
package docs
import "github.com/swaggo/swag"
@@ -368,6 +368,80 @@ const docTemplate = `{
}
},
"/authorization": {
"get": {
"tags": [
"authorization"
],
"parameters": [
{
"type": "integer",
"description": "page_index",
"name": "page_index",
"in": "query",
"required": true
},
{
"type": "integer",
"description": "page_size",
"name": "page_size",
"in": "query",
"required": true
},
{
"type": "integer",
"description": "node id",
"name": "node_id",
"in": "query"
},
{
"type": "integer",
"description": "asset id",
"name": "asset_id",
"in": "query"
},
{
"type": "integer",
"description": "account id",
"name": "account_id",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/controller.HttpResponse"
},
{
"type": "object",
"properties": {
"data": {
"allOf": [
{
"$ref": "#/definitions/controller.ListData"
},
{
"type": "object",
"properties": {
"list": {
"type": "array",
"items": {
"$ref": "#/definitions/model.Account"
}
}
}
}
]
}
}
}
]
}
}
}
},
"post": {
"tags": [
"authorization"
@@ -394,37 +468,6 @@ const docTemplate = `{
}
},
"/authorization/:id": {
"put": {
"tags": [
"authorization"
],
"parameters": [
{
"type": "integer",
"description": "authorization id",
"name": "id",
"in": "path",
"required": true
},
{
"description": "authorization",
"name": "authorization",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/model.Authorization"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
},
"delete": {
"tags": [
"authorization"

View File

@@ -357,6 +357,80 @@
}
},
"/authorization": {
"get": {
"tags": [
"authorization"
],
"parameters": [
{
"type": "integer",
"description": "page_index",
"name": "page_index",
"in": "query",
"required": true
},
{
"type": "integer",
"description": "page_size",
"name": "page_size",
"in": "query",
"required": true
},
{
"type": "integer",
"description": "node id",
"name": "node_id",
"in": "query"
},
{
"type": "integer",
"description": "asset id",
"name": "asset_id",
"in": "query"
},
{
"type": "integer",
"description": "account id",
"name": "account_id",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/controller.HttpResponse"
},
{
"type": "object",
"properties": {
"data": {
"allOf": [
{
"$ref": "#/definitions/controller.ListData"
},
{
"type": "object",
"properties": {
"list": {
"type": "array",
"items": {
"$ref": "#/definitions/model.Account"
}
}
}
}
]
}
}
}
]
}
}
}
},
"post": {
"tags": [
"authorization"
@@ -383,37 +457,6 @@
}
},
"/authorization/:id": {
"put": {
"tags": [
"authorization"
],
"parameters": [
{
"type": "integer",
"description": "authorization id",
"name": "id",
"in": "path",
"required": true
},
{
"description": "authorization",
"name": "authorization",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/model.Authorization"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
},
"delete": {
"tags": [
"authorization"

View File

@@ -688,6 +688,49 @@ paths:
tags:
- asset
/authorization:
get:
parameters:
- description: page_index
in: query
name: page_index
required: true
type: integer
- description: page_size
in: query
name: page_size
required: true
type: integer
- description: node id
in: query
name: node_id
type: integer
- description: asset id
in: query
name: asset_id
type: integer
- description: account id
in: query
name: account_id
type: integer
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/controller.HttpResponse'
- properties:
data:
allOf:
- $ref: '#/definitions/controller.ListData'
- properties:
list:
items:
$ref: '#/definitions/model.Account'
type: array
type: object
type: object
tags:
- authorization
post:
parameters:
- description: authorization
@@ -718,26 +761,6 @@ paths:
$ref: '#/definitions/controller.HttpResponse'
tags:
- authorization
put:
parameters:
- description: authorization id
in: path
name: id
required: true
type: integer
- description: authorization
in: body
name: authorization
required: true
schema:
$ref: '#/definitions/model.Authorization'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controller.HttpResponse'
tags:
- authorization
/command:
get:
parameters:

View File

@@ -16,9 +16,9 @@ type Asset struct {
Comment string `json:"comment" gorm:"column:comment"`
ParentId int `json:"parent_id" gorm:"column:parent_id"`
Ip string `json:"ip" gorm:"column:ip"`
Protocols Slice[string] `json:"protocols" gorm:"column:protocols"`
Protocols Slice[string] `json:"protocols" gorm:"column:protocols;type:text"`
GatewayId int `json:"gateway_id" gorm:"column:gateway_id"`
Authorization Map[int, Slice[int]] `json:"authorization" gorm:"column:authorization"`
Authorization Map[int, Slice[int]] `json:"authorization" gorm:"column:authorization;type:text"`
AccessAuth AccessAuth `json:"access_auth" gorm:"embedded;column:access_auth"`
Connectable bool `json:"connectable" gorm:"column:connectable"`
NodeChain string `json:"node_chain" gorm:"-"`
@@ -34,8 +34,8 @@ type Asset struct {
type AccessAuth struct {
Start *time.Time `json:"start,omitempty" gorm:"column:start"`
End *time.Time `json:"end,omitempty" gorm:"column:end"`
CmdIds Slice[int] `json:"cmd_ids" gorm:"column:cmd_ids"`
Ranges Slice[Range] `json:"ranges" gorm:"column:ranges"`
CmdIds Slice[int] `json:"cmd_ids" gorm:"column:cmd_ids;type:text"`
Ranges Slice[Range] `json:"ranges" gorm:"column:ranges;type:text"`
Allow bool `json:"allow" gorm:"column:allow"`
}

View File

@@ -15,9 +15,9 @@ type Node struct {
Name string `json:"name" gorm:"column:name"`
Comment string `json:"comment" gorm:"column:comment"`
ParentId int `json:"parent_id" gorm:"column:parent_id"`
Authorization Map[int, Slice[int]] `json:"authorization" gorm:"column:authorization"`
Authorization Map[int, Slice[int]] `json:"authorization" gorm:"column:authorization;type:text"`
AccessAuth AccessAuth `json:"access_auth" gorm:"embedded;column:access_auth"`
Protocols Slice[string] `json:"protocols" gorm:"column:protocols"`
Protocols Slice[string] `json:"protocols" gorm:"column:protocols;type:text"`
GatewayId int `json:"gateway_id" gorm:"column:gateway_id"`
// ResourceId int `json:"resource_id"`

View File

@@ -28,6 +28,7 @@ type Session struct {
Uid int `json:"uid" gorm:"column:uid"`
UserName string `json:"user_name" gorm:"column:user_name"`
AssetId int `json:"asset_id" gorm:"column:asset_id"`
Asset *Asset `json:"-" gorm:"-"`
AssetInfo string `json:"asset_info" gorm:"column:asset_info"`
AccountId int `json:"account_id" gorm:"column:account_id"`
AccountInfo string `json:"account_info" gorm:"column:account_info"`

View File

@@ -30,7 +30,7 @@ func UpdateConnectables(ids ...int) (err error) {
if len(ids) > 0 {
db = db.Where("id IN ?", ids)
} else {
db = db.Where("updated_at <= ?", time.Now().Add(time.Hour).Unix())
db = db.Where("updated_at <= ?", time.Now().Add(-time.Hour))
}
if err = db.
Find(&assets).Error; err != nil {

View File

@@ -23,7 +23,6 @@ import (
"github.com/veops/oneterm/acl"
"github.com/veops/oneterm/api/controller"
redis "github.com/veops/oneterm/cache"
"github.com/veops/oneterm/conf"
mysql "github.com/veops/oneterm/db"
"github.com/veops/oneterm/logger"
"github.com/veops/oneterm/model"
@@ -226,24 +225,22 @@ func (m *view) refresh() {
auths := make([]*model.Authorization, 0)
assets := make([]*model.Asset, 0)
accounts := make([]*model.Account, 0)
dbAuth := mysql.DB.Model(auths)
dbAsset := mysql.DB.Model(assets)
dbAccount := mysql.DB.Model(accounts)
if !acl.IsAdmin(m.currentUser) {
rs, err := acl.GetRoleResources(ctx, m.currentUser.Acl.Rid, conf.GetResourceTypeName(conf.RESOURCE_AUTHORIZATION))
assetIds, err := controller.GetAssetIdsByAuthorization(m.Ctx)
if err != nil {
logger.L().Error("auths", zap.Error(err))
return
}
dbAuth = dbAuth.Where("resource_id IN ?", lo.Map(rs, func(r *acl.Resource, _ int) int { return r.ResourceId }))
}
if err := dbAuth.Find(&auths).Error; err != nil {
logger.L().Error("auths", zap.Error(err))
dbAccount = dbAccount.Where("id IN ?", assetIds)
accountIds, err := controller.GetAccountIdsByAuthorization(m.Ctx)
if err != nil {
return
}
dbAccount = dbAccount.Where("id IN ?", lo.Map(auths, func(a *model.Authorization, _ int) int { return *a.AccountId }))
dbAsset = dbAsset.Where("id IN ?", lo.Map(auths, func(a *model.Authorization, _ int) int { return *a.AssetId }))
dbAsset = dbAsset.Where("id IN ?", accountIds)
}
eg := &errgroup.Group{}
eg.Go(func() error {