diff --git a/backend/api/api.go b/backend/api/api.go index 62bb4e0..2a9d8a9 100644 --- a/backend/api/api.go +++ b/backend/api/api.go @@ -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) } } diff --git a/backend/api/controller/account.go b/backend/api/controller/account.go index 71fcec5..84c2a0e 100644 --- a/backend/api/controller/account.go +++ b/backend/api/controller/account.go @@ -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 } diff --git a/backend/api/controller/asset.go b/backend/api/controller/asset.go index c0000f2..9acdc18 100644 --- a/backend/api/controller/asset.go +++ b/backend/api/controller/asset.go @@ -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,28 +156,32 @@ 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 = mysql.DB.Model(&model.Node{}).Find(&nodes).Error; err != nil { - return + 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 { @@ -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 } diff --git a/backend/api/controller/authorization.go b/backend/api/controller/authorization.go index d7d9959..8330a7f 100644 --- a/backend/api/controller/authorization.go +++ b/backend/api/controller/authorization.go @@ -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) } diff --git a/backend/api/controller/connect.go b/backend/api/controller/connect.go index 962729e..ced6043 100644 --- a/backend/api/controller/connect.go +++ b/backend/api/controller/connect.go @@ -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 diff --git a/backend/api/controller/file.go b/backend/api/controller/file.go index 472cac4..37936ec 100644 --- a/backend/api/controller/file.go +++ b/backend/api/controller/file.go @@ -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 } diff --git a/backend/api/controller/node.go b/backend/api/controller/node.go index 2c68a8e..db35322 100644 --- a/backend/api/controller/node.go +++ b/backend/api/controller/node.go @@ -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,10 +210,13 @@ 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 = mysql.DB.Model(&model.Node{}).Find(&nodes).Error; err != nil { - return + 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 { @@ -221,10 +234,13 @@ 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 = mysql.DB.Model(&model.Node{}).Find(&nodes).Error; err != nil { - return + 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 { @@ -244,10 +260,14 @@ 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 = mysql.DB.Model(&model.Node{}).Find(&nodes).Error; err != nil { - return + 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 { @@ -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) diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 847edd4..a85d42a 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -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" diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 8a83933..9dfa619 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -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" diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 4df06d9..993f010 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -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: diff --git a/backend/model/asset.go b/backend/model/asset.go index ea5b214..28a5f06 100644 --- a/backend/model/asset.go +++ b/backend/model/asset.go @@ -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"` } diff --git a/backend/model/node.go b/backend/model/node.go index 779b391..9f05b8d 100644 --- a/backend/model/node.go +++ b/backend/model/node.go @@ -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"` diff --git a/backend/model/session.go b/backend/model/session.go index 03a4e4c..2ef0684 100644 --- a/backend/model/session.go +++ b/backend/model/session.go @@ -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"` diff --git a/backend/schedule/connectable.go b/backend/schedule/connectable.go index be298d3..c09c7a4 100644 --- a/backend/schedule/connectable.go +++ b/backend/schedule/connectable.go @@ -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 { diff --git a/backend/sshsrv/view.go b/backend/sshsrv/view.go index a162dae..a24a792 100644 --- a/backend/sshsrv/view.go +++ b/backend/sshsrv/view.go @@ -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 })) + dbAccount = dbAccount.Where("id IN ?", assetIds) + + accountIds, err := controller.GetAccountIdsByAuthorization(m.Ctx) + if err != nil { + return + } + dbAsset = dbAsset.Where("id IN ?", accountIds) } - if err := dbAuth.Find(&auths).Error; err != nil { - logger.L().Error("auths", zap.Error(err)) - 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 })) eg := &errgroup.Group{} eg.Go(func() error {