mirror of
https://github.com/veops/oneterm.git
synced 2025-10-05 23:37:03 +08:00
refactor(backend): authorization v2
This commit is contained in:
@@ -47,6 +47,8 @@ require (
|
|||||||
gorm.io/plugin/soft_delete v1.2.1
|
gorm.io/plugin/soft_delete v1.2.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require github.com/stretchr/testify v1.9.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
|
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||||
@@ -56,6 +58,7 @@ require (
|
|||||||
github.com/charmbracelet/x/term v0.1.1 // indirect
|
github.com/charmbracelet/x/term v0.1.1 // indirect
|
||||||
github.com/charmbracelet/x/windows v0.1.0 // indirect
|
github.com/charmbracelet/x/windows v0.1.0 // indirect
|
||||||
github.com/clbanning/mxj v1.8.4 // indirect
|
github.com/clbanning/mxj v1.8.4 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/go-ini/ini v1.67.0 // indirect
|
github.com/go-ini/ini v1.67.0 // indirect
|
||||||
@@ -75,6 +78,7 @@ require (
|
|||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.15.2 // indirect
|
github.com/muesli/termenv v0.15.2 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/rs/xid v1.6.0 // indirect
|
github.com/rs/xid v1.6.0 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/time v0.6.0 // indirect
|
golang.org/x/time v0.6.0 // indirect
|
||||||
|
@@ -63,6 +63,178 @@ func MigrateNode() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MigrateCommand() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
rts, err := GetResourceTypes(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.L().Fatal("get resource type failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure command resource type exists
|
||||||
|
if !lo.ContainsBy(rts, func(rt *ResourceType) bool { return rt.Name == "command" }) {
|
||||||
|
if err = AddResourceTypes(ctx, &ResourceType{Name: "command", Perms: AllPermissions}); err != nil {
|
||||||
|
logger.L().Fatal("add command resource type failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure command_template resource type exists
|
||||||
|
if !lo.ContainsBy(rts, func(rt *ResourceType) bool { return rt.Name == "command_template" }) {
|
||||||
|
if err = AddResourceTypes(ctx, &ResourceType{Name: "command_template", Perms: AllPermissions}); err != nil {
|
||||||
|
logger.L().Fatal("add command_template resource type failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate Commands
|
||||||
|
commands := make([]*model.Command, 0)
|
||||||
|
if err = dbpkg.DB.Model(&commands).Where("resource_id = 0").Or("resource_id IS NULL").Find(&commands).Error; err != nil {
|
||||||
|
logger.L().Fatal("get commands failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get existing command resources to avoid duplicates
|
||||||
|
existingCommandResources, err := GetCommandResources(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.L().Fatal("get existing command resources failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
commandNameToResourceId := lo.SliceToMap(existingCommandResources, func(r *Resource) (string, int) {
|
||||||
|
return r.Name, r.ResourceId
|
||||||
|
})
|
||||||
|
|
||||||
|
eg := errgroup.Group{}
|
||||||
|
for _, c := range commands {
|
||||||
|
cmd := c
|
||||||
|
eg.Go(func() error {
|
||||||
|
var resourceId int
|
||||||
|
|
||||||
|
// Check if resource already exists
|
||||||
|
if existingResourceId, exists := commandNameToResourceId[cmd.Name]; exists {
|
||||||
|
resourceId = existingResourceId
|
||||||
|
logger.L().Info("Using existing resource for command",
|
||||||
|
zap.String("command", cmd.Name),
|
||||||
|
zap.Int("resource_id", resourceId))
|
||||||
|
} else {
|
||||||
|
// Create new resource
|
||||||
|
r, err := AddResource(ctx, cmd.CreatorId, "command", cmd.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resourceId = r.ResourceId
|
||||||
|
logger.L().Info("Created new resource for command",
|
||||||
|
zap.String("command", cmd.Name),
|
||||||
|
zap.Int("resource_id", resourceId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update command with resource_id
|
||||||
|
if err := dbpkg.DB.Model(&cmd).Where("id=?", cmd.Id).Update("resource_id", resourceId).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate CommandTemplates
|
||||||
|
templates := make([]*model.CommandTemplate, 0)
|
||||||
|
if err = dbpkg.DB.Model(&templates).Where("resource_id = 0").Or("resource_id IS NULL").Find(&templates).Error; err != nil {
|
||||||
|
logger.L().Fatal("get command templates failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get existing command_template resources to avoid duplicates
|
||||||
|
existingTemplateResources, err := GetCommandTemplateResources(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.L().Fatal("get existing command template resources failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
templateNameToResourceId := lo.SliceToMap(existingTemplateResources, func(r *Resource) (string, int) {
|
||||||
|
return r.Name, r.ResourceId
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, t := range templates {
|
||||||
|
tmpl := t
|
||||||
|
eg.Go(func() error {
|
||||||
|
var resourceId int
|
||||||
|
|
||||||
|
// Check if resource already exists
|
||||||
|
if existingResourceId, exists := templateNameToResourceId[tmpl.Name]; exists {
|
||||||
|
resourceId = existingResourceId
|
||||||
|
logger.L().Info("Using existing resource for command template",
|
||||||
|
zap.String("template", tmpl.Name),
|
||||||
|
zap.Int("resource_id", resourceId))
|
||||||
|
} else {
|
||||||
|
// Create new resource
|
||||||
|
r, err := AddResource(ctx, tmpl.CreatorId, "command_template", tmpl.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resourceId = r.ResourceId
|
||||||
|
logger.L().Info("Created new resource for command template",
|
||||||
|
zap.String("template", tmpl.Name),
|
||||||
|
zap.Int("resource_id", resourceId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update template with resource_id
|
||||||
|
if err := dbpkg.DB.Model(&tmpl).Where("id=?", tmpl.Id).Update("resource_id", resourceId).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = eg.Wait(); err != nil {
|
||||||
|
logger.L().Fatal("migrate command and template resources failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCommandResources retrieves all command resources from ACL system
|
||||||
|
func GetCommandResources(ctx context.Context) ([]*Resource, error) {
|
||||||
|
token, err := remote.GetAclToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := &ResourceResult{}
|
||||||
|
url := fmt.Sprintf("%s/acl/resources", config.Cfg.Auth.Acl.Url)
|
||||||
|
resp, err := remote.RC.R().
|
||||||
|
SetHeader("App-Access-Token", token).
|
||||||
|
SetQueryParams(map[string]string{
|
||||||
|
"app_id": config.Cfg.Auth.Acl.AppId,
|
||||||
|
"resource_type_id": "command",
|
||||||
|
"page_size": "1000", // Get all resources
|
||||||
|
}).
|
||||||
|
SetResult(data).
|
||||||
|
Get(url)
|
||||||
|
|
||||||
|
if err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true }); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.Resources, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCommandTemplateResources retrieves all command template resources from ACL system
|
||||||
|
func GetCommandTemplateResources(ctx context.Context) ([]*Resource, error) {
|
||||||
|
token, err := remote.GetAclToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := &ResourceResult{}
|
||||||
|
url := fmt.Sprintf("%s/acl/resources", config.Cfg.Auth.Acl.Url)
|
||||||
|
resp, err := remote.RC.R().
|
||||||
|
SetHeader("App-Access-Token", token).
|
||||||
|
SetQueryParams(map[string]string{
|
||||||
|
"app_id": config.Cfg.Auth.Acl.AppId,
|
||||||
|
"resource_type_id": "command_template",
|
||||||
|
"page_size": "1000", // Get all resources
|
||||||
|
}).
|
||||||
|
SetResult(data).
|
||||||
|
Get(url)
|
||||||
|
|
||||||
|
if err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true }); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.Resources, nil
|
||||||
|
}
|
||||||
|
|
||||||
func GetResourceTypes(ctx context.Context) (rt []*ResourceType, err error) {
|
func GetResourceTypes(ctx context.Context) (rt []*ResourceType, err error) {
|
||||||
token, err := remote.GetAclToken(ctx)
|
token, err := remote.GetAclToken(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -28,11 +28,12 @@ func initDB() {
|
|||||||
cfg := db.ConfigFromGlobal()
|
cfg := db.ConfigFromGlobal()
|
||||||
|
|
||||||
if err := db.Init(cfg, true,
|
if err := db.Init(cfg, true,
|
||||||
model.DefaultAccount, model.DefaultAsset, model.DefaultAuthorization, model.DefaultCommand,
|
model.DefaultAccount, model.DefaultAsset, model.DefaultAuthorization, model.DefaultAuthorizationV2,
|
||||||
model.DefaultConfig, model.DefaultFileHistory, model.DefaultGateway, model.DefaultHistory,
|
model.DefaultCommand, model.DefaultCommandTemplate, model.DefaultConfig, model.DefaultFileHistory,
|
||||||
model.DefaultNode, model.DefaultPublicKey, model.DefaultSession, model.DefaultSessionCmd,
|
model.DefaultGateway, model.DefaultHistory, model.DefaultNode, model.DefaultPublicKey,
|
||||||
model.DefaultShare, model.DefaultQuickCommand, model.DefaultUserPreference,
|
model.DefaultSession, model.DefaultSessionCmd, model.DefaultShare, model.DefaultQuickCommand,
|
||||||
model.DefaultStorageConfig, model.DefaultStorageMetrics,
|
model.DefaultUserPreference, model.DefaultStorageConfig, model.DefaultStorageMetrics,
|
||||||
|
model.DefaultTimeTemplate, model.DefaultMigrationRecord,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
logger.L().Fatal("Failed to init database", zap.Error(err))
|
logger.L().Fatal("Failed to init database", zap.Error(err))
|
||||||
}
|
}
|
||||||
@@ -42,6 +43,7 @@ func initDB() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
acl.MigrateNode()
|
acl.MigrateNode()
|
||||||
|
acl.MigrateCommand()
|
||||||
|
|
||||||
gsession.InitSessionCleanup()
|
gsession.InitSessionCleanup()
|
||||||
}
|
}
|
||||||
@@ -49,6 +51,17 @@ func initDB() {
|
|||||||
func initServices() {
|
func initServices() {
|
||||||
service.InitAuthorizationService()
|
service.InitAuthorizationService()
|
||||||
fileservice.InitFileService()
|
fileservice.InitFileService()
|
||||||
|
|
||||||
|
// Initialize predefined dangerous commands and templates
|
||||||
|
if err := service.InitBuiltinCommands(); err != nil {
|
||||||
|
logger.L().Error("Failed to initialize builtin commands", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize built-in time templates
|
||||||
|
timeTemplateService := service.NewTimeTemplateService()
|
||||||
|
if err := timeTemplateService.InitializeBuiltInTemplates(ctx); err != nil {
|
||||||
|
logger.L().Error("Failed to initialize built-in time templates", zap.Error(err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func initStorage() error {
|
func initStorage() error {
|
||||||
|
@@ -7,7 +7,6 @@ import (
|
|||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
|
|
||||||
"github.com/veops/oneterm/internal/acl"
|
|
||||||
"github.com/veops/oneterm/internal/model"
|
"github.com/veops/oneterm/internal/model"
|
||||||
"github.com/veops/oneterm/internal/service"
|
"github.com/veops/oneterm/internal/service"
|
||||||
"github.com/veops/oneterm/pkg/config"
|
"github.com/veops/oneterm/pkg/config"
|
||||||
@@ -103,27 +102,18 @@ func (c *Controller) UpdateAccount(ctx *gin.Context) {
|
|||||||
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Account}}
|
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Account}}
|
||||||
// @Router /account [get]
|
// @Router /account [get]
|
||||||
func (c *Controller) GetAccounts(ctx *gin.Context) {
|
func (c *Controller) GetAccounts(ctx *gin.Context) {
|
||||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
||||||
info := cast.ToBool(ctx.Query("info"))
|
info := cast.ToBool(ctx.Query("info"))
|
||||||
|
|
||||||
// Build base query using service layer
|
// Build query with integrated V2 authorization filter
|
||||||
db := accountService.BuildQuery(ctx)
|
db, err := accountService.BuildQueryWithAuthorization(ctx)
|
||||||
|
|
||||||
// Apply select fields for info mode
|
|
||||||
if info {
|
|
||||||
db = db.Select("id", "name", "account")
|
|
||||||
|
|
||||||
// Apply authorization filter if needed
|
|
||||||
if !acl.IsAdmin(currentUser) {
|
|
||||||
assetIds, err := GetAssetIdsByAuthorization(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter accounts by asset IDs
|
// Apply info mode settings
|
||||||
db = accountService.FilterByAssetIds(db, assetIds)
|
if info {
|
||||||
}
|
db = db.Select("id", "name", "account")
|
||||||
}
|
}
|
||||||
|
|
||||||
doGet(ctx, !info, db, config.RESOURCE_ACCOUNT, accountPostHooks...)
|
doGet(ctx, !info, db, config.RESOURCE_ACCOUNT, accountPostHooks...)
|
||||||
|
@@ -2,12 +2,14 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/veops/oneterm/internal/acl"
|
"github.com/samber/lo"
|
||||||
"github.com/veops/oneterm/internal/model"
|
"github.com/veops/oneterm/internal/model"
|
||||||
"github.com/veops/oneterm/internal/service"
|
"github.com/veops/oneterm/internal/service"
|
||||||
"github.com/veops/oneterm/pkg/config"
|
"github.com/veops/oneterm/pkg/config"
|
||||||
@@ -39,19 +41,6 @@ var (
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Apply authorization filters
|
|
||||||
func(ctx *gin.Context, data []*model.Asset) {
|
|
||||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
||||||
if acl.IsAdmin(currentUser) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
authorizationIds, _ := ctx.Value(kAuthorizationIds).([]*model.AuthorizationIds)
|
|
||||||
nodeIds, _ := ctx.Value(kNodeIds).([]int)
|
|
||||||
accountIds, _ := ctx.Value(kAccountIds).([]int)
|
|
||||||
|
|
||||||
assetService.ApplyAuthorizationFilters(ctx, data, authorizationIds, nodeIds, accountIds)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -105,11 +94,10 @@ func (c *Controller) UpdateAsset(ctx *gin.Context) {
|
|||||||
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Asset}}
|
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Asset}}
|
||||||
// @Router /asset [get]
|
// @Router /asset [get]
|
||||||
func (c *Controller) GetAssets(ctx *gin.Context) {
|
func (c *Controller) GetAssets(ctx *gin.Context) {
|
||||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
||||||
info := cast.ToBool(ctx.Query("info"))
|
info := cast.ToBool(ctx.Query("info"))
|
||||||
|
|
||||||
// Build base query using service layer
|
// Build query with integrated V2 authorization filter
|
||||||
db, err := assetService.BuildQuery(ctx)
|
db, err := assetService.BuildQueryWithAuthorization(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AbortWithError(http.StatusInternalServerError, &errors.ApiError{Code: errors.ErrInternal, Data: map[string]any{"err": err}})
|
ctx.AbortWithError(http.StatusInternalServerError, &errors.ApiError{Code: errors.ErrInternal, Data: map[string]any{"err": err}})
|
||||||
return
|
return
|
||||||
@@ -127,24 +115,116 @@ func (c *Controller) GetAssets(ctx *gin.Context) {
|
|||||||
|
|
||||||
// Apply info mode settings
|
// Apply info mode settings
|
||||||
if info {
|
if info {
|
||||||
db = db.Select("id", "parent_id", "name", "ip", "protocols", "connectable", "authorization")
|
db = db.Select("id", "parent_id", "name", "ip", "protocols", "connectable", "authorization", "resource_id", "access_time_control", "asset_command_control")
|
||||||
|
|
||||||
// Apply authorization filter if needed
|
|
||||||
if !acl.IsAdmin(currentUser) {
|
|
||||||
ids, err := GetAssetIdsByAuthorization(ctx)
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(http.StatusInternalServerError, &errors.ApiError{Code: errors.ErrInternal, Data: map[string]any{"err": err}})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
db = db.Where("id IN ?", ids)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
doGet(ctx, !info, db, config.RESOURCE_ASSET, assetPostHooks...)
|
doGet(ctx, false, db, config.RESOURCE_ASSET, assetPostHooks...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAssetIdsByAuthorization gets asset IDs by authorization
|
// GetAssetIdsByAuthorization gets asset IDs by authorization
|
||||||
func GetAssetIdsByAuthorization(ctx *gin.Context) ([]int, error) {
|
func GetAssetIdsByAuthorization(ctx *gin.Context) ([]int, error) {
|
||||||
_, assetIds, _, err := assetService.GetAssetIdsByAuthorization(ctx)
|
// Use V2 authorization system for asset filtering
|
||||||
|
authV2Service := service.NewAuthorizationV2Service()
|
||||||
|
_, assetIds, _, err := authV2Service.GetAuthorizationScopeByACL(ctx)
|
||||||
return assetIds, err
|
return assetIds, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AssetPermissionResult represents simplified permission result without permissions field
|
||||||
|
type AssetPermissionResult struct {
|
||||||
|
Allowed bool `json:"allowed"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
RuleId int `json:"rule_id"`
|
||||||
|
RuleName string `json:"rule_name"`
|
||||||
|
Restrictions map[string]interface{} `json:"restrictions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssetPermissionBatchResult represents batch permission results without permissions field
|
||||||
|
type AssetPermissionBatchResult struct {
|
||||||
|
Results map[model.AuthAction]*AssetPermissionResult `json:"results"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssetPermissionMultiAccountResult represents permission results for multiple accounts
|
||||||
|
type AssetPermissionMultiAccountResult struct {
|
||||||
|
Results map[int]*AssetPermissionBatchResult `json:"results"` // accountId -> batch results
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAssetPermissions godoc
|
||||||
|
//
|
||||||
|
// @Tags asset
|
||||||
|
// @Param id path int true "asset id"
|
||||||
|
// @Param account_ids query string false "account ids (comma separated, e.g. 123,456,789)"
|
||||||
|
// @Success 200 {object} HttpResponse{data=AssetPermissionMultiAccountResult}
|
||||||
|
// @Router /asset/:id/permissions [get]
|
||||||
|
func (c *Controller) GetAssetPermissions(ctx *gin.Context) {
|
||||||
|
assetId := cast.ToInt(ctx.Param("id"))
|
||||||
|
accountIdsStr := ctx.Query("account_ids")
|
||||||
|
|
||||||
|
if assetId <= 0 {
|
||||||
|
ctx.AbortWithError(http.StatusBadRequest, &errors.ApiError{Code: errors.ErrInvalidArgument, Data: map[string]any{"err": "invalid asset id"}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse account_ids parameter
|
||||||
|
var accountIds []int
|
||||||
|
if accountIdsStr != "" {
|
||||||
|
// Split by comma and convert to integers
|
||||||
|
idStrs := strings.Split(accountIdsStr, ",")
|
||||||
|
for _, idStr := range idStrs {
|
||||||
|
idStr = strings.TrimSpace(idStr)
|
||||||
|
if idStr != "" {
|
||||||
|
if id, err := strconv.Atoi(idStr); err == nil && id > 0 {
|
||||||
|
accountIds = append(accountIds, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove duplicates
|
||||||
|
accountIds = lo.Uniq(accountIds)
|
||||||
|
|
||||||
|
// Use V2 authorization service to get permissions
|
||||||
|
authV2Service := service.NewAuthorizationV2Service()
|
||||||
|
|
||||||
|
// If no account IDs provided, return empty result
|
||||||
|
if len(accountIds) == 0 {
|
||||||
|
ctx.JSON(http.StatusOK, HttpResponse{
|
||||||
|
Data: &AssetPermissionBatchResult{
|
||||||
|
Results: make(map[model.AuthAction]*AssetPermissionResult),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
multiResult := &AssetPermissionMultiAccountResult{
|
||||||
|
Results: make(map[int]*AssetPermissionBatchResult),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, accId := range accountIds {
|
||||||
|
result, err := authV2Service.GetAssetPermissions(ctx, assetId, accId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusInternalServerError, &errors.ApiError{Code: errors.ErrInternal, Data: map[string]any{"err": err}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to simplified result without permissions field
|
||||||
|
simplifiedResult := &AssetPermissionBatchResult{
|
||||||
|
Results: make(map[model.AuthAction]*AssetPermissionResult),
|
||||||
|
}
|
||||||
|
|
||||||
|
for action, authResult := range result.Results {
|
||||||
|
simplifiedResult.Results[action] = &AssetPermissionResult{
|
||||||
|
Allowed: authResult.Allowed,
|
||||||
|
Reason: authResult.Reason,
|
||||||
|
RuleId: authResult.RuleId,
|
||||||
|
RuleName: authResult.RuleName,
|
||||||
|
Restrictions: authResult.Restrictions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
multiResult.Results[accId] = simplifiedResult
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, HttpResponse{
|
||||||
|
Data: multiResult,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@@ -11,7 +11,6 @@ import (
|
|||||||
"github.com/veops/oneterm/internal/acl"
|
"github.com/veops/oneterm/internal/acl"
|
||||||
"github.com/veops/oneterm/internal/model"
|
"github.com/veops/oneterm/internal/model"
|
||||||
"github.com/veops/oneterm/internal/service"
|
"github.com/veops/oneterm/internal/service"
|
||||||
gsession "github.com/veops/oneterm/internal/session"
|
|
||||||
myErrors "github.com/veops/oneterm/pkg/errors"
|
myErrors "github.com/veops/oneterm/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -143,11 +142,6 @@ func handleAuthorization(ctx *gin.Context, tx *gorm.DB, action int, asset *model
|
|||||||
return service.DefaultAuthService.HandleAuthorization(ctx, tx, action, asset, auths...)
|
return service.DefaultAuthService.HandleAuthorization(ctx, tx, action, asset, auths...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasAuthorization checks if the session has authorization
|
|
||||||
func hasAuthorization(ctx *gin.Context, sess *gsession.Session) (ok bool, err error) {
|
|
||||||
return service.DefaultAuthService.HasAuthorization(ctx, sess)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getIdsByAuthorizationIds(ctx *gin.Context) (nodeIds, assetIds, accountIds []int) {
|
func getIdsByAuthorizationIds(ctx *gin.Context) (nodeIds, assetIds, accountIds []int) {
|
||||||
authorizationIds, ok := ctx.Value(kAuthorizationIds).([]*model.AuthorizationIds)
|
authorizationIds, ok := ctx.Value(kAuthorizationIds).([]*model.AuthorizationIds)
|
||||||
if !ok || len(authorizationIds) == 0 {
|
if !ok || len(authorizationIds) == 0 {
|
||||||
|
329
backend/internal/api/controller/authorization_v2.go
Normal file
329
backend/internal/api/controller/authorization_v2.go
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/veops/oneterm/internal/acl"
|
||||||
|
"github.com/veops/oneterm/internal/model"
|
||||||
|
"github.com/veops/oneterm/internal/service"
|
||||||
|
myErrors "github.com/veops/oneterm/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateAuthorizationV2 godoc
|
||||||
|
//
|
||||||
|
// @Tags authorization_v2
|
||||||
|
// @Param authorization body model.AuthorizationV2 true "authorization rule"
|
||||||
|
// @Success 200 {object} HttpResponse
|
||||||
|
// @Router /authorization_v2 [post]
|
||||||
|
func (c *Controller) CreateAuthorizationV2(ctx *gin.Context) {
|
||||||
|
auth := &model.AuthorizationV2{}
|
||||||
|
err := ctx.ShouldBindBodyWithJSON(auth)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authV2Service := service.NewAuthorizationV2Service()
|
||||||
|
|
||||||
|
// Check permissions
|
||||||
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||||
|
if !acl.IsAdmin(currentUser) {
|
||||||
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.GRANT}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the rule
|
||||||
|
err = authV2Service.CreateRule(ctx, auth)
|
||||||
|
if err != nil {
|
||||||
|
if ctx.IsAborted() {
|
||||||
|
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": auth.GetId(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAuthorizationV2 godoc
|
||||||
|
//
|
||||||
|
// @Tags authorization_v2
|
||||||
|
// @Param id path int true "authorization id"
|
||||||
|
// @Param authorization body model.AuthorizationV2 true "authorization rule"
|
||||||
|
// @Success 200 {object} HttpResponse
|
||||||
|
// @Router /authorization_v2/:id [put]
|
||||||
|
func (c *Controller) UpdateAuthorizationV2(ctx *gin.Context) {
|
||||||
|
authId := cast.ToInt(ctx.Param("id"))
|
||||||
|
auth := &model.AuthorizationV2{}
|
||||||
|
err := ctx.ShouldBindBodyWithJSON(auth)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.Id = authId
|
||||||
|
authV2Service := service.NewAuthorizationV2Service()
|
||||||
|
|
||||||
|
// Check permissions
|
||||||
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||||
|
if !acl.IsAdmin(currentUser) {
|
||||||
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.GRANT}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the rule
|
||||||
|
err = authV2Service.UpdateRule(ctx, auth)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||||
|
} else {
|
||||||
|
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": auth.GetId(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAuthorizationV2 godoc
|
||||||
|
//
|
||||||
|
// @Tags authorization_v2
|
||||||
|
// @Param id path int true "authorization id"
|
||||||
|
// @Success 200 {object} HttpResponse
|
||||||
|
// @Router /authorization_v2/:id [delete]
|
||||||
|
func (c *Controller) DeleteAuthorizationV2(ctx *gin.Context) {
|
||||||
|
authId := cast.ToInt(ctx.Param("id"))
|
||||||
|
|
||||||
|
authV2Service := service.NewAuthorizationV2Service()
|
||||||
|
|
||||||
|
// Check permissions
|
||||||
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||||
|
if !acl.IsAdmin(currentUser) {
|
||||||
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.GRANT}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the rule
|
||||||
|
if err := authV2Service.DeleteRule(ctx, authId); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||||
|
} else {
|
||||||
|
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": authId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAuthorizationV2 godoc
|
||||||
|
//
|
||||||
|
// @Tags authorization_v2
|
||||||
|
// @Param id path int true "authorization id"
|
||||||
|
// @Success 200 {object} HttpResponse{data=model.AuthorizationV2}
|
||||||
|
// @Router /authorization_v2/:id [get]
|
||||||
|
func (c *Controller) GetAuthorizationV2(ctx *gin.Context) {
|
||||||
|
authId := cast.ToInt(ctx.Param("id"))
|
||||||
|
|
||||||
|
authV2Service := service.NewAuthorizationV2Service()
|
||||||
|
|
||||||
|
// Get the rule
|
||||||
|
auth, err := authV2Service.GetRuleById(ctx, authId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
ctx.AbortWithError(http.StatusNotFound, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||||
|
} else {
|
||||||
|
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if auth == nil {
|
||||||
|
ctx.AbortWithError(http.StatusNotFound, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": "rule not found"}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, HttpResponse{
|
||||||
|
Data: auth,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloneAuthorizationV2 godoc
|
||||||
|
//
|
||||||
|
// @Tags authorization_v2
|
||||||
|
// @Param id path int true "source authorization id"
|
||||||
|
// @Param body body object true "clone request"
|
||||||
|
// @Success 200 {object} HttpResponse{data=model.AuthorizationV2}
|
||||||
|
// @Router /authorization_v2/:id/clone [post]
|
||||||
|
func (c *Controller) CloneAuthorizationV2(ctx *gin.Context) {
|
||||||
|
sourceId := cast.ToInt(ctx.Param("id"))
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
var req struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
}
|
||||||
|
err := ctx.ShouldBindBodyWithJSON(&req)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authV2Service := service.NewAuthorizationV2Service()
|
||||||
|
|
||||||
|
// Check permissions
|
||||||
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||||
|
if !acl.IsAdmin(currentUser) {
|
||||||
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.GRANT}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone the rule
|
||||||
|
clonedRule, err := authV2Service.CloneRule(ctx, sourceId, req.Name)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
ctx.AbortWithError(http.StatusNotFound, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": "source rule not found"}})
|
||||||
|
} else {
|
||||||
|
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, HttpResponse{
|
||||||
|
Data: clonedRule,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAuthorizationsV2 godoc
|
||||||
|
//
|
||||||
|
// @Tags authorization_v2
|
||||||
|
// @Param page_index query int false "page index"
|
||||||
|
// @Param page_size query int false "page size"
|
||||||
|
// @Param enabled query bool false "filter by enabled status"
|
||||||
|
// @Param search query string false "search by name or description"
|
||||||
|
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.AuthorizationV2}}
|
||||||
|
// @Router /authorization_v2 [get]
|
||||||
|
func (c *Controller) GetAuthorizationsV2(ctx *gin.Context) {
|
||||||
|
pageIndex := cast.ToInt(ctx.DefaultQuery("page_index", "1"))
|
||||||
|
pageSize := cast.ToInt(ctx.DefaultQuery("page_size", "20"))
|
||||||
|
enabled := ctx.Query("enabled")
|
||||||
|
search := ctx.Query("search")
|
||||||
|
|
||||||
|
// Check permissions
|
||||||
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||||
|
if !acl.IsAdmin(currentUser) {
|
||||||
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.READ}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authV2Service := service.NewAuthorizationV2Service()
|
||||||
|
|
||||||
|
// Build query with filters
|
||||||
|
db, err := authV2Service.BuildQuery(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if enabled != "" {
|
||||||
|
db = db.Where("enabled = ?", enabled == "true")
|
||||||
|
}
|
||||||
|
if search != "" {
|
||||||
|
db = db.Where("name LIKE ? OR description LIKE ?", "%"+search+"%", "%"+search+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total count
|
||||||
|
var count int64
|
||||||
|
if err := db.Count(&count).Error; err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply pagination
|
||||||
|
offset := (pageIndex - 1) * pageSize
|
||||||
|
var auths []*model.AuthorizationV2
|
||||||
|
if err := db.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&auths).Error; err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to slice of any type
|
||||||
|
authsAny := make([]any, 0, len(auths))
|
||||||
|
for _, a := range auths {
|
||||||
|
authsAny = append(authsAny, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &ListData{
|
||||||
|
Count: count,
|
||||||
|
List: authsAny,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, HttpResponse{
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckPermissionV2 godoc
|
||||||
|
//
|
||||||
|
// @Tags authorization_v2
|
||||||
|
// @Param request body CheckPermissionRequest true "permission check request"
|
||||||
|
// @Success 200 {object} HttpResponse{data=model.AuthResult}
|
||||||
|
// @Router /authorization_v2/check [post]
|
||||||
|
func (c *Controller) CheckPermissionV2(ctx *gin.Context) {
|
||||||
|
var req CheckPermissionRequest
|
||||||
|
if err := ctx.ShouldBindBodyWithJSON(&req); err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := service.DefaultAuthService.CheckPermission(ctx, req.NodeId, req.AssetId, req.AccountId, req.Action)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, HttpResponse{Data: result})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckPermissionRequest represents a permission check request
|
||||||
|
type CheckPermissionRequest struct {
|
||||||
|
NodeId int `json:"node_id" binding:"gte=0"`
|
||||||
|
AssetId int `json:"asset_id" binding:"gte=0"`
|
||||||
|
AccountId int `json:"account_id" binding:"gte=0"`
|
||||||
|
Action model.AuthAction `json:"action" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizationV2 hooks
|
||||||
|
var (
|
||||||
|
authV2PreHooks = []preHook[*model.AuthorizationV2]{
|
||||||
|
func(ctx *gin.Context, data *model.AuthorizationV2) {
|
||||||
|
authV2Service := service.NewAuthorizationV2Service()
|
||||||
|
if err := authV2Service.ValidateRule(ctx, data); err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err.Error()}})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
authV2PostHooks = []postHook[*model.AuthorizationV2]{
|
||||||
|
func(ctx *gin.Context, data []*model.AuthorizationV2) {
|
||||||
|
// Add any post-processing logic here
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
@@ -435,10 +435,6 @@ func hasPerm[T model.Model](ctx context.Context, md T, resourceTypeName, action
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handlePermissions[T any](ctx *gin.Context, data []T, resourceTypeName string) (err error) {
|
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)
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||||
|
|
||||||
if !lo.Contains(config.PermResource, resourceTypeName) {
|
if !lo.Contains(config.PermResource, resourceTypeName) {
|
||||||
|
@@ -54,6 +54,7 @@ func (c *Controller) UpdateCommandTemplate(ctx *gin.Context) {
|
|||||||
// @Tags command_template
|
// @Tags command_template
|
||||||
// @Param page_index query int false "page index"
|
// @Param page_index query int false "page index"
|
||||||
// @Param page_size query int false "page size"
|
// @Param page_size query int false "page size"
|
||||||
|
// @Param search query string false "search by name or description"
|
||||||
// @Param category query string false "template category"
|
// @Param category query string false "template category"
|
||||||
// @Param builtin query bool false "filter by builtin status"
|
// @Param builtin query bool false "filter by builtin status"
|
||||||
// @Param info query bool false "info mode"
|
// @Param info query bool false "info mode"
|
||||||
@@ -62,23 +63,13 @@ func (c *Controller) UpdateCommandTemplate(ctx *gin.Context) {
|
|||||||
func (c *Controller) GetCommandTemplates(ctx *gin.Context) {
|
func (c *Controller) GetCommandTemplates(ctx *gin.Context) {
|
||||||
info := cast.ToBool(ctx.Query("info"))
|
info := cast.ToBool(ctx.Query("info"))
|
||||||
|
|
||||||
// Build base query using service layer
|
// Build base query using service layer with all filters
|
||||||
db, err := commandTemplateService.BuildQuery(ctx)
|
db, err := commandTemplateService.BuildQuery(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AbortWithError(http.StatusInternalServerError, &pkgErrors.ApiError{Code: pkgErrors.ErrInternal, Data: map[string]any{"err": err}})
|
ctx.AbortWithError(http.StatusInternalServerError, &pkgErrors.ApiError{Code: pkgErrors.ErrInternal, Data: map[string]any{"err": err}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply filters
|
|
||||||
if category := ctx.Query("category"); category != "" {
|
|
||||||
db = db.Where("category = ?", category)
|
|
||||||
}
|
|
||||||
|
|
||||||
if builtinStr := ctx.Query("builtin"); builtinStr != "" {
|
|
||||||
builtin := cast.ToBool(builtinStr)
|
|
||||||
db = db.Where("is_builtin = ?", builtin)
|
|
||||||
}
|
|
||||||
|
|
||||||
doGet(ctx, !info, db, config.RESOURCE_AUTHORIZATION, commandTemplatePostHooks...)
|
doGet(ctx, !info, db, config.RESOURCE_AUTHORIZATION, commandTemplatePostHooks...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -63,10 +63,14 @@ func (c *Controller) GetConfig(ctx *gin.Context) {
|
|||||||
|
|
||||||
cfg, err := configService.GetConfig(ctx)
|
cfg, err := configService.GetConfig(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
// Return default configuration if no config exists
|
||||||
|
defaultCfg := model.GetDefaultConfig()
|
||||||
|
ctx.JSON(http.StatusOK, NewHttpResponseWithData(defaultCfg))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(cfg))
|
ctx.JSON(http.StatusOK, NewHttpResponseWithData(cfg))
|
||||||
|
@@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/veops/oneterm/internal/acl"
|
"github.com/veops/oneterm/internal/acl"
|
||||||
"github.com/veops/oneterm/internal/guacd"
|
"github.com/veops/oneterm/internal/guacd"
|
||||||
"github.com/veops/oneterm/internal/model"
|
"github.com/veops/oneterm/internal/model"
|
||||||
|
"github.com/veops/oneterm/internal/service"
|
||||||
fileservice "github.com/veops/oneterm/internal/service/file"
|
fileservice "github.com/veops/oneterm/internal/service/file"
|
||||||
gsession "github.com/veops/oneterm/internal/session"
|
gsession "github.com/veops/oneterm/internal/session"
|
||||||
myErrors "github.com/veops/oneterm/pkg/errors"
|
myErrors "github.com/veops/oneterm/pkg/errors"
|
||||||
@@ -78,10 +79,11 @@ func (c *Controller) FileLS(ctx *gin.Context) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok, err := hasAuthorization(ctx, sess); err != nil {
|
// Check connect permission first
|
||||||
|
if result, err := service.DefaultAuthService.HasAuthorizationV2(ctx, sess, model.ActionConnect); err != nil {
|
||||||
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||||
return
|
return
|
||||||
} else if !ok {
|
} else if !result.IsAllowed(model.ActionConnect) {
|
||||||
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{}})
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -148,11 +150,12 @@ func (c *Controller) FileMkdir(ctx *gin.Context) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok, err := hasAuthorization(ctx, sess); err != nil {
|
// Check file upload permission (mkdir is considered an upload operation)
|
||||||
|
if result, err := service.DefaultAuthService.HasAuthorizationV2(ctx, sess, model.ActionFileUpload); err != nil {
|
||||||
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||||
return
|
return
|
||||||
} else if !ok {
|
} else if !result.IsAllowed(model.ActionFileUpload) {
|
||||||
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{}})
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"message": "No file upload permission"}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,11 +192,12 @@ func (c *Controller) FileUpload(ctx *gin.Context) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok, err := hasAuthorization(ctx, sess); err != nil {
|
// Check file upload permission using V2 system
|
||||||
|
if result, err := service.DefaultAuthService.HasAuthorizationV2(ctx, sess, model.ActionFileUpload); err != nil {
|
||||||
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||||
return
|
return
|
||||||
} else if !ok {
|
} else if !result.IsAllowed(model.ActionFileUpload) {
|
||||||
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{}})
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"message": "No file upload permission"}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,11 +354,12 @@ func (c *Controller) FileDownload(ctx *gin.Context) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok, err := hasAuthorization(ctx, sess); err != nil {
|
// Check file download permission using V2 system
|
||||||
|
if result, err := service.DefaultAuthService.HasAuthorizationV2(ctx, sess, model.ActionFileDownload); err != nil {
|
||||||
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||||
return
|
return
|
||||||
} else if !ok {
|
} else if !result.IsAllowed(model.ActionFileDownload) {
|
||||||
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{}})
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"message": "No file download permission"}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,7 +436,7 @@ func (c *Controller) RDPFileList(ctx *gin.Context) {
|
|||||||
sessionId := ctx.Param("session_id")
|
sessionId := ctx.Param("session_id")
|
||||||
path := ctx.DefaultQuery("path", "/")
|
path := ctx.DefaultQuery("path", "/")
|
||||||
|
|
||||||
tunnel, err := c.validateRDPAccess(ctx, sessionId)
|
tunnel, authResult, err := c.validateRDPAccess(ctx, sessionId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "permission") {
|
if strings.Contains(err.Error(), "permission") {
|
||||||
ctx.JSON(http.StatusForbidden, HttpResponse{
|
ctx.JSON(http.StatusForbidden, HttpResponse{
|
||||||
@@ -447,6 +452,9 @@ func (c *Controller) RDPFileList(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Connect permission already validated in validateRDPAccess
|
||||||
|
_ = authResult // Use the already validated result
|
||||||
|
|
||||||
// Check if RDP drive is enabled
|
// Check if RDP drive is enabled
|
||||||
if !fileservice.IsRDPDriveEnabled(tunnel) {
|
if !fileservice.IsRDPDriveEnabled(tunnel) {
|
||||||
ctx.JSON(http.StatusBadRequest, HttpResponse{
|
ctx.JSON(http.StatusBadRequest, HttpResponse{
|
||||||
@@ -503,7 +511,7 @@ func (c *Controller) RDPFileUpload(ctx *gin.Context) {
|
|||||||
// Create progress record IMMEDIATELY when request starts
|
// Create progress record IMMEDIATELY when request starts
|
||||||
fileservice.CreateTransferProgress(transferId, "rdp")
|
fileservice.CreateTransferProgress(transferId, "rdp")
|
||||||
|
|
||||||
tunnel, err := c.validateRDPAccess(ctx, sessionId)
|
tunnel, authResult, err := c.validateRDPAccess(ctx, sessionId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "permission") {
|
if strings.Contains(err.Error(), "permission") {
|
||||||
ctx.JSON(http.StatusForbidden, HttpResponse{
|
ctx.JSON(http.StatusForbidden, HttpResponse{
|
||||||
@@ -519,6 +527,15 @@ func (c *Controller) RDPFileUpload(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check specific file upload permission using already validated result
|
||||||
|
if !authResult.IsAllowed(model.ActionFileUpload) {
|
||||||
|
ctx.JSON(http.StatusForbidden, HttpResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Message: "No file upload permission",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !fileservice.IsRDPDriveEnabled(tunnel) {
|
if !fileservice.IsRDPDriveEnabled(tunnel) {
|
||||||
logger.L().Error("RDP drive is not enabled for session", zap.String("sessionId", sessionId))
|
logger.L().Error("RDP drive is not enabled for session", zap.String("sessionId", sessionId))
|
||||||
ctx.JSON(http.StatusBadRequest, HttpResponse{
|
ctx.JSON(http.StatusBadRequest, HttpResponse{
|
||||||
@@ -726,7 +743,7 @@ func (c *Controller) RDPFileUpload(ctx *gin.Context) {
|
|||||||
func (c *Controller) RDPFileDownload(ctx *gin.Context) {
|
func (c *Controller) RDPFileDownload(ctx *gin.Context) {
|
||||||
sessionId := ctx.Param("session_id")
|
sessionId := ctx.Param("session_id")
|
||||||
|
|
||||||
tunnel, validationErr := c.validateRDPAccess(ctx, sessionId)
|
tunnel, authResult, validationErr := c.validateRDPAccess(ctx, sessionId)
|
||||||
if validationErr != nil {
|
if validationErr != nil {
|
||||||
if strings.Contains(validationErr.Error(), "permission") {
|
if strings.Contains(validationErr.Error(), "permission") {
|
||||||
ctx.JSON(http.StatusForbidden, HttpResponse{
|
ctx.JSON(http.StatusForbidden, HttpResponse{
|
||||||
@@ -742,6 +759,15 @@ func (c *Controller) RDPFileDownload(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check specific file download permission using already validated result
|
||||||
|
if !authResult.IsAllowed(model.ActionFileDownload) {
|
||||||
|
ctx.JSON(http.StatusForbidden, HttpResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Message: "No file download permission",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !fileservice.IsRDPDriveEnabled(tunnel) {
|
if !fileservice.IsRDPDriveEnabled(tunnel) {
|
||||||
ctx.JSON(http.StatusForbidden, HttpResponse{
|
ctx.JSON(http.StatusForbidden, HttpResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
@@ -876,7 +902,7 @@ func (c *Controller) RDPFileMkdir(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tunnel, validateErr := c.validateRDPAccess(ctx, sessionId)
|
tunnel, authResult, validateErr := c.validateRDPAccess(ctx, sessionId)
|
||||||
if validateErr != nil {
|
if validateErr != nil {
|
||||||
if strings.Contains(validateErr.Error(), "permission") {
|
if strings.Contains(validateErr.Error(), "permission") {
|
||||||
ctx.JSON(http.StatusForbidden, HttpResponse{
|
ctx.JSON(http.StatusForbidden, HttpResponse{
|
||||||
@@ -892,6 +918,15 @@ func (c *Controller) RDPFileMkdir(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check specific file upload permission using already validated result (mkdir is considered an upload operation)
|
||||||
|
if !authResult.IsAllowed(model.ActionFileUpload) {
|
||||||
|
ctx.JSON(http.StatusForbidden, HttpResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Message: "No file upload permission",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Check if upload is allowed (mkdir is considered an upload operation)
|
// Check if upload is allowed (mkdir is considered an upload operation)
|
||||||
if !fileservice.IsRDPUploadAllowed(tunnel) {
|
if !fileservice.IsRDPUploadAllowed(tunnel) {
|
||||||
ctx.JSON(http.StatusForbidden, HttpResponse{
|
ctx.JSON(http.StatusForbidden, HttpResponse{
|
||||||
@@ -927,23 +962,37 @@ func (c *Controller) RDPFileMkdir(ctx *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) validateRDPAccess(ctx *gin.Context, sessionId string) (*guacd.Tunnel, error) {
|
func (c *Controller) validateRDPAccess(ctx *gin.Context, sessionId string) (*guacd.Tunnel, *model.BatchAuthResult, error) {
|
||||||
currentUser, err := acl.GetSessionFromCtx(ctx)
|
currentUser, err := acl.GetSessionFromCtx(ctx)
|
||||||
if err != nil || currentUser == nil {
|
if err != nil || currentUser == nil {
|
||||||
return nil, fmt.Errorf("no permission to access this session")
|
return nil, nil, fmt.Errorf("no permission to access this session")
|
||||||
}
|
}
|
||||||
|
|
||||||
onlineSession := gsession.GetOnlineSessionById(sessionId)
|
onlineSession := gsession.GetOnlineSessionById(sessionId)
|
||||||
if onlineSession == nil {
|
if onlineSession == nil {
|
||||||
return nil, fmt.Errorf("session not found or not active")
|
return nil, nil, fmt.Errorf("session not found or not active")
|
||||||
}
|
}
|
||||||
|
|
||||||
tunnel := onlineSession.GuacdTunnel
|
tunnel := onlineSession.GuacdTunnel
|
||||||
if tunnel == nil {
|
if tunnel == nil {
|
||||||
return nil, fmt.Errorf("session not found or not active")
|
return nil, nil, fmt.Errorf("session not found or not active")
|
||||||
}
|
}
|
||||||
|
|
||||||
return tunnel, nil
|
// V2 authorization check for RDP file operations
|
||||||
|
// Check connect permission and file permissions using batch check
|
||||||
|
result, err := service.DefaultAuthService.HasAuthorizationV2(ctx, onlineSession,
|
||||||
|
model.ActionConnect, model.ActionFileUpload, model.ActionFileDownload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("authorization check failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must have connect permission to access the session
|
||||||
|
if !result.IsAllowed(model.ActionConnect) {
|
||||||
|
return nil, nil, fmt.Errorf("no permission to access this session")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return tunnel and permission result for efficient reuse
|
||||||
|
return tunnel, result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -985,11 +1034,11 @@ func (c *Controller) SftpFileLS(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check authorization using the same logic as legacy API
|
// Check connect permission for SFTP file listing
|
||||||
if ok, err := hasAuthorization(ctx, onlineSession); err != nil {
|
if result, err := service.DefaultAuthService.HasAuthorizationV2(ctx, onlineSession, model.ActionConnect); err != nil {
|
||||||
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||||
return
|
return
|
||||||
} else if !ok {
|
} else if !result.IsAllowed(model.ActionConnect) {
|
||||||
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{}})
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1073,12 +1122,12 @@ func (c *Controller) SftpFileMkdir(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check authorization using the same logic as legacy API
|
// Check file upload permission for SFTP mkdir
|
||||||
if ok, err := hasAuthorization(ctx, onlineSession); err != nil {
|
if result, err := service.DefaultAuthService.HasAuthorizationV2(ctx, onlineSession, model.ActionFileUpload); err != nil {
|
||||||
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||||
return
|
return
|
||||||
} else if !ok {
|
} else if !result.IsAllowed(model.ActionFileUpload) {
|
||||||
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{}})
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"message": "No file upload permission"}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1239,11 +1288,12 @@ func (c *Controller) SftpFileUpload(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok, err := hasAuthorization(ctx, onlineSession); err != nil {
|
// Check file upload permission for SFTP upload
|
||||||
|
if result, err := service.DefaultAuthService.HasAuthorizationV2(ctx, onlineSession, model.ActionFileUpload); err != nil {
|
||||||
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||||
return
|
return
|
||||||
} else if !ok {
|
} else if !result.IsAllowed(model.ActionFileUpload) {
|
||||||
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{}})
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"message": "No file upload permission"}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1444,12 +1494,12 @@ func (c *Controller) SftpFileDownload(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check authorization using the same logic as legacy API
|
// Check file download permission for SFTP download
|
||||||
if ok, err := hasAuthorization(ctx, onlineSession); err != nil {
|
if result, err := service.DefaultAuthService.HasAuthorizationV2(ctx, onlineSession, model.ActionFileDownload); err != nil {
|
||||||
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||||
return
|
return
|
||||||
} else if !ok {
|
} else if !result.IsAllowed(model.ActionFileDownload) {
|
||||||
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{}})
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"message": "No file download permission"}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -111,7 +111,9 @@ func (c *Controller) GetGateways(ctx *gin.Context) {
|
|||||||
|
|
||||||
// Apply authorization filter if needed
|
// Apply authorization filter if needed
|
||||||
if info && !acl.IsAdmin(currentUser) {
|
if info && !acl.IsAdmin(currentUser) {
|
||||||
assetIds, err := GetAssetIdsByAuthorization(ctx)
|
// Use V2 authorization system for asset filtering
|
||||||
|
authV2Service := service.NewAuthorizationV2Service()
|
||||||
|
_, assetIds, _, err := authV2Service.GetAuthorizationScopeByACL(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AbortWithError(http.StatusInternalServerError, &errors.ApiError{Code: errors.ErrInternal, Data: map[string]any{"err": err}})
|
ctx.AbortWithError(http.StatusInternalServerError, &errors.ApiError{Code: errors.ErrInternal, Data: map[string]any{"err": err}})
|
||||||
return
|
return
|
||||||
|
@@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
|
|
||||||
"github.com/veops/oneterm/internal/acl"
|
|
||||||
"github.com/veops/oneterm/internal/model"
|
"github.com/veops/oneterm/internal/model"
|
||||||
"github.com/veops/oneterm/internal/repository"
|
"github.com/veops/oneterm/internal/repository"
|
||||||
"github.com/veops/oneterm/internal/service"
|
"github.com/veops/oneterm/internal/service"
|
||||||
@@ -82,16 +81,21 @@ func (c *Controller) UpdateNode(ctx *gin.Context) {
|
|||||||
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Node}}
|
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Node}}
|
||||||
// @Router /node [get]
|
// @Router /node [get]
|
||||||
func (c *Controller) GetNodes(ctx *gin.Context) {
|
func (c *Controller) GetNodes(ctx *gin.Context) {
|
||||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
||||||
info := cast.ToBool(ctx.Query("info"))
|
info := cast.ToBool(ctx.Query("info"))
|
||||||
recursive := cast.ToBool(ctx.Query("recursive"))
|
recursive := cast.ToBool(ctx.Query("recursive"))
|
||||||
|
|
||||||
db, err := nodeService.BuildQuery(ctx, currentUser, info)
|
// Build query with integrated V2 authorization filter
|
||||||
|
db, err := nodeService.BuildQueryWithAuthorization(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AbortWithError(http.StatusInternalServerError, &errors.ApiError{Code: errors.ErrInternal, Data: map[string]any{"err": err}})
|
ctx.AbortWithError(http.StatusInternalServerError, &errors.ApiError{Code: errors.ErrInternal, Data: map[string]any{"err": err}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply info mode settings
|
||||||
|
if info {
|
||||||
|
db = db.Select("id", "parent_id", "name")
|
||||||
|
}
|
||||||
|
|
||||||
if recursive {
|
if recursive {
|
||||||
treeNodes, err := nodeService.GetNodesTree(ctx, db, !info, config.RESOURCE_NODE)
|
treeNodes, err := nodeService.GetNodesTree(ctx, db, !info, config.RESOURCE_NODE)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
198
backend/internal/api/controller/time_template.go
Normal file
198
backend/internal/api/controller/time_template.go
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
|
||||||
|
"github.com/veops/oneterm/internal/acl"
|
||||||
|
"github.com/veops/oneterm/internal/model"
|
||||||
|
"github.com/veops/oneterm/internal/service"
|
||||||
|
pkgErrors "github.com/veops/oneterm/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
timeTemplateService = service.NewTimeTemplateService()
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateTimeTemplate godoc
|
||||||
|
//
|
||||||
|
// @Tags time_template
|
||||||
|
// @Param template body model.TimeTemplate true "time template"
|
||||||
|
// @Success 200 {object} HttpResponse
|
||||||
|
// @Router /time_template [post]
|
||||||
|
func (c *Controller) CreateTimeTemplate(ctx *gin.Context) {
|
||||||
|
// Time templates require admin permission - no ACL needed
|
||||||
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||||
|
if !acl.IsAdmin(currentUser) {
|
||||||
|
ctx.AbortWithError(http.StatusForbidden, &pkgErrors.ApiError{Code: pkgErrors.ErrNoPerm, Data: map[string]any{"perm": "admin"}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
template := &model.TimeTemplate{}
|
||||||
|
doCreate(ctx, false, template, "", timeTemplatePreHooks...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTimeTemplate godoc
|
||||||
|
//
|
||||||
|
// @Tags time_template
|
||||||
|
// @Param id path int true "template id"
|
||||||
|
// @Success 200 {object} HttpResponse
|
||||||
|
// @Router /time_template/:id [delete]
|
||||||
|
func (c *Controller) DeleteTimeTemplate(ctx *gin.Context) {
|
||||||
|
// Time templates require admin permission - no ACL needed
|
||||||
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||||
|
if !acl.IsAdmin(currentUser) {
|
||||||
|
ctx.AbortWithError(http.StatusForbidden, &pkgErrors.ApiError{Code: pkgErrors.ErrNoPerm, Data: map[string]any{"perm": "admin"}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
doDelete(ctx, false, &model.TimeTemplate{}, "", timeTemplateDeleteChecks...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTimeTemplate godoc
|
||||||
|
//
|
||||||
|
// @Tags time_template
|
||||||
|
// @Param id path int true "template id"
|
||||||
|
// @Param template body model.TimeTemplate true "time template"
|
||||||
|
// @Success 200 {object} HttpResponse
|
||||||
|
// @Router /time_template/:id [put]
|
||||||
|
func (c *Controller) UpdateTimeTemplate(ctx *gin.Context) {
|
||||||
|
// Time templates require admin permission - no ACL needed
|
||||||
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||||
|
if !acl.IsAdmin(currentUser) {
|
||||||
|
ctx.AbortWithError(http.StatusForbidden, &pkgErrors.ApiError{Code: pkgErrors.ErrNoPerm, Data: map[string]any{"perm": "admin"}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
template := &model.TimeTemplate{}
|
||||||
|
doUpdate(ctx, false, template, "", timeTemplatePreHooks...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimeTemplates godoc
|
||||||
|
//
|
||||||
|
// @Tags time_template
|
||||||
|
// @Param page_index query int false "page index"
|
||||||
|
// @Param page_size query int false "page size"
|
||||||
|
// @Param search query string false "search by name or description"
|
||||||
|
// @Param category query string false "template category"
|
||||||
|
// @Param active query bool false "filter by active status"
|
||||||
|
// @Param info query bool false "info mode"
|
||||||
|
// @Success 200 {object} HttpResponse{data=[]model.TimeTemplate}
|
||||||
|
// @Router /time_template [get]
|
||||||
|
func (c *Controller) GetTimeTemplates(ctx *gin.Context) {
|
||||||
|
info := cast.ToBool(ctx.Query("info"))
|
||||||
|
|
||||||
|
// Build base query using service layer with all filters
|
||||||
|
db, err := timeTemplateService.BuildQuery(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusInternalServerError, &pkgErrors.ApiError{Code: pkgErrors.ErrInternal, Data: map[string]any{"err": err}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
doGet(ctx, !info, db, "", timeTemplatePostHooks...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBuiltInTimeTemplates godoc
|
||||||
|
//
|
||||||
|
// @Tags time_template
|
||||||
|
// @Success 200 {object} HttpResponse{data=[]model.TimeTemplate}
|
||||||
|
// @Router /time_template/builtin [get]
|
||||||
|
func (c *Controller) GetBuiltInTimeTemplates(ctx *gin.Context) {
|
||||||
|
templates, err := timeTemplateService.GetBuiltInTemplates(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusInternalServerError, &pkgErrors.ApiError{Code: pkgErrors.ErrInternal, Data: map[string]any{"err": err}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, NewHttpResponseWithData(templates))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckTimeAccess godoc
|
||||||
|
//
|
||||||
|
// @Tags time_template
|
||||||
|
// @Param request body CheckTimeAccessRequest true "time access check request"
|
||||||
|
// @Success 200 {object} HttpResponse{data=CheckTimeAccessResponse}
|
||||||
|
// @Router /time_template/check [post]
|
||||||
|
func (c *Controller) CheckTimeAccess(ctx *gin.Context) {
|
||||||
|
var req CheckTimeAccessRequest
|
||||||
|
if err := ctx.ShouldBindBodyWithJSON(&req); err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusBadRequest, &pkgErrors.ApiError{Code: pkgErrors.ErrInvalidArgument})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allowed, err := timeTemplateService.CheckTimeAccess(ctx, req.TemplateID, req.Timezone)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusInternalServerError, &pkgErrors.ApiError{Code: pkgErrors.ErrInternal, Data: map[string]any{"err": err}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := CheckTimeAccessResponse{
|
||||||
|
Allowed: allowed,
|
||||||
|
TemplateID: req.TemplateID,
|
||||||
|
Timezone: req.Timezone,
|
||||||
|
CheckedAt: "now",
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, NewHttpResponseWithData(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitBuiltInTemplates godoc
|
||||||
|
//
|
||||||
|
// @Tags time_template
|
||||||
|
// @Success 200 {object} HttpResponse
|
||||||
|
// @Router /time_template/init [post]
|
||||||
|
func (c *Controller) InitBuiltInTemplates(ctx *gin.Context) {
|
||||||
|
if err := timeTemplateService.InitializeBuiltInTemplates(ctx); err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusInternalServerError, &pkgErrors.ApiError{Code: pkgErrors.ErrInternal, Data: map[string]any{"err": err}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, NewHttpResponseWithData("Built-in time templates initialized successfully"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckTimeAccessRequest represents a time access check request
|
||||||
|
type CheckTimeAccessRequest struct {
|
||||||
|
TemplateID int `json:"template_id" binding:"required,gt=0"`
|
||||||
|
Timezone string `json:"timezone"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckTimeAccessResponse represents a time access check response
|
||||||
|
type CheckTimeAccessResponse struct {
|
||||||
|
Allowed bool `json:"allowed"`
|
||||||
|
TemplateID int `json:"template_id"`
|
||||||
|
Timezone string `json:"timezone"`
|
||||||
|
CheckedAt string `json:"checked_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time template hooks
|
||||||
|
var (
|
||||||
|
timeTemplatePreHooks = []preHook[*model.TimeTemplate]{
|
||||||
|
func(ctx *gin.Context, data *model.TimeTemplate) {
|
||||||
|
if err := timeTemplateService.ValidateTimeTemplate(data); err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusBadRequest, &pkgErrors.ApiError{Code: pkgErrors.ErrInvalidArgument, Data: map[string]any{"err": err.Error()}})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
timeTemplatePostHooks = []postHook[*model.TimeTemplate]{
|
||||||
|
func(ctx *gin.Context, data []*model.TimeTemplate) {
|
||||||
|
// Add any post-processing logic here
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
timeTemplateDeleteChecks = []deleteCheck{
|
||||||
|
func(ctx *gin.Context, id int) {
|
||||||
|
// Check if template is built-in
|
||||||
|
template, err := timeTemplateService.GetTimeTemplate(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusInternalServerError, &pkgErrors.ApiError{Code: pkgErrors.ErrInternal, Data: map[string]any{"err": err}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if template != nil && template.IsBuiltIn {
|
||||||
|
ctx.AbortWithError(http.StatusBadRequest, &pkgErrors.ApiError{Code: pkgErrors.ErrInvalidArgument, Data: map[string]any{"err": "cannot delete built-in time template"}})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,7 @@ func SetupRouter(r *gin.Engine) {
|
|||||||
asset.DELETE("/:id", c.DeleteAsset)
|
asset.DELETE("/:id", c.DeleteAsset)
|
||||||
asset.PUT("/:id", c.UpdateAsset)
|
asset.PUT("/:id", c.UpdateAsset)
|
||||||
asset.GET("", c.GetAssets)
|
asset.GET("", c.GetAssets)
|
||||||
|
asset.GET("/:id/permissions", c.GetAssetPermissions)
|
||||||
}
|
}
|
||||||
|
|
||||||
node := v1.Group("node")
|
node := v1.Group("node")
|
||||||
@@ -147,10 +148,21 @@ func SetupRouter(r *gin.Engine) {
|
|||||||
authorization := v1.Group("/authorization")
|
authorization := v1.Group("/authorization")
|
||||||
{
|
{
|
||||||
authorization.POST("", c.UpsertAuthorization)
|
authorization.POST("", c.UpsertAuthorization)
|
||||||
authorization.DELETE("/:id", c.DeleteAccount)
|
authorization.DELETE("/:id", c.DeleteAuthorization)
|
||||||
authorization.GET("", c.GetAuthorizations)
|
authorization.GET("", c.GetAuthorizations)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authorizationV2 := v1.Group("/authorization_v2")
|
||||||
|
{
|
||||||
|
authorizationV2.POST("", c.CreateAuthorizationV2)
|
||||||
|
authorizationV2.GET("", c.GetAuthorizationsV2)
|
||||||
|
authorizationV2.GET("/:id", c.GetAuthorizationV2)
|
||||||
|
authorizationV2.PUT("/:id", c.UpdateAuthorizationV2)
|
||||||
|
authorizationV2.DELETE("/:id", c.DeleteAuthorizationV2)
|
||||||
|
authorizationV2.POST("/:id/clone", c.CloneAuthorizationV2)
|
||||||
|
authorizationV2.POST("/check", c.CheckPermissionV2)
|
||||||
|
}
|
||||||
|
|
||||||
quickCommand := v1.Group("/quick_command")
|
quickCommand := v1.Group("/quick_command")
|
||||||
{
|
{
|
||||||
quickCommand.POST("", c.CreateQuickCommand)
|
quickCommand.POST("", c.CreateQuickCommand)
|
||||||
@@ -189,5 +201,28 @@ func SetupRouter(r *gin.Engine) {
|
|||||||
storage.PUT("/configs/:id/set-primary", c.SetPrimaryStorage)
|
storage.PUT("/configs/:id/set-primary", c.SetPrimaryStorage)
|
||||||
storage.PUT("/configs/:id/toggle", c.ToggleStorageProvider)
|
storage.PUT("/configs/:id/toggle", c.ToggleStorageProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Time template management routes
|
||||||
|
timeTemplate := v1.Group("/time_template")
|
||||||
|
{
|
||||||
|
timeTemplate.POST("", c.CreateTimeTemplate)
|
||||||
|
timeTemplate.DELETE("/:id", c.DeleteTimeTemplate)
|
||||||
|
timeTemplate.PUT("/:id", c.UpdateTimeTemplate)
|
||||||
|
timeTemplate.GET("", c.GetTimeTemplates)
|
||||||
|
timeTemplate.GET("/builtin", c.GetBuiltInTimeTemplates)
|
||||||
|
timeTemplate.POST("/check", c.CheckTimeAccess)
|
||||||
|
timeTemplate.POST("/init", c.InitBuiltInTemplates)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command template management routes
|
||||||
|
commandTemplate := v1.Group("/command_template")
|
||||||
|
{
|
||||||
|
commandTemplate.POST("", c.CreateCommandTemplate)
|
||||||
|
commandTemplate.DELETE("/:id", c.DeleteCommandTemplate)
|
||||||
|
commandTemplate.PUT("/:id", c.UpdateCommandTemplate)
|
||||||
|
commandTemplate.GET("", c.GetCommandTemplates)
|
||||||
|
commandTemplate.GET("/builtin", c.GetBuiltInCommandTemplates)
|
||||||
|
commandTemplate.GET("/:id/commands", c.GetTemplateCommands)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -207,41 +206,61 @@ func DoConnect(ctx *gin.Context, ws *websocket.Conn) (sess *gsession.Session, er
|
|||||||
sess.SshParser = gsession.NewParser(sess.SessionId, w, h)
|
sess.SshParser = gsession.NewParser(sess.SessionId, w, h)
|
||||||
sess.SshParser.Protocol = sess.Protocol
|
sess.SshParser.Protocol = sess.Protocol
|
||||||
|
|
||||||
sessionService := service.NewSessionService()
|
// Use V2 command analyzer instead of legacy method
|
||||||
cmds, err := sessionService.GetSshParserCommands(ctx, []int(asset.AccessAuth.CmdIds))
|
commandAnalyzer := service.NewCommandAnalyzer()
|
||||||
|
cmds, err := commandAnalyzer.AnalyzeSessionCommands(ctx, sess)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sess, err
|
logger.L().Error("Failed to analyze session commands", zap.String("sessionId", sess.SessionId), zap.Error(err))
|
||||||
|
// Continue with empty command list (no command restrictions)
|
||||||
|
cmds = []*model.Command{}
|
||||||
}
|
}
|
||||||
sess.SshParser.Cmds = cmds
|
sess.SshParser.Cmds = cmds
|
||||||
|
|
||||||
for _, c := range sess.SshParser.Cmds {
|
|
||||||
if c.IsRe {
|
|
||||||
c.Re, _ = regexp.Compile(c.Cmd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if sess.SshRecoder, err = gsession.NewAsciinema(sess.SessionId, w, h); err != nil {
|
if sess.SshRecoder, err = gsession.NewAsciinema(sess.SessionId, w, h); err != nil {
|
||||||
return sess, err
|
return sess, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if sess.SessionType == model.SESSIONTYPE_WEB {
|
switch sess.SessionType {
|
||||||
|
case model.SESSIONTYPE_WEB:
|
||||||
sess.ClientIp = ctx.ClientIP()
|
sess.ClientIp = ctx.ClientIP()
|
||||||
} else if sess.SessionType == model.SESSIONTYPE_CLIENT {
|
case model.SESSIONTYPE_CLIENT:
|
||||||
sess.ClientIp = ctx.RemoteIP()
|
sess.ClientIp = ctx.RemoteIP()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !protocols.CheckTime(asset.AccessAuth) {
|
// V2 authorization check - determine required permissions based on protocol
|
||||||
err = &myErrors.ApiError{Code: myErrors.ErrAccessTime}
|
protocol := strings.Split(sess.Protocol, ":")[0]
|
||||||
return
|
var requiredActions []model.AuthAction
|
||||||
|
|
||||||
|
// All protocols need connect permission
|
||||||
|
requiredActions = append(requiredActions, model.ActionConnect)
|
||||||
|
|
||||||
|
// SSH protocol needs file permissions since it will initialize SFTP client
|
||||||
|
if protocol == "ssh" {
|
||||||
|
requiredActions = append(requiredActions, model.ActionFileUpload, model.ActionFileDownload)
|
||||||
}
|
}
|
||||||
if ok, err := service.DefaultAuthService.HasAuthorization(ctx, sess); err != nil {
|
|
||||||
|
// RDP/VNC are handled separately in ConnectGuacd with their own batch permission check
|
||||||
|
// but we still check connect permission here for consistency
|
||||||
|
|
||||||
|
result, err := service.DefaultAuthService.HasAuthorizationV2(ctx, sess, requiredActions...)
|
||||||
|
if err != nil {
|
||||||
err = &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}}
|
err = &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}}
|
||||||
return sess, err
|
return sess, err
|
||||||
} else if !ok {
|
}
|
||||||
|
|
||||||
|
// Check connect permission (required for all protocols)
|
||||||
|
if !result.IsAllowed(model.ActionConnect) {
|
||||||
err = &myErrors.ApiError{Code: myErrors.ErrUnauthorized, Data: map[string]any{"perm": "connect"}}
|
err = &myErrors.ApiError{Code: myErrors.ErrUnauthorized, Data: map[string]any{"perm": "connect"}}
|
||||||
return sess, err
|
return sess, err
|
||||||
}
|
}
|
||||||
|
|
||||||
switch strings.Split(sess.Protocol, ":")[0] {
|
// For SSH, check if user has any file permissions before initializing SFTP
|
||||||
|
hasFilePermissions := false
|
||||||
|
if protocol == "ssh" {
|
||||||
|
hasFilePermissions = result.IsAllowed(model.ActionFileUpload) || result.IsAllowed(model.ActionFileDownload)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch protocol {
|
||||||
case "ssh":
|
case "ssh":
|
||||||
go protocols.ConnectSsh(ctx, sess, asset, account, gateway)
|
go protocols.ConnectSsh(ctx, sess, asset, account, gateway)
|
||||||
case "redis", "mysql", "mongodb", "postgresql":
|
case "redis", "mysql", "mongodb", "postgresql":
|
||||||
@@ -263,10 +282,10 @@ func DoConnect(ctx *gin.Context, ws *websocket.Conn) (sess *gsession.Session, er
|
|||||||
gsession.GetOnlineSession().Store(sess.SessionId, sess)
|
gsession.GetOnlineSession().Store(sess.SessionId, sess)
|
||||||
gsession.UpsertSession(sess)
|
gsession.UpsertSession(sess)
|
||||||
|
|
||||||
// Initialize session-based file client for high-performance file operations
|
// Initialize session-based file client only for SSH and only if user has file permissions
|
||||||
// Only for SSH-based protocols that support SFTP
|
switch protocol {
|
||||||
protocol := strings.Split(sess.Protocol, ":")[0]
|
case "ssh":
|
||||||
if protocol == "ssh" {
|
if hasFilePermissions {
|
||||||
if err := fileservice.DefaultFileService.InitSessionFileClient(sess.SessionId, sess.AssetId, sess.AccountId); err != nil {
|
if err := fileservice.DefaultFileService.InitSessionFileClient(sess.SessionId, sess.AssetId, sess.AccountId); err != nil {
|
||||||
logger.L().Warn("Failed to initialize session file client",
|
logger.L().Warn("Failed to initialize session file client",
|
||||||
zap.String("sessionId", sess.SessionId),
|
zap.String("sessionId", sess.SessionId),
|
||||||
@@ -280,7 +299,13 @@ func DoConnect(ctx *gin.Context, ws *websocket.Conn) (sess *gsession.Session, er
|
|||||||
zap.Int("assetId", sess.AssetId),
|
zap.Int("assetId", sess.AssetId),
|
||||||
zap.Int("accountId", sess.AccountId))
|
zap.Int("accountId", sess.AccountId))
|
||||||
}
|
}
|
||||||
} else if protocol == "rdp" || protocol == "vnc" {
|
} else {
|
||||||
|
logger.L().Info("Skipping SFTP client initialization - no file permissions",
|
||||||
|
zap.String("sessionId", sess.SessionId),
|
||||||
|
zap.Int("assetId", sess.AssetId),
|
||||||
|
zap.Int("accountId", sess.AccountId))
|
||||||
|
}
|
||||||
|
case "rdp", "vnc":
|
||||||
logger.L().Debug("Skipping session file client initialization for Guacamole protocol",
|
logger.L().Debug("Skipping session file client initialization for Guacamole protocol",
|
||||||
zap.String("protocol", protocol),
|
zap.String("protocol", protocol),
|
||||||
zap.String("sessionId", sess.SessionId))
|
zap.String("sessionId", sess.SessionId))
|
||||||
|
@@ -30,7 +30,28 @@ func ConnectGuacd(ctx *gin.Context, sess *gsession.Session, asset *model.Asset,
|
|||||||
|
|
||||||
w, h, dpi := cast.ToInt(ctx.Query("w")), cast.ToInt(ctx.Query("h")), cast.ToInt(ctx.Query("dpi"))
|
w, h, dpi := cast.ToInt(ctx.Query("w")), cast.ToInt(ctx.Query("h")), cast.ToInt(ctx.Query("dpi"))
|
||||||
|
|
||||||
t, err := guacd.NewTunnel("", sess.SessionId, w, h, dpi, sess.Protocol, asset, account, gateway)
|
// Get permissions for guacd connection using batch check
|
||||||
|
permissions := &guacd.PermissionInfo{}
|
||||||
|
|
||||||
|
// Check all relevant permissions in one batch call
|
||||||
|
batchResult, err := service.DefaultAuthService.HasAuthorizationV2(ctx, sess,
|
||||||
|
model.ActionCopy,
|
||||||
|
model.ActionPaste,
|
||||||
|
model.ActionFileUpload,
|
||||||
|
model.ActionFileDownload)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.L().Warn("Failed to check permissions, using default settings", zap.Error(err))
|
||||||
|
// Continue with default (denied) permissions if check fails
|
||||||
|
} else {
|
||||||
|
// Extract individual permissions from batch result
|
||||||
|
permissions.AllowCopy = batchResult.IsAllowed(model.ActionCopy)
|
||||||
|
permissions.AllowPaste = batchResult.IsAllowed(model.ActionPaste)
|
||||||
|
permissions.AllowFileUpload = batchResult.IsAllowed(model.ActionFileUpload)
|
||||||
|
permissions.AllowFileDownload = batchResult.IsAllowed(model.ActionFileDownload)
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := guacd.NewTunnel("", sess.SessionId, w, h, dpi, sess.Protocol, asset, account, gateway, permissions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L().Error("guacd tunnel failed", zap.Error(err))
|
logger.L().Error("guacd tunnel failed", zap.Error(err))
|
||||||
return
|
return
|
||||||
@@ -135,7 +156,8 @@ func MonitGuacd(ctx *gin.Context, sess *gsession.Session, chs *gsession.SessionC
|
|||||||
chs.ErrChan <- err
|
chs.ErrChan <- err
|
||||||
}()
|
}()
|
||||||
|
|
||||||
t, err := guacd.NewTunnel(sess.ConnectionId, "", w, h, dpi, ":", nil, nil, nil)
|
// For monitoring, no permissions needed since it's read-only
|
||||||
|
t, err := guacd.NewTunnel(sess.ConnectionId, "", w, h, dpi, ":", nil, nil, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L().Error("guacd tunnel failed", zap.Error(err))
|
logger.L().Error("guacd tunnel failed", zap.Error(err))
|
||||||
return
|
return
|
||||||
|
@@ -34,6 +34,14 @@ const (
|
|||||||
DRIVE_NAME = "drive-name"
|
DRIVE_NAME = "drive-name"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// PermissionInfo contains permission information for guacd connection
|
||||||
|
type PermissionInfo struct {
|
||||||
|
AllowCopy bool
|
||||||
|
AllowPaste bool
|
||||||
|
AllowFileUpload bool
|
||||||
|
AllowFileDownload bool
|
||||||
|
}
|
||||||
|
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
Protocol string
|
Protocol string
|
||||||
Parameters map[string]string
|
Parameters map[string]string
|
||||||
@@ -57,7 +65,7 @@ type Tunnel struct {
|
|||||||
drivePath string
|
drivePath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTunnel(connectionId, sessionId string, w, h, dpi int, protocol string, asset *model.Asset, account *model.Account, gateway *model.Gateway) (t *Tunnel, err error) {
|
func NewTunnel(connectionId, sessionId string, w, h, dpi int, protocol string, asset *model.Asset, account *model.Account, gateway *model.Gateway, permissions *PermissionInfo) (t *Tunnel, err error) {
|
||||||
var hostPort string
|
var hostPort string
|
||||||
if strings.Contains(config.Cfg.Guacd.Host, ":") {
|
if strings.Contains(config.Cfg.Guacd.Host, ":") {
|
||||||
// IPv6 address
|
// IPv6 address
|
||||||
@@ -70,9 +78,34 @@ func NewTunnel(connectionId, sessionId string, w, h, dpi int, protocol string, a
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ss := strings.Split(protocol, ":")
|
// Find the port for the protocol from asset.Protocols
|
||||||
protocol, port := ss[0], ss[1]
|
var port string
|
||||||
cfg := model.GlobalConfig.Load()
|
protocolLower := strings.ToLower(protocol)
|
||||||
|
for _, p := range asset.Protocols {
|
||||||
|
if strings.HasPrefix(strings.ToLower(p), protocolLower) {
|
||||||
|
parts := strings.Split(p, ":")
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
port = parts[1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to default ports if not found in asset.Protocols
|
||||||
|
if port == "" {
|
||||||
|
switch protocolLower {
|
||||||
|
case "rdp":
|
||||||
|
port = "3389"
|
||||||
|
case "vnc":
|
||||||
|
port = "5900"
|
||||||
|
default:
|
||||||
|
port = "22" // SSH default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if file transfer should be enabled
|
||||||
|
enableFileTransfer := permissions != nil && (permissions.AllowFileUpload || permissions.AllowFileDownload)
|
||||||
|
|
||||||
t = &Tunnel{
|
t = &Tunnel{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
reader: bufio.NewReader(conn),
|
reader: bufio.NewReader(conn),
|
||||||
@@ -84,7 +117,7 @@ func NewTunnel(connectionId, sessionId string, w, h, dpi int, protocol string, a
|
|||||||
Parameters: lo.TernaryF(
|
Parameters: lo.TernaryF(
|
||||||
connectionId == "",
|
connectionId == "",
|
||||||
func() map[string]string {
|
func() map[string]string {
|
||||||
return map[string]string{
|
params := map[string]string{
|
||||||
"version": VERSION,
|
"version": VERSION,
|
||||||
"client-name": "OneTerm",
|
"client-name": "OneTerm",
|
||||||
"recording-path": RECORDING_PATH,
|
"recording-path": RECORDING_PATH,
|
||||||
@@ -98,22 +131,36 @@ func NewTunnel(connectionId, sessionId string, w, h, dpi int, protocol string, a
|
|||||||
"port": port,
|
"port": port,
|
||||||
"username": account.Account,
|
"username": account.Account,
|
||||||
"password": account.Password,
|
"password": account.Password,
|
||||||
"disable-copy": cast.ToString(lo.Ternary(strings.Contains(protocol, "rdp"), !cfg.RdpConfig.Copy, !cfg.VncConfig.Copy)),
|
|
||||||
"disable-paste": cast.ToString(lo.Ternary(strings.Contains(protocol, "rdp"), !cfg.RdpConfig.Paste, !cfg.VncConfig.Paste)),
|
|
||||||
"resize-method": "display-update",
|
"resize-method": "display-update",
|
||||||
// Set file transfer related parameters from config
|
|
||||||
// DRIVE_ENABLE: cast.ToString(lo.Ternary(strings.Contains(protocol, "rdp"), cfg.RdpConfig.EnableDrive, false)),
|
|
||||||
// DRIVE_PATH: cast.ToString(lo.Ternary(strings.Contains(protocol, "rdp"), cfg.RdpConfig.DrivePath, "")),
|
|
||||||
// DRIVE_CREATE_PATH: cast.ToString(lo.Ternary(strings.Contains(protocol, "rdp"), cfg.RdpConfig.CreateDrivePath, false)),
|
|
||||||
// DRIVE_DISABLE_UPLOAD: cast.ToString(lo.Ternary(strings.Contains(protocol, "rdp"), cfg.RdpConfig.DisableUpload, false)),
|
|
||||||
// DRIVE_DISABLE_DOWNLOAD: cast.ToString(lo.Ternary(strings.Contains(protocol, "rdp"), cfg.RdpConfig.DisableDownload, false)),
|
|
||||||
DRIVE_ENABLE: "true",
|
|
||||||
DRIVE_PATH: fmt.Sprintf("/rdp/asset_%d", asset.Id),
|
|
||||||
DRIVE_CREATE_PATH: "true",
|
|
||||||
DRIVE_DISABLE_UPLOAD: "false",
|
|
||||||
DRIVE_DISABLE_DOWNLOAD: "false",
|
|
||||||
DRIVE_NAME: "Drive",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set permission-based parameters
|
||||||
|
if permissions != nil {
|
||||||
|
// Copy/Paste permissions
|
||||||
|
params["disable-copy"] = cast.ToString(!permissions.AllowCopy)
|
||||||
|
params["disable-paste"] = cast.ToString(!permissions.AllowPaste)
|
||||||
|
|
||||||
|
// File transfer permissions
|
||||||
|
params[DRIVE_ENABLE] = cast.ToString(enableFileTransfer)
|
||||||
|
params[DRIVE_DISABLE_UPLOAD] = cast.ToString(!permissions.AllowFileUpload)
|
||||||
|
params[DRIVE_DISABLE_DOWNLOAD] = cast.ToString(!permissions.AllowFileDownload)
|
||||||
|
} else {
|
||||||
|
// Default to deny all permissions if no permissions provided
|
||||||
|
params["disable-copy"] = "true"
|
||||||
|
params["disable-paste"] = "true"
|
||||||
|
params[DRIVE_ENABLE] = "false"
|
||||||
|
params[DRIVE_DISABLE_UPLOAD] = "true"
|
||||||
|
params[DRIVE_DISABLE_DOWNLOAD] = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only set file transfer parameters if enabled
|
||||||
|
if enableFileTransfer {
|
||||||
|
params[DRIVE_PATH] = fmt.Sprintf("/rdp/asset_%d", asset.Id)
|
||||||
|
params[DRIVE_CREATE_PATH] = "true"
|
||||||
|
params[DRIVE_NAME] = "Drive"
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
}, func() map[string]string {
|
}, func() map[string]string {
|
||||||
return map[string]string{
|
return map[string]string{
|
||||||
"width": cast.ToString(w),
|
"width": cast.ToString(w),
|
||||||
|
@@ -242,4 +242,370 @@ var (
|
|||||||
One: "\x1b[31;47m Welcome: {{.User}}",
|
One: "\x1b[31;47m Welcome: {{.User}}",
|
||||||
Other: "\x1b[31;47m Welcome: {{.User}}",
|
Other: "\x1b[31;47m Welcome: {{.User}}",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Predefined dangerous commands
|
||||||
|
CmdDeleteRootDir = &i18n.Message{
|
||||||
|
ID: "CmdDeleteRootDir",
|
||||||
|
One: "Delete root directory",
|
||||||
|
Other: "Delete root directory",
|
||||||
|
}
|
||||||
|
CmdDeleteRootDirDesc = &i18n.Message{
|
||||||
|
ID: "CmdDeleteRootDirDesc",
|
||||||
|
One: "Prohibit deletion of root directory, this will destroy the entire system",
|
||||||
|
Other: "Prohibit deletion of root directory, this will destroy the entire system",
|
||||||
|
}
|
||||||
|
CmdDeleteSystemDirs = &i18n.Message{
|
||||||
|
ID: "CmdDeleteSystemDirs",
|
||||||
|
One: "Delete system directories",
|
||||||
|
Other: "Delete system directories",
|
||||||
|
}
|
||||||
|
CmdDeleteSystemDirsDesc = &i18n.Message{
|
||||||
|
ID: "CmdDeleteSystemDirsDesc",
|
||||||
|
One: "Prohibit deletion of critical system directories",
|
||||||
|
Other: "Prohibit deletion of critical system directories",
|
||||||
|
}
|
||||||
|
CmdDiskDestruction = &i18n.Message{
|
||||||
|
ID: "CmdDiskDestruction",
|
||||||
|
One: "Disk destruction operations",
|
||||||
|
Other: "Disk destruction operations",
|
||||||
|
}
|
||||||
|
CmdDiskDestructionDesc = &i18n.Message{
|
||||||
|
ID: "CmdDiskDestructionDesc",
|
||||||
|
One: "Prohibit writing random data to disk devices, will destroy data",
|
||||||
|
Other: "Prohibit writing random data to disk devices, will destroy data",
|
||||||
|
}
|
||||||
|
CmdFormatDisk = &i18n.Message{
|
||||||
|
ID: "CmdFormatDisk",
|
||||||
|
One: "Format disk",
|
||||||
|
Other: "Format disk",
|
||||||
|
}
|
||||||
|
CmdFormatDiskDesc = &i18n.Message{
|
||||||
|
ID: "CmdFormatDiskDesc",
|
||||||
|
One: "Prohibit formatting disk partitions",
|
||||||
|
Other: "Prohibit formatting disk partitions",
|
||||||
|
}
|
||||||
|
CmdForkBomb = &i18n.Message{
|
||||||
|
ID: "CmdForkBomb",
|
||||||
|
One: "Fork bomb",
|
||||||
|
Other: "Fork bomb",
|
||||||
|
}
|
||||||
|
CmdForkBombDesc = &i18n.Message{
|
||||||
|
ID: "CmdForkBombDesc",
|
||||||
|
One: "Prohibit fork bomb attacks",
|
||||||
|
Other: "Prohibit fork bomb attacks",
|
||||||
|
}
|
||||||
|
CmdSystemReboot = &i18n.Message{
|
||||||
|
ID: "CmdSystemReboot",
|
||||||
|
One: "System reboot shutdown",
|
||||||
|
Other: "System reboot shutdown",
|
||||||
|
}
|
||||||
|
CmdSystemRebootDesc = &i18n.Message{
|
||||||
|
ID: "CmdSystemRebootDesc",
|
||||||
|
One: "Restrict system reboot and shutdown operations",
|
||||||
|
Other: "Restrict system reboot and shutdown operations",
|
||||||
|
}
|
||||||
|
CmdModifySystemFiles = &i18n.Message{
|
||||||
|
ID: "CmdModifySystemFiles",
|
||||||
|
One: "Modify critical system files",
|
||||||
|
Other: "Modify critical system files",
|
||||||
|
}
|
||||||
|
CmdModifySystemFilesDesc = &i18n.Message{
|
||||||
|
ID: "CmdModifySystemFilesDesc",
|
||||||
|
One: "Restrict direct modification of system configuration files",
|
||||||
|
Other: "Restrict direct modification of system configuration files",
|
||||||
|
}
|
||||||
|
CmdDropDatabase = &i18n.Message{
|
||||||
|
ID: "CmdDropDatabase",
|
||||||
|
One: "Drop database",
|
||||||
|
Other: "Drop database",
|
||||||
|
}
|
||||||
|
CmdDropDatabaseDesc = &i18n.Message{
|
||||||
|
ID: "CmdDropDatabaseDesc",
|
||||||
|
One: "Restrict dropping entire databases",
|
||||||
|
Other: "Restrict dropping entire databases",
|
||||||
|
}
|
||||||
|
CmdTruncateTable = &i18n.Message{
|
||||||
|
ID: "CmdTruncateTable",
|
||||||
|
One: "Truncate table data",
|
||||||
|
Other: "Truncate table data",
|
||||||
|
}
|
||||||
|
CmdTruncateTableDesc = &i18n.Message{
|
||||||
|
ID: "CmdTruncateTableDesc",
|
||||||
|
One: "Restrict clearing all table data",
|
||||||
|
Other: "Restrict clearing all table data",
|
||||||
|
}
|
||||||
|
CmdModifyPermissions = &i18n.Message{
|
||||||
|
ID: "CmdModifyPermissions",
|
||||||
|
One: "Modify user permissions",
|
||||||
|
Other: "Modify user permissions",
|
||||||
|
}
|
||||||
|
CmdModifyPermissionsDesc = &i18n.Message{
|
||||||
|
ID: "CmdModifyPermissionsDesc",
|
||||||
|
One: "Restrict modifying root directory permissions",
|
||||||
|
Other: "Restrict modifying root directory permissions",
|
||||||
|
}
|
||||||
|
CmdDropTable = &i18n.Message{
|
||||||
|
ID: "CmdDropTable",
|
||||||
|
One: "Drop table",
|
||||||
|
Other: "Drop table",
|
||||||
|
}
|
||||||
|
CmdDropTableDesc = &i18n.Message{
|
||||||
|
ID: "CmdDropTableDesc",
|
||||||
|
One: "Warning: Dropping database table",
|
||||||
|
Other: "Warning: Dropping database table",
|
||||||
|
}
|
||||||
|
CmdServiceControl = &i18n.Message{
|
||||||
|
ID: "CmdServiceControl",
|
||||||
|
One: "Service control commands",
|
||||||
|
Other: "Service control commands",
|
||||||
|
}
|
||||||
|
CmdServiceControlDesc = &i18n.Message{
|
||||||
|
ID: "CmdServiceControlDesc",
|
||||||
|
One: "Warning: Operating on system services",
|
||||||
|
Other: "Warning: Operating on system services",
|
||||||
|
}
|
||||||
|
CmdNetworkConfig = &i18n.Message{
|
||||||
|
ID: "CmdNetworkConfig",
|
||||||
|
One: "Network configuration modification",
|
||||||
|
Other: "Network configuration modification",
|
||||||
|
}
|
||||||
|
CmdNetworkConfigDesc = &i18n.Message{
|
||||||
|
ID: "CmdNetworkConfigDesc",
|
||||||
|
One: "Warning: Modifying network configuration",
|
||||||
|
Other: "Warning: Modifying network configuration",
|
||||||
|
}
|
||||||
|
CmdUserManagement = &i18n.Message{
|
||||||
|
ID: "CmdUserManagement",
|
||||||
|
One: "User management",
|
||||||
|
Other: "User management",
|
||||||
|
}
|
||||||
|
CmdUserManagementDesc = &i18n.Message{
|
||||||
|
ID: "CmdUserManagementDesc",
|
||||||
|
One: "Warning: Performing user management operations",
|
||||||
|
Other: "Warning: Performing user management operations",
|
||||||
|
}
|
||||||
|
CmdKernelModule = &i18n.Message{
|
||||||
|
ID: "CmdKernelModule",
|
||||||
|
One: "Kernel module operations",
|
||||||
|
Other: "Kernel module operations",
|
||||||
|
}
|
||||||
|
CmdKernelModuleDesc = &i18n.Message{
|
||||||
|
ID: "CmdKernelModuleDesc",
|
||||||
|
One: "Warning: Operating on kernel modules",
|
||||||
|
Other: "Warning: Operating on kernel modules",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Predefined command templates
|
||||||
|
TmplBasicSecurity = &i18n.Message{
|
||||||
|
ID: "TmplBasicSecurity",
|
||||||
|
One: "Basic Security Protection",
|
||||||
|
Other: "Basic Security Protection",
|
||||||
|
}
|
||||||
|
TmplBasicSecurityDesc = &i18n.Message{
|
||||||
|
ID: "TmplBasicSecurityDesc",
|
||||||
|
One: "Basic security command restrictions for all production environments",
|
||||||
|
Other: "Basic security command restrictions for all production environments",
|
||||||
|
}
|
||||||
|
TmplDatabaseProtection = &i18n.Message{
|
||||||
|
ID: "TmplDatabaseProtection",
|
||||||
|
One: "Production Database Protection",
|
||||||
|
Other: "Production Database Protection",
|
||||||
|
}
|
||||||
|
TmplDatabaseProtectionDesc = &i18n.Message{
|
||||||
|
ID: "TmplDatabaseProtectionDesc",
|
||||||
|
One: "Security policies to protect production databases",
|
||||||
|
Other: "Security policies to protect production databases",
|
||||||
|
}
|
||||||
|
TmplServiceRestrictions = &i18n.Message{
|
||||||
|
ID: "TmplServiceRestrictions",
|
||||||
|
One: "System Service Control Restrictions",
|
||||||
|
Other: "System Service Control Restrictions",
|
||||||
|
}
|
||||||
|
TmplServiceRestrictionsDesc = &i18n.Message{
|
||||||
|
ID: "TmplServiceRestrictionsDesc",
|
||||||
|
One: "Restrictions on critical system service operations",
|
||||||
|
Other: "Restrictions on critical system service operations",
|
||||||
|
}
|
||||||
|
TmplNetworkSecurity = &i18n.Message{
|
||||||
|
ID: "TmplNetworkSecurity",
|
||||||
|
One: "Network Security Control",
|
||||||
|
Other: "Network Security Control",
|
||||||
|
}
|
||||||
|
TmplNetworkSecurityDesc = &i18n.Message{
|
||||||
|
ID: "TmplNetworkSecurityDesc",
|
||||||
|
One: "Security controls for network configuration and user permissions",
|
||||||
|
Other: "Security controls for network configuration and user permissions",
|
||||||
|
}
|
||||||
|
TmplDevEnvironment = &i18n.Message{
|
||||||
|
ID: "TmplDevEnvironment",
|
||||||
|
One: "Development Environment Basic Restrictions",
|
||||||
|
Other: "Development Environment Basic Restrictions",
|
||||||
|
}
|
||||||
|
TmplDevEnvironmentDesc = &i18n.Message{
|
||||||
|
ID: "TmplDevEnvironmentDesc",
|
||||||
|
One: "Minimal security restrictions for development environments",
|
||||||
|
Other: "Minimal security restrictions for development environments",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command tags
|
||||||
|
TagDangerous = &i18n.Message{
|
||||||
|
ID: "TagDangerous",
|
||||||
|
One: "dangerous",
|
||||||
|
Other: "dangerous",
|
||||||
|
}
|
||||||
|
TagDelete = &i18n.Message{
|
||||||
|
ID: "TagDelete",
|
||||||
|
One: "delete",
|
||||||
|
Other: "delete",
|
||||||
|
}
|
||||||
|
TagSystem = &i18n.Message{
|
||||||
|
ID: "TagSystem",
|
||||||
|
One: "system",
|
||||||
|
Other: "system",
|
||||||
|
}
|
||||||
|
TagSystemDirs = &i18n.Message{
|
||||||
|
ID: "TagSystemDirs",
|
||||||
|
One: "system-dirs",
|
||||||
|
Other: "system-dirs",
|
||||||
|
}
|
||||||
|
TagDisk = &i18n.Message{
|
||||||
|
ID: "TagDisk",
|
||||||
|
One: "disk",
|
||||||
|
Other: "disk",
|
||||||
|
}
|
||||||
|
TagDestruction = &i18n.Message{
|
||||||
|
ID: "TagDestruction",
|
||||||
|
One: "destruction",
|
||||||
|
Other: "destruction",
|
||||||
|
}
|
||||||
|
TagFormat = &i18n.Message{
|
||||||
|
ID: "TagFormat",
|
||||||
|
One: "format",
|
||||||
|
Other: "format",
|
||||||
|
}
|
||||||
|
TagAttack = &i18n.Message{
|
||||||
|
ID: "TagAttack",
|
||||||
|
One: "attack",
|
||||||
|
Other: "attack",
|
||||||
|
}
|
||||||
|
TagResourceExhaustion = &i18n.Message{
|
||||||
|
ID: "TagResourceExhaustion",
|
||||||
|
One: "resource-exhaustion",
|
||||||
|
Other: "resource-exhaustion",
|
||||||
|
}
|
||||||
|
TagReboot = &i18n.Message{
|
||||||
|
ID: "TagReboot",
|
||||||
|
One: "reboot",
|
||||||
|
Other: "reboot",
|
||||||
|
}
|
||||||
|
TagShutdown = &i18n.Message{
|
||||||
|
ID: "TagShutdown",
|
||||||
|
One: "shutdown",
|
||||||
|
Other: "shutdown",
|
||||||
|
}
|
||||||
|
TagEdit = &i18n.Message{
|
||||||
|
ID: "TagEdit",
|
||||||
|
One: "edit",
|
||||||
|
Other: "edit",
|
||||||
|
}
|
||||||
|
TagSystemFiles = &i18n.Message{
|
||||||
|
ID: "TagSystemFiles",
|
||||||
|
One: "system-files",
|
||||||
|
Other: "system-files",
|
||||||
|
}
|
||||||
|
TagConfig = &i18n.Message{
|
||||||
|
ID: "TagConfig",
|
||||||
|
One: "config",
|
||||||
|
Other: "config",
|
||||||
|
}
|
||||||
|
TagDatabase = &i18n.Message{
|
||||||
|
ID: "TagDatabase",
|
||||||
|
One: "database",
|
||||||
|
Other: "database",
|
||||||
|
}
|
||||||
|
TagDrop = &i18n.Message{
|
||||||
|
ID: "TagDrop",
|
||||||
|
One: "drop",
|
||||||
|
Other: "drop",
|
||||||
|
}
|
||||||
|
TagClear = &i18n.Message{
|
||||||
|
ID: "TagClear",
|
||||||
|
One: "clear",
|
||||||
|
Other: "clear",
|
||||||
|
}
|
||||||
|
TagTruncate = &i18n.Message{
|
||||||
|
ID: "TagTruncate",
|
||||||
|
One: "truncate",
|
||||||
|
Other: "truncate",
|
||||||
|
}
|
||||||
|
TagPermissions = &i18n.Message{
|
||||||
|
ID: "TagPermissions",
|
||||||
|
One: "permissions",
|
||||||
|
Other: "permissions",
|
||||||
|
}
|
||||||
|
TagChmod = &i18n.Message{
|
||||||
|
ID: "TagChmod",
|
||||||
|
One: "chmod",
|
||||||
|
Other: "chmod",
|
||||||
|
}
|
||||||
|
TagSecurity = &i18n.Message{
|
||||||
|
ID: "TagSecurity",
|
||||||
|
One: "security",
|
||||||
|
Other: "security",
|
||||||
|
}
|
||||||
|
TagTable = &i18n.Message{
|
||||||
|
ID: "TagTable",
|
||||||
|
One: "table",
|
||||||
|
Other: "table",
|
||||||
|
}
|
||||||
|
TagService = &i18n.Message{
|
||||||
|
ID: "TagService",
|
||||||
|
One: "service",
|
||||||
|
Other: "service",
|
||||||
|
}
|
||||||
|
TagSystemctl = &i18n.Message{
|
||||||
|
ID: "TagSystemctl",
|
||||||
|
One: "systemctl",
|
||||||
|
Other: "systemctl",
|
||||||
|
}
|
||||||
|
TagControl = &i18n.Message{
|
||||||
|
ID: "TagControl",
|
||||||
|
One: "control",
|
||||||
|
Other: "control",
|
||||||
|
}
|
||||||
|
TagNetwork = &i18n.Message{
|
||||||
|
ID: "TagNetwork",
|
||||||
|
One: "network",
|
||||||
|
Other: "network",
|
||||||
|
}
|
||||||
|
TagFirewall = &i18n.Message{
|
||||||
|
ID: "TagFirewall",
|
||||||
|
One: "firewall",
|
||||||
|
Other: "firewall",
|
||||||
|
}
|
||||||
|
TagRouting = &i18n.Message{
|
||||||
|
ID: "TagRouting",
|
||||||
|
One: "routing",
|
||||||
|
Other: "routing",
|
||||||
|
}
|
||||||
|
TagUser = &i18n.Message{
|
||||||
|
ID: "TagUser",
|
||||||
|
One: "user",
|
||||||
|
Other: "user",
|
||||||
|
}
|
||||||
|
TagManagement = &i18n.Message{
|
||||||
|
ID: "TagManagement",
|
||||||
|
One: "management",
|
||||||
|
Other: "management",
|
||||||
|
}
|
||||||
|
TagKernel = &i18n.Message{
|
||||||
|
ID: "TagKernel",
|
||||||
|
One: "kernel",
|
||||||
|
Other: "kernel",
|
||||||
|
}
|
||||||
|
TagModule = &i18n.Message{
|
||||||
|
ID: "TagModule",
|
||||||
|
One: "module",
|
||||||
|
Other: "module",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
@@ -153,3 +153,290 @@ other = "Bad Request: Invalid SSH public key"
|
|||||||
[MsgWrongPvk]
|
[MsgWrongPvk]
|
||||||
one = "Bad Request: Invalid SSH private key"
|
one = "Bad Request: Invalid SSH private key"
|
||||||
other = "Bad Request: Invalid SSH private key"
|
other = "Bad Request: Invalid SSH private key"
|
||||||
|
|
||||||
|
# Predefined dangerous commands
|
||||||
|
[CmdDeleteRootDir]
|
||||||
|
one = "Delete root directory"
|
||||||
|
other = "Delete root directory"
|
||||||
|
|
||||||
|
[CmdDeleteRootDirDesc]
|
||||||
|
one = "Prohibit deletion of root directory, this will destroy the entire system"
|
||||||
|
other = "Prohibit deletion of root directory, this will destroy the entire system"
|
||||||
|
|
||||||
|
[CmdDeleteSystemDirs]
|
||||||
|
one = "Delete system directories"
|
||||||
|
other = "Delete system directories"
|
||||||
|
|
||||||
|
[CmdDeleteSystemDirsDesc]
|
||||||
|
one = "Prohibit deletion of critical system directories"
|
||||||
|
other = "Prohibit deletion of critical system directories"
|
||||||
|
|
||||||
|
[CmdDiskDestruction]
|
||||||
|
one = "Disk destruction operations"
|
||||||
|
other = "Disk destruction operations"
|
||||||
|
|
||||||
|
[CmdDiskDestructionDesc]
|
||||||
|
one = "Prohibit writing random data to disk devices, will destroy data"
|
||||||
|
other = "Prohibit writing random data to disk devices, will destroy data"
|
||||||
|
|
||||||
|
[CmdFormatDisk]
|
||||||
|
one = "Format disk"
|
||||||
|
other = "Format disk"
|
||||||
|
|
||||||
|
[CmdFormatDiskDesc]
|
||||||
|
one = "Prohibit formatting disk partitions"
|
||||||
|
other = "Prohibit formatting disk partitions"
|
||||||
|
|
||||||
|
[CmdForkBomb]
|
||||||
|
one = "Fork bomb"
|
||||||
|
other = "Fork bomb"
|
||||||
|
|
||||||
|
[CmdForkBombDesc]
|
||||||
|
one = "Prohibit fork bomb attacks"
|
||||||
|
other = "Prohibit fork bomb attacks"
|
||||||
|
|
||||||
|
[CmdSystemReboot]
|
||||||
|
one = "System reboot shutdown"
|
||||||
|
other = "System reboot shutdown"
|
||||||
|
|
||||||
|
[CmdSystemRebootDesc]
|
||||||
|
one = "Restrict system reboot and shutdown operations"
|
||||||
|
other = "Restrict system reboot and shutdown operations"
|
||||||
|
|
||||||
|
[CmdModifySystemFiles]
|
||||||
|
one = "Modify critical system files"
|
||||||
|
other = "Modify critical system files"
|
||||||
|
|
||||||
|
[CmdModifySystemFilesDesc]
|
||||||
|
one = "Restrict direct modification of system configuration files"
|
||||||
|
other = "Restrict direct modification of system configuration files"
|
||||||
|
|
||||||
|
[CmdDropDatabase]
|
||||||
|
one = "Drop database"
|
||||||
|
other = "Drop database"
|
||||||
|
|
||||||
|
[CmdDropDatabaseDesc]
|
||||||
|
one = "Restrict dropping entire databases"
|
||||||
|
other = "Restrict dropping entire databases"
|
||||||
|
|
||||||
|
[CmdTruncateTable]
|
||||||
|
one = "Truncate table data"
|
||||||
|
other = "Truncate table data"
|
||||||
|
|
||||||
|
[CmdTruncateTableDesc]
|
||||||
|
one = "Restrict clearing all table data"
|
||||||
|
other = "Restrict clearing all table data"
|
||||||
|
|
||||||
|
[CmdModifyPermissions]
|
||||||
|
one = "Modify user permissions"
|
||||||
|
other = "Modify user permissions"
|
||||||
|
|
||||||
|
[CmdModifyPermissionsDesc]
|
||||||
|
one = "Restrict modifying root directory permissions"
|
||||||
|
other = "Restrict modifying root directory permissions"
|
||||||
|
|
||||||
|
[CmdDropTable]
|
||||||
|
one = "Drop table"
|
||||||
|
other = "Drop table"
|
||||||
|
|
||||||
|
[CmdDropTableDesc]
|
||||||
|
one = "Warning: Dropping database table"
|
||||||
|
other = "Warning: Dropping database table"
|
||||||
|
|
||||||
|
[CmdServiceControl]
|
||||||
|
one = "Service control commands"
|
||||||
|
other = "Service control commands"
|
||||||
|
|
||||||
|
[CmdServiceControlDesc]
|
||||||
|
one = "Warning: Operating on system services"
|
||||||
|
other = "Warning: Operating on system services"
|
||||||
|
|
||||||
|
[CmdNetworkConfig]
|
||||||
|
one = "Network configuration modification"
|
||||||
|
other = "Network configuration modification"
|
||||||
|
|
||||||
|
[CmdNetworkConfigDesc]
|
||||||
|
one = "Warning: Modifying network configuration"
|
||||||
|
other = "Warning: Modifying network configuration"
|
||||||
|
|
||||||
|
[CmdUserManagement]
|
||||||
|
one = "User management"
|
||||||
|
other = "User management"
|
||||||
|
|
||||||
|
[CmdUserManagementDesc]
|
||||||
|
one = "Warning: Performing user management operations"
|
||||||
|
other = "Warning: Performing user management operations"
|
||||||
|
|
||||||
|
[CmdKernelModule]
|
||||||
|
one = "Kernel module operations"
|
||||||
|
other = "Kernel module operations"
|
||||||
|
|
||||||
|
# Predefined command templates
|
||||||
|
[TmplBasicSecurity]
|
||||||
|
one = "Basic Security Protection"
|
||||||
|
other = "Basic Security Protection"
|
||||||
|
|
||||||
|
[TmplBasicSecurityDesc]
|
||||||
|
one = "Basic security command restrictions for all production environments"
|
||||||
|
other = "Basic security command restrictions for all production environments"
|
||||||
|
|
||||||
|
[TmplDatabaseProtection]
|
||||||
|
one = "Production Database Protection"
|
||||||
|
other = "Production Database Protection"
|
||||||
|
|
||||||
|
[TmplDatabaseProtectionDesc]
|
||||||
|
one = "Security policies to protect production databases"
|
||||||
|
other = "Security policies to protect production databases"
|
||||||
|
|
||||||
|
[TmplServiceRestrictions]
|
||||||
|
one = "System Service Control Restrictions"
|
||||||
|
other = "System Service Control Restrictions"
|
||||||
|
|
||||||
|
[TmplServiceRestrictionsDesc]
|
||||||
|
one = "Restrictions on critical system service operations"
|
||||||
|
other = "Restrictions on critical system service operations"
|
||||||
|
|
||||||
|
[TmplNetworkSecurity]
|
||||||
|
one = "Network Security Control"
|
||||||
|
other = "Network Security Control"
|
||||||
|
|
||||||
|
[TmplNetworkSecurityDesc]
|
||||||
|
one = "Security controls for network configuration and user permissions"
|
||||||
|
other = "Security controls for network configuration and user permissions"
|
||||||
|
|
||||||
|
[TmplDevEnvironment]
|
||||||
|
one = "Development Environment Basic Restrictions"
|
||||||
|
other = "Development Environment Basic Restrictions"
|
||||||
|
|
||||||
|
[TmplDevEnvironmentDesc]
|
||||||
|
one = "Minimal security restrictions for development environments"
|
||||||
|
other = "Minimal security restrictions for development environments"
|
||||||
|
|
||||||
|
# Tags
|
||||||
|
[TagDangerous]
|
||||||
|
one = "dangerous"
|
||||||
|
other = "dangerous"
|
||||||
|
|
||||||
|
[TagDelete]
|
||||||
|
one = "delete"
|
||||||
|
other = "delete"
|
||||||
|
|
||||||
|
[TagSystem]
|
||||||
|
one = "system"
|
||||||
|
other = "system"
|
||||||
|
|
||||||
|
[TagSystemDirs]
|
||||||
|
one = "system-dirs"
|
||||||
|
other = "system-dirs"
|
||||||
|
|
||||||
|
[TagDisk]
|
||||||
|
one = "disk"
|
||||||
|
other = "disk"
|
||||||
|
|
||||||
|
[TagDestruction]
|
||||||
|
one = "destruction"
|
||||||
|
other = "destruction"
|
||||||
|
|
||||||
|
[TagFormat]
|
||||||
|
one = "format"
|
||||||
|
other = "format"
|
||||||
|
|
||||||
|
[TagAttack]
|
||||||
|
one = "attack"
|
||||||
|
other = "attack"
|
||||||
|
|
||||||
|
[TagResourceExhaustion]
|
||||||
|
one = "resource-exhaustion"
|
||||||
|
other = "resource-exhaustion"
|
||||||
|
|
||||||
|
[TagReboot]
|
||||||
|
one = "reboot"
|
||||||
|
other = "reboot"
|
||||||
|
|
||||||
|
[TagShutdown]
|
||||||
|
one = "shutdown"
|
||||||
|
other = "shutdown"
|
||||||
|
|
||||||
|
[TagEdit]
|
||||||
|
one = "edit"
|
||||||
|
other = "edit"
|
||||||
|
|
||||||
|
[TagSystemFiles]
|
||||||
|
one = "system-files"
|
||||||
|
other = "system-files"
|
||||||
|
|
||||||
|
[TagConfig]
|
||||||
|
one = "config"
|
||||||
|
other = "config"
|
||||||
|
|
||||||
|
[TagDatabase]
|
||||||
|
one = "database"
|
||||||
|
other = "database"
|
||||||
|
|
||||||
|
[TagDrop]
|
||||||
|
one = "drop"
|
||||||
|
other = "drop"
|
||||||
|
|
||||||
|
[TagClear]
|
||||||
|
one = "clear"
|
||||||
|
other = "clear"
|
||||||
|
|
||||||
|
[TagTruncate]
|
||||||
|
one = "truncate"
|
||||||
|
other = "truncate"
|
||||||
|
|
||||||
|
[TagPermissions]
|
||||||
|
one = "permissions"
|
||||||
|
other = "permissions"
|
||||||
|
|
||||||
|
[TagChmod]
|
||||||
|
one = "chmod"
|
||||||
|
other = "chmod"
|
||||||
|
|
||||||
|
[TagSecurity]
|
||||||
|
one = "security"
|
||||||
|
other = "security"
|
||||||
|
|
||||||
|
[TagTable]
|
||||||
|
one = "table"
|
||||||
|
other = "table"
|
||||||
|
|
||||||
|
[TagService]
|
||||||
|
one = "service"
|
||||||
|
other = "service"
|
||||||
|
|
||||||
|
[TagSystemctl]
|
||||||
|
one = "systemctl"
|
||||||
|
other = "systemctl"
|
||||||
|
|
||||||
|
[TagControl]
|
||||||
|
one = "control"
|
||||||
|
other = "control"
|
||||||
|
|
||||||
|
[TagNetwork]
|
||||||
|
one = "network"
|
||||||
|
other = "network"
|
||||||
|
|
||||||
|
[TagFirewall]
|
||||||
|
one = "firewall"
|
||||||
|
other = "firewall"
|
||||||
|
|
||||||
|
[TagRouting]
|
||||||
|
one = "routing"
|
||||||
|
other = "routing"
|
||||||
|
|
||||||
|
[TagUser]
|
||||||
|
one = "user"
|
||||||
|
other = "user"
|
||||||
|
|
||||||
|
[TagManagement]
|
||||||
|
one = "management"
|
||||||
|
other = "management"
|
||||||
|
|
||||||
|
[TagKernel]
|
||||||
|
one = "kernel"
|
||||||
|
other = "kernel"
|
||||||
|
|
||||||
|
[TagModule]
|
||||||
|
one = "module"
|
||||||
|
other = "module"
|
||||||
|
@@ -153,3 +153,222 @@ other = "请求错误: 非法SSH公钥"
|
|||||||
[MsgWrongPvk]
|
[MsgWrongPvk]
|
||||||
hash = "sha1-fd11d7d098d05415f5ed082abdf31223cb2aeda9"
|
hash = "sha1-fd11d7d098d05415f5ed082abdf31223cb2aeda9"
|
||||||
other = "请求错误: 非法SSH私钥"
|
other = "请求错误: 非法SSH私钥"
|
||||||
|
|
||||||
|
# 预定义危险命令
|
||||||
|
[CmdDeleteRootDir]
|
||||||
|
other = "删除根目录"
|
||||||
|
|
||||||
|
[CmdDeleteRootDirDesc]
|
||||||
|
other = "禁止删除根目录,这会摧毁整个系统"
|
||||||
|
|
||||||
|
[CmdDeleteSystemDirs]
|
||||||
|
other = "删除系统目录"
|
||||||
|
|
||||||
|
[CmdDeleteSystemDirsDesc]
|
||||||
|
other = "禁止删除系统关键目录"
|
||||||
|
|
||||||
|
[CmdDiskDestruction]
|
||||||
|
other = "磁盘破坏操作"
|
||||||
|
|
||||||
|
[CmdDiskDestructionDesc]
|
||||||
|
other = "禁止向磁盘设备写入随机数据,会破坏数据"
|
||||||
|
|
||||||
|
[CmdFormatDisk]
|
||||||
|
other = "格式化磁盘"
|
||||||
|
|
||||||
|
[CmdFormatDiskDesc]
|
||||||
|
other = "禁止格式化磁盘分区"
|
||||||
|
|
||||||
|
[CmdForkBomb]
|
||||||
|
other = "fork炸弹"
|
||||||
|
|
||||||
|
[CmdForkBombDesc]
|
||||||
|
other = "禁止fork炸弹攻击"
|
||||||
|
|
||||||
|
[CmdSystemReboot]
|
||||||
|
other = "系统重启关机"
|
||||||
|
|
||||||
|
[CmdSystemRebootDesc]
|
||||||
|
other = "限制系统重启和关机操作"
|
||||||
|
|
||||||
|
[CmdModifySystemFiles]
|
||||||
|
other = "修改关键系统文件"
|
||||||
|
|
||||||
|
[CmdModifySystemFilesDesc]
|
||||||
|
other = "限制直接修改系统配置文件"
|
||||||
|
|
||||||
|
[CmdDropDatabase]
|
||||||
|
other = "删除数据库"
|
||||||
|
|
||||||
|
[CmdDropDatabaseDesc]
|
||||||
|
other = "限制删除整个数据库的操作"
|
||||||
|
|
||||||
|
[CmdTruncateTable]
|
||||||
|
other = "清空表数据"
|
||||||
|
|
||||||
|
[CmdTruncateTableDesc]
|
||||||
|
other = "限制清空表数据的操作"
|
||||||
|
|
||||||
|
[CmdModifyPermissions]
|
||||||
|
other = "修改用户权限"
|
||||||
|
|
||||||
|
[CmdModifyPermissionsDesc]
|
||||||
|
other = "限制修改根目录权限"
|
||||||
|
|
||||||
|
[CmdDropTable]
|
||||||
|
other = "删除表"
|
||||||
|
|
||||||
|
[CmdDropTableDesc]
|
||||||
|
other = "警告:正在删除数据库表"
|
||||||
|
|
||||||
|
[CmdServiceControl]
|
||||||
|
other = "服务控制命令"
|
||||||
|
|
||||||
|
[CmdServiceControlDesc]
|
||||||
|
other = "警告:正在操作系统服务"
|
||||||
|
|
||||||
|
[CmdNetworkConfig]
|
||||||
|
other = "网络配置修改"
|
||||||
|
|
||||||
|
[CmdNetworkConfigDesc]
|
||||||
|
other = "警告:正在修改网络配置"
|
||||||
|
|
||||||
|
[CmdUserManagement]
|
||||||
|
other = "用户管理"
|
||||||
|
|
||||||
|
[CmdUserManagementDesc]
|
||||||
|
other = "警告:正在进行用户管理操作"
|
||||||
|
|
||||||
|
[CmdKernelModule]
|
||||||
|
other = "内核模块操作"
|
||||||
|
|
||||||
|
[CmdKernelModuleDesc]
|
||||||
|
other = "警告:正在操作内核模块"
|
||||||
|
|
||||||
|
# 预定义命令模板
|
||||||
|
[TmplBasicSecurity]
|
||||||
|
other = "基础安全防护"
|
||||||
|
|
||||||
|
[TmplBasicSecurityDesc]
|
||||||
|
other = "适用于所有生产环境的基础安全命令限制"
|
||||||
|
|
||||||
|
[TmplDatabaseProtection]
|
||||||
|
other = "生产环境数据库保护"
|
||||||
|
|
||||||
|
[TmplDatabaseProtectionDesc]
|
||||||
|
other = "保护生产环境数据库的安全策略"
|
||||||
|
|
||||||
|
[TmplServiceRestrictions]
|
||||||
|
other = "系统服务控制限制"
|
||||||
|
|
||||||
|
[TmplServiceRestrictionsDesc]
|
||||||
|
other = "限制对关键系统服务的操作"
|
||||||
|
|
||||||
|
[TmplNetworkSecurity]
|
||||||
|
other = "网络安全控制"
|
||||||
|
|
||||||
|
[TmplNetworkSecurityDesc]
|
||||||
|
other = "网络配置和用户权限相关的安全控制"
|
||||||
|
|
||||||
|
[TmplDevEnvironment]
|
||||||
|
other = "开发环境基础限制"
|
||||||
|
|
||||||
|
[TmplDevEnvironmentDesc]
|
||||||
|
other = "适用于开发环境的最小安全限制"
|
||||||
|
|
||||||
|
# 标签
|
||||||
|
[TagDangerous]
|
||||||
|
other = "危险"
|
||||||
|
|
||||||
|
[TagDelete]
|
||||||
|
other = "删除"
|
||||||
|
|
||||||
|
[TagSystem]
|
||||||
|
other = "系统"
|
||||||
|
|
||||||
|
[TagSystemDirs]
|
||||||
|
other = "系统目录"
|
||||||
|
|
||||||
|
[TagDisk]
|
||||||
|
other = "磁盘"
|
||||||
|
|
||||||
|
[TagDestruction]
|
||||||
|
other = "破坏"
|
||||||
|
|
||||||
|
[TagFormat]
|
||||||
|
other = "格式化"
|
||||||
|
|
||||||
|
[TagAttack]
|
||||||
|
other = "攻击"
|
||||||
|
|
||||||
|
[TagResourceExhaustion]
|
||||||
|
other = "资源耗尽"
|
||||||
|
|
||||||
|
[TagReboot]
|
||||||
|
other = "重启"
|
||||||
|
|
||||||
|
[TagShutdown]
|
||||||
|
other = "关机"
|
||||||
|
|
||||||
|
[TagEdit]
|
||||||
|
other = "编辑"
|
||||||
|
|
||||||
|
[TagSystemFiles]
|
||||||
|
other = "系统文件"
|
||||||
|
|
||||||
|
[TagConfig]
|
||||||
|
other = "配置"
|
||||||
|
|
||||||
|
[TagDatabase]
|
||||||
|
other = "数据库"
|
||||||
|
|
||||||
|
[TagDrop]
|
||||||
|
other = "删除"
|
||||||
|
|
||||||
|
[TagClear]
|
||||||
|
other = "清空"
|
||||||
|
|
||||||
|
[TagTruncate]
|
||||||
|
other = "truncate"
|
||||||
|
|
||||||
|
[TagPermissions]
|
||||||
|
other = "权限"
|
||||||
|
|
||||||
|
[TagChmod]
|
||||||
|
other = "chmod"
|
||||||
|
|
||||||
|
[TagSecurity]
|
||||||
|
other = "安全"
|
||||||
|
|
||||||
|
[TagTable]
|
||||||
|
other = "表"
|
||||||
|
|
||||||
|
[TagService]
|
||||||
|
other = "服务"
|
||||||
|
|
||||||
|
[TagSystemctl]
|
||||||
|
other = "systemctl"
|
||||||
|
|
||||||
|
[TagControl]
|
||||||
|
other = "控制"
|
||||||
|
|
||||||
|
[TagNetwork]
|
||||||
|
other = "网络"
|
||||||
|
|
||||||
|
[TagFirewall]
|
||||||
|
other = "防火墙"
|
||||||
|
|
||||||
|
[TagRouting]
|
||||||
|
other = "路由"
|
||||||
|
|
||||||
|
[TagUser]
|
||||||
|
other = "用户"
|
||||||
|
|
||||||
|
[TagManagement]
|
||||||
|
other = "管理"
|
||||||
|
|
||||||
|
[TagKernel]
|
||||||
|
other = "内核"
|
||||||
|
|
||||||
|
[TagModule]
|
||||||
|
other = "模块"
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/plugin/soft_delete"
|
"gorm.io/plugin/soft_delete"
|
||||||
@@ -10,6 +12,104 @@ const (
|
|||||||
TABLE_NAME_ASSET = "asset"
|
TABLE_NAME_ASSET = "asset"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AccountAuthorization represents authorization info for a specific account
|
||||||
|
type AccountAuthorization struct {
|
||||||
|
Rids Slice[int] `json:"rids"` // Role IDs for ACL system
|
||||||
|
Permissions *AuthPermissions `json:"permissions"` // V2 permissions (connect, file_upload, etc.)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizationMap is a custom type that handles V1 to V2 authorization format conversion
|
||||||
|
type AuthorizationMap map[int]AccountAuthorization
|
||||||
|
|
||||||
|
// Scan implements the driver.Scanner interface for database deserialization
|
||||||
|
func (am *AuthorizationMap) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
*am = make(AuthorizationMap)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, ok := value.([]byte)
|
||||||
|
if !ok {
|
||||||
|
*am = make(AuthorizationMap)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to unmarshal as V2 format first
|
||||||
|
var v2Auth map[int]AccountAuthorization
|
||||||
|
if err := json.Unmarshal(bytes, &v2Auth); err == nil {
|
||||||
|
// Check if this is actually V2 format (has Permissions field)
|
||||||
|
for _, auth := range v2Auth {
|
||||||
|
if auth.Permissions != nil {
|
||||||
|
*am = AuthorizationMap(v2Auth)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to unmarshal as V1 format
|
||||||
|
var v1Auth map[int][]int
|
||||||
|
if err := json.Unmarshal(bytes, &v1Auth); err == nil {
|
||||||
|
// Successfully parsed as V1 format, convert to V2
|
||||||
|
defaultPermissions := getDefaultPermissionsForAsset()
|
||||||
|
v2Auth = make(map[int]AccountAuthorization)
|
||||||
|
for accountId, roleIds := range v1Auth {
|
||||||
|
v2Auth[accountId] = AccountAuthorization{
|
||||||
|
Rids: roleIds,
|
||||||
|
Permissions: &defaultPermissions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*am = AuthorizationMap(v2Auth)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot parse as either format, set to empty map
|
||||||
|
*am = make(AuthorizationMap)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver.Valuer interface for database serialization
|
||||||
|
func (am AuthorizationMap) Value() (driver.Value, error) {
|
||||||
|
if am == nil {
|
||||||
|
return "{}", nil
|
||||||
|
}
|
||||||
|
return json.Marshal(am)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements JSON unmarshaling for API requests
|
||||||
|
func (am *AuthorizationMap) UnmarshalJSON(data []byte) error {
|
||||||
|
// Try to unmarshal as V2 format first
|
||||||
|
var v2Auth map[int]AccountAuthorization
|
||||||
|
if err := json.Unmarshal(data, &v2Auth); err == nil {
|
||||||
|
*am = AuthorizationMap(v2Auth)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to unmarshal as V1 format
|
||||||
|
var v1Auth map[int][]int
|
||||||
|
if err := json.Unmarshal(data, &v1Auth); err == nil {
|
||||||
|
// Successfully parsed as V1 format, convert to V2
|
||||||
|
defaultPermissions := getDefaultPermissionsForAsset()
|
||||||
|
v2Auth = make(map[int]AccountAuthorization)
|
||||||
|
for accountId, roleIds := range v1Auth {
|
||||||
|
v2Auth[accountId] = AccountAuthorization{
|
||||||
|
Rids: roleIds,
|
||||||
|
Permissions: &defaultPermissions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*am = AuthorizationMap(v2Auth)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot parse as either format, set to empty map
|
||||||
|
*am = make(AuthorizationMap)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements JSON marshaling
|
||||||
|
func (am AuthorizationMap) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(map[int]AccountAuthorization(am))
|
||||||
|
}
|
||||||
|
|
||||||
type Asset struct {
|
type Asset struct {
|
||||||
Id int `json:"id" gorm:"column:id;primarykey;autoIncrement"`
|
Id int `json:"id" gorm:"column:id;primarykey;autoIncrement"`
|
||||||
Name string `json:"name" gorm:"column:name;uniqueIndex:name_del;size:128"`
|
Name string `json:"name" gorm:"column:name;uniqueIndex:name_del;size:128"`
|
||||||
@@ -18,11 +118,15 @@ type Asset struct {
|
|||||||
Ip string `json:"ip" gorm:"column:ip"`
|
Ip string `json:"ip" gorm:"column:ip"`
|
||||||
Protocols Slice[string] `json:"protocols" gorm:"column:protocols;type:text"`
|
Protocols Slice[string] `json:"protocols" gorm:"column:protocols;type:text"`
|
||||||
GatewayId int `json:"gateway_id" gorm:"column:gateway_id"`
|
GatewayId int `json:"gateway_id" gorm:"column:gateway_id"`
|
||||||
Authorization Map[int, Slice[int]] `json:"authorization" gorm:"column:authorization;type:text"`
|
Authorization AuthorizationMap `json:"authorization" gorm:"column:authorization;type:text"`
|
||||||
AccessAuth AccessAuth `json:"access_auth" gorm:"embedded;column:access_auth"`
|
AccessAuth AccessAuth `json:"access_auth" gorm:"embedded;column:access_auth"` // Deprecated: Use V2 fields below
|
||||||
Connectable bool `json:"connectable" gorm:"column:connectable"`
|
Connectable bool `json:"connectable" gorm:"column:connectable"`
|
||||||
NodeChain string `json:"node_chain" gorm:"-"`
|
NodeChain string `json:"node_chain" gorm:"-"`
|
||||||
|
|
||||||
|
// V2 Access Control (replaces AccessAuth)
|
||||||
|
AccessTimeControl *AccessTimeControl `json:"access_time_control,omitempty" gorm:"column:access_time_control;type:json"`
|
||||||
|
AssetCommandControl *AssetCommandControl `json:"asset_command_control,omitempty" gorm:"column:asset_command_control;type:json"`
|
||||||
|
|
||||||
Permissions []string `json:"permissions" gorm:"-"`
|
Permissions []string `json:"permissions" gorm:"-"`
|
||||||
ResourceId int `json:"resource_id" gorm:"column:resource_id"`
|
ResourceId int `json:"resource_id" gorm:"column:resource_id"`
|
||||||
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
|
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
|
||||||
@@ -32,6 +136,24 @@ type Asset struct {
|
|||||||
DeletedAt soft_delete.DeletedAt `json:"-" gorm:"column:deleted_at;uniqueIndex:name_del"`
|
DeletedAt soft_delete.DeletedAt `json:"-" gorm:"column:deleted_at;uniqueIndex:name_del"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getDefaultPermissionsForAsset returns default permissions for asset authorization conversion
|
||||||
|
func getDefaultPermissionsForAsset() AuthPermissions {
|
||||||
|
// Try to get from global config first
|
||||||
|
if config := GlobalConfig.Load(); config != nil {
|
||||||
|
return config.GetDefaultPermissionsAsAuthPermissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to connect-only permissions for security
|
||||||
|
return AuthPermissions{
|
||||||
|
Connect: true,
|
||||||
|
FileUpload: false,
|
||||||
|
FileDownload: false,
|
||||||
|
Copy: false,
|
||||||
|
Paste: false,
|
||||||
|
Share: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type AccessAuth struct {
|
type AccessAuth struct {
|
||||||
Start *time.Time `json:"start,omitempty" gorm:"column:start"`
|
Start *time.Time `json:"start,omitempty" gorm:"column:start"`
|
||||||
End *time.Time `json:"end,omitempty" gorm:"column:end"`
|
End *time.Time `json:"end,omitempty" gorm:"column:end"`
|
||||||
@@ -40,6 +162,8 @@ type AccessAuth struct {
|
|||||||
Allow bool `json:"allow" gorm:"column:allow"`
|
Allow bool `json:"allow" gorm:"column:allow"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AccessTimeControl and AssetCommandControl are defined in authorization_v2.go
|
||||||
|
|
||||||
type Range struct {
|
type Range struct {
|
||||||
Week int `json:"week" gorm:"column:week"`
|
Week int `json:"week" gorm:"column:week"`
|
||||||
Times Slice[string] `json:"times" gorm:"column:times"`
|
Times Slice[string] `json:"times" gorm:"column:times"`
|
||||||
|
@@ -75,12 +75,12 @@ type AssetInfo struct {
|
|||||||
Protocols Slice[string] `json:"protocols" gorm:"column:protocols"`
|
Protocols Slice[string] `json:"protocols" gorm:"column:protocols"`
|
||||||
Connectable bool `json:"connectable" gorm:"column:connectable"`
|
Connectable bool `json:"connectable" gorm:"column:connectable"`
|
||||||
NodeChain string `json:"node_chain" gorm:"-"`
|
NodeChain string `json:"node_chain" gorm:"-"`
|
||||||
*AccessAuth `json:"access_auth" gorm:"column:access_auth"`
|
|
||||||
Authorization Map[int, Slice[int]] `json:"-" gorm:"column:authorization"`
|
Authorization Map[int, Slice[int]] `json:"-" gorm:"column:authorization"`
|
||||||
GatewayId int `json:"-" gorm:"column:gateway_id"`
|
GatewayId int `json:"-" gorm:"column:gateway_id"`
|
||||||
Gateway *GatewayInfo `json:"gateway,omitempty" gorm:"-"`
|
Gateway *GatewayInfo `json:"gateway,omitempty" gorm:"-"`
|
||||||
Accounts []*AccountInfo `json:"accounts" gorm:"-"`
|
Accounts []*AccountInfo `json:"accounts" gorm:"-"`
|
||||||
Commands []*CmdInfo `json:"commands" gorm:"-"`
|
Commands []*CmdInfo `json:"commands" gorm:"-"`
|
||||||
|
*AccessAuth `json:"access_auth" gorm:"column:access_auth"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *AssetInfo) GetId() int {
|
func (m *AssetInfo) GetId() int {
|
||||||
|
391
backend/internal/model/authorization_v2.go
Normal file
391
backend/internal/model/authorization_v2.go
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/plugin/soft_delete"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CustomTime struct {
|
||||||
|
time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *CustomTime) UnmarshalJSON(data []byte) error {
|
||||||
|
str := string(data)
|
||||||
|
str = strings.Trim(str, "\"")
|
||||||
|
|
||||||
|
parsedTime, err := time.Parse("2006-01-02 15:04:05", str)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse time %s: %v", str, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ct.Time = parsedTime
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct CustomTime) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(ct.Time.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *CustomTime) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
ct.Time = time.Time{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if t, ok := value.(time.Time); ok {
|
||||||
|
ct.Time = t
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("cannot scan %T into CustomTime", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct CustomTime) Value() (driver.Value, error) {
|
||||||
|
return ct.Time, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectorType defines the type of target selector
|
||||||
|
type SelectorType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SelectorTypeAll SelectorType = "all"
|
||||||
|
SelectorTypeIds SelectorType = "ids"
|
||||||
|
SelectorTypeRegex SelectorType = "regex"
|
||||||
|
SelectorTypeTags SelectorType = "tags"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthAction defines the supported authorization actions
|
||||||
|
type AuthAction string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ActionConnect AuthAction = "connect"
|
||||||
|
ActionFileUpload AuthAction = "file_upload"
|
||||||
|
ActionFileDownload AuthAction = "file_download"
|
||||||
|
ActionCopy AuthAction = "copy"
|
||||||
|
ActionPaste AuthAction = "paste"
|
||||||
|
ActionShare AuthAction = "share"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TimeRange defines time restrictions
|
||||||
|
type TimeRange struct {
|
||||||
|
StartTime string `json:"start_time" gorm:"column:start_time"`
|
||||||
|
EndTime string `json:"end_time" gorm:"column:end_time"`
|
||||||
|
Weekdays Slice[int] `json:"weekdays" gorm:"column:weekdays"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TimeRange) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(value.([]byte), t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TimeRange) Value() (driver.Value, error) {
|
||||||
|
return json.Marshal(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimeRanges is a custom slice type for TimeRange
|
||||||
|
type TimeRanges []TimeRange
|
||||||
|
|
||||||
|
func (t *TimeRanges) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(value.([]byte), t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TimeRanges) Value() (driver.Value, error) {
|
||||||
|
return json.Marshal(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccessTimeControl defines time-based access control (used by both Asset and AuthorizationV2)
|
||||||
|
type AccessTimeControl struct {
|
||||||
|
Enabled bool `json:"enabled" gorm:"column:enabled"`
|
||||||
|
TimeRanges TimeRanges `json:"time_ranges" gorm:"column:time_ranges"`
|
||||||
|
Timezone string `json:"timezone" gorm:"column:timezone"` // e.g., "Asia/Shanghai"
|
||||||
|
Comment string `json:"comment" gorm:"column:comment"` // Description of the time restriction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AccessTimeControl) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(value.([]byte), a)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AccessTimeControl) Value() (driver.Value, error) {
|
||||||
|
return json.Marshal(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssetCommandControl defines command control restrictions for assets
|
||||||
|
type AssetCommandControl struct {
|
||||||
|
Enabled bool `json:"enabled" gorm:"column:enabled"`
|
||||||
|
CmdIds Slice[int] `json:"cmd_ids" gorm:"column:cmd_ids"` // Command IDs to control
|
||||||
|
TemplateIds Slice[int] `json:"template_ids" gorm:"column:template_ids"` // Command template IDs
|
||||||
|
Comment string `json:"comment" gorm:"column:comment"` // Description of the command restriction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AssetCommandControl) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(value.([]byte), a)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AssetCommandControl) Value() (driver.Value, error) {
|
||||||
|
return json.Marshal(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TargetSelector defines how to select targets (nodes, assets, accounts)
|
||||||
|
type TargetSelector struct {
|
||||||
|
Type SelectorType `json:"type" gorm:"column:type"`
|
||||||
|
Values Slice[string] `json:"values" gorm:"column:values"`
|
||||||
|
ExcludeIds Slice[int] `json:"exclude_ids" gorm:"column:exclude_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TargetSelector) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(value.([]byte), t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TargetSelector) Value() (driver.Value, error) {
|
||||||
|
return json.Marshal(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthPermissions defines the permissions for different actions
|
||||||
|
type AuthPermissions struct {
|
||||||
|
Connect bool `json:"connect" gorm:"column:connect"`
|
||||||
|
FileUpload bool `json:"file_upload" gorm:"column:file_upload"`
|
||||||
|
FileDownload bool `json:"file_download" gorm:"column:file_download"`
|
||||||
|
Copy bool `json:"copy" gorm:"column:copy"`
|
||||||
|
Paste bool `json:"paste" gorm:"column:paste"`
|
||||||
|
Share bool `json:"share" gorm:"column:share"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AuthPermissions) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(value.([]byte), p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p AuthPermissions) Value() (driver.Value, error) {
|
||||||
|
return json.Marshal(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AuthPermissions) HasPermission(action AuthAction) bool {
|
||||||
|
switch action {
|
||||||
|
case ActionConnect:
|
||||||
|
return p.Connect
|
||||||
|
case ActionFileUpload:
|
||||||
|
return p.FileUpload
|
||||||
|
case ActionFileDownload:
|
||||||
|
return p.FileDownload
|
||||||
|
case ActionCopy:
|
||||||
|
return p.Copy
|
||||||
|
case ActionPaste:
|
||||||
|
return p.Paste
|
||||||
|
case ActionShare:
|
||||||
|
return p.Share
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommandAction defines simple command actions
|
||||||
|
type CommandAction string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CommandActionAllow CommandAction = "allow"
|
||||||
|
CommandActionDeny CommandAction = "deny"
|
||||||
|
CommandActionAudit CommandAction = "audit" // Log but allow
|
||||||
|
)
|
||||||
|
|
||||||
|
// AccessControl defines access control restrictions for authorization rules with time template support
|
||||||
|
type AccessControl struct {
|
||||||
|
IPWhitelist Slice[string] `json:"ip_whitelist" gorm:"column:ip_whitelist"`
|
||||||
|
|
||||||
|
// Time control options
|
||||||
|
TimeTemplate *TimeTemplateReference `json:"time_template" gorm:"column:time_template;type:json"` // Reference to template
|
||||||
|
CustomTimeRanges TimeRanges `json:"custom_time_ranges" gorm:"column:custom_time_ranges;type:json"` // Direct definition
|
||||||
|
Timezone string `json:"timezone" gorm:"column:timezone;size:64"`
|
||||||
|
|
||||||
|
// Command control
|
||||||
|
CmdIds Slice[int] `json:"cmd_ids" gorm:"column:cmd_ids"`
|
||||||
|
TemplateIds Slice[int] `json:"template_ids" gorm:"column:template_ids"`
|
||||||
|
|
||||||
|
MaxSessions int `json:"max_sessions" gorm:"column:max_sessions"`
|
||||||
|
SessionTimeout int `json:"session_timeout" gorm:"column:session_timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AccessControl) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(value.([]byte), a)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AccessControl) Value() (driver.Value, error) {
|
||||||
|
return json.Marshal(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizationV2 is the new flexible authorization model
|
||||||
|
type AuthorizationV2 struct {
|
||||||
|
Id int `json:"id" gorm:"column:id;primarykey;autoIncrement"`
|
||||||
|
Name string `json:"name" gorm:"column:name;uniqueIndex:name_del;size:128"`
|
||||||
|
Description string `json:"description" gorm:"column:description"`
|
||||||
|
Enabled bool `json:"enabled" gorm:"column:enabled;default:true"`
|
||||||
|
|
||||||
|
// Rule validity period
|
||||||
|
ValidFrom *CustomTime `json:"valid_from" gorm:"column:valid_from"`
|
||||||
|
ValidTo *CustomTime `json:"valid_to" gorm:"column:valid_to"`
|
||||||
|
|
||||||
|
// Target selectors
|
||||||
|
NodeSelector TargetSelector `json:"node_selector" gorm:"column:node_selector;type:json"`
|
||||||
|
AssetSelector TargetSelector `json:"asset_selector" gorm:"column:asset_selector;type:json"`
|
||||||
|
AccountSelector TargetSelector `json:"account_selector" gorm:"column:account_selector;type:json"`
|
||||||
|
|
||||||
|
// Permissions configuration
|
||||||
|
Permissions AuthPermissions `json:"permissions" gorm:"column:permissions;type:json"`
|
||||||
|
|
||||||
|
// Access control with time template support
|
||||||
|
AccessControl AccessControl `json:"access_control" gorm:"column:access_control;type:json"`
|
||||||
|
|
||||||
|
// Role IDs for ACL integration
|
||||||
|
Rids Slice[int] `json:"rids" gorm:"column:rids"`
|
||||||
|
|
||||||
|
// Standard fields
|
||||||
|
ResourceId int `json:"resource_id" gorm:"column:resource_id"`
|
||||||
|
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
|
||||||
|
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
|
||||||
|
DeletedAt soft_delete.DeletedAt `json:"-" gorm:"column:deleted_at;uniqueIndex:name_del"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AuthorizationV2) TableName() string {
|
||||||
|
return "authorization_v2"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AuthorizationV2) GetName() string {
|
||||||
|
return m.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AuthorizationV2) GetId() int {
|
||||||
|
return m.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AuthorizationV2) SetId(id int) {
|
||||||
|
m.Id = id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AuthorizationV2) SetCreatorId(creatorId int) {
|
||||||
|
m.CreatorId = creatorId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AuthorizationV2) SetUpdaterId(updaterId int) {
|
||||||
|
m.UpdaterId = updaterId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AuthorizationV2) SetResourceId(resourceId int) {
|
||||||
|
m.ResourceId = resourceId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AuthorizationV2) GetResourceId() int {
|
||||||
|
return m.ResourceId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AuthorizationV2) SetPerms(perms []string) {}
|
||||||
|
|
||||||
|
// IsValid checks if the authorization rule is currently valid based on time period
|
||||||
|
func (m *AuthorizationV2) IsValid(checkTime time.Time) bool {
|
||||||
|
if !m.Enabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check valid from time
|
||||||
|
if m.ValidFrom != nil && checkTime.Before(m.ValidFrom.Time) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check valid to time
|
||||||
|
if m.ValidTo != nil && checkTime.After(m.ValidTo.Time) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCurrentlyValid checks if the authorization rule is currently valid
|
||||||
|
func (m *AuthorizationV2) IsCurrentlyValid() bool {
|
||||||
|
return m.IsValid(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthRequest represents an authorization request
|
||||||
|
type AuthRequest struct {
|
||||||
|
UserId int `json:"user_id"`
|
||||||
|
NodeId int `json:"node_id"`
|
||||||
|
AssetId int `json:"asset_id"`
|
||||||
|
AccountId int `json:"account_id"`
|
||||||
|
Action AuthAction `json:"action"`
|
||||||
|
ClientIP string `json:"client_ip"`
|
||||||
|
UserAgent string `json:"user_agent"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchAuthRequest represents a batch authorization request for multiple actions
|
||||||
|
type BatchAuthRequest struct {
|
||||||
|
UserId int `json:"user_id"`
|
||||||
|
NodeId int `json:"node_id"`
|
||||||
|
AssetId int `json:"asset_id"`
|
||||||
|
AccountId int `json:"account_id"`
|
||||||
|
Actions []AuthAction `json:"actions"`
|
||||||
|
ClientIP string `json:"client_ip"`
|
||||||
|
UserAgent string `json:"user_agent"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthResult represents the result of an authorization check
|
||||||
|
type AuthResult struct {
|
||||||
|
Allowed bool `json:"allowed"`
|
||||||
|
Permissions AuthPermissions `json:"permissions"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
RuleId int `json:"rule_id"`
|
||||||
|
RuleName string `json:"rule_name"`
|
||||||
|
Restrictions map[string]interface{} `json:"restrictions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchAuthResult represents the result of a batch authorization check
|
||||||
|
type BatchAuthResult struct {
|
||||||
|
Results map[AuthAction]*AuthResult `json:"results"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAllowed checks if a specific action is allowed in the batch result
|
||||||
|
func (r *BatchAuthResult) IsAllowed(action AuthAction) bool {
|
||||||
|
if result, exists := r.Results[action]; exists {
|
||||||
|
return result.Allowed
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResult returns the authorization result for a specific action
|
||||||
|
func (r *BatchAuthResult) GetResult(action AuthAction) *AuthResult {
|
||||||
|
return r.Results[action]
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommandCheckResult represents the result of a command permission check
|
||||||
|
type CommandCheckResult struct {
|
||||||
|
Allowed bool `json:"allowed"`
|
||||||
|
Action CommandAction `json:"action"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultAuthorizationV2 for caching
|
||||||
|
var DefaultAuthorizationV2 = &AuthorizationV2{}
|
@@ -11,30 +11,22 @@ var (
|
|||||||
GlobalConfig atomic.Pointer[Config]
|
GlobalConfig atomic.Pointer[Config]
|
||||||
)
|
)
|
||||||
|
|
||||||
type SshConfig struct {
|
// DefaultPermissions defines default permissions for authorization
|
||||||
Copy bool `json:"copy" gorm:"column:copy"`
|
type DefaultPermissions struct {
|
||||||
Paste bool `json:"paste" gorm:"column:paste"`
|
Connect bool `json:"connect" gorm:"column:connect"`
|
||||||
}
|
FileUpload bool `json:"file_upload" gorm:"column:file_upload"`
|
||||||
type RdpConfig struct {
|
FileDownload bool `json:"file_download" gorm:"column:file_download"`
|
||||||
Copy bool `json:"copy" gorm:"column:copy"`
|
|
||||||
Paste bool `json:"paste" gorm:"column:paste"`
|
|
||||||
EnableDrive bool `json:"enable_drive" gorm:"column:enable_drive"`
|
|
||||||
DrivePath string `json:"drive_path" gorm:"column:drive_path"`
|
|
||||||
CreateDrivePath bool `json:"create_drive_path" gorm:"column:create_drive_path"`
|
|
||||||
DisableUpload bool `json:"disable_upload" gorm:"column:disable_upload"`
|
|
||||||
DisableDownload bool `json:"disable_download" gorm:"column:disable_download"`
|
|
||||||
}
|
|
||||||
type VncConfig struct {
|
|
||||||
Copy bool `json:"copy" gorm:"column:copy"`
|
Copy bool `json:"copy" gorm:"column:copy"`
|
||||||
Paste bool `json:"paste" gorm:"column:paste"`
|
Paste bool `json:"paste" gorm:"column:paste"`
|
||||||
|
Share bool `json:"share" gorm:"column:share"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Id int `json:"id" gorm:"column:id;primarykey;autoIncrement"`
|
Id int `json:"id" gorm:"column:id;primarykey;autoIncrement"`
|
||||||
Timeout int `json:"timeout" gorm:"column:timeout"`
|
Timeout int `json:"timeout" gorm:"column:timeout"`
|
||||||
SshConfig SshConfig `json:"ssh_config" gorm:"embedded;embeddedPrefix:ssh_;column:ssh_config"`
|
|
||||||
RdpConfig RdpConfig `json:"rdp_config" gorm:"embedded;embeddedPrefix:rdp_;column:rdp_config"`
|
// Default permissions for authorization creation
|
||||||
VncConfig VncConfig `json:"vnc_config" gorm:"embedded;embeddedPrefix:vnc_;column:vnc_config"`
|
DefaultPermissions DefaultPermissions `json:"default_permissions" gorm:"embedded;embeddedPrefix:default_"`
|
||||||
|
|
||||||
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
|
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
|
||||||
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
|
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
|
||||||
@@ -47,6 +39,23 @@ func (m *Config) TableName() string {
|
|||||||
return "config"
|
return "config"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDefaultPermissions returns the default permissions configuration
|
||||||
|
func (c *Config) GetDefaultPermissions() DefaultPermissions {
|
||||||
|
return c.DefaultPermissions
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaultPermissionsAsAuthPermissions converts to AuthPermissions format
|
||||||
|
func (c *Config) GetDefaultPermissionsAsAuthPermissions() AuthPermissions {
|
||||||
|
return AuthPermissions{
|
||||||
|
Connect: c.DefaultPermissions.Connect,
|
||||||
|
FileUpload: c.DefaultPermissions.FileUpload,
|
||||||
|
FileDownload: c.DefaultPermissions.FileDownload,
|
||||||
|
Copy: c.DefaultPermissions.Copy,
|
||||||
|
Paste: c.DefaultPermissions.Paste,
|
||||||
|
Share: c.DefaultPermissions.Share,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ScheduleConfig defines configuration for scheduled tasks
|
// ScheduleConfig defines configuration for scheduled tasks
|
||||||
type ScheduleConfig struct {
|
type ScheduleConfig struct {
|
||||||
ConnectableCheckInterval time.Duration `json:"connectable_check_interval" yaml:"connectable_check_interval" default:"30m"`
|
ConnectableCheckInterval time.Duration `json:"connectable_check_interval" yaml:"connectable_check_interval" default:"30m"`
|
||||||
@@ -66,3 +75,18 @@ func GetDefaultScheduleConfig() *ScheduleConfig {
|
|||||||
ConnectTimeout: 3 * time.Second, // 3 second timeout for connectivity tests
|
ConnectTimeout: 3 * time.Second, // 3 second timeout for connectivity tests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDefaultConfig returns a default configuration with reasonable defaults
|
||||||
|
func GetDefaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
Timeout: 1800, // 30 minutes
|
||||||
|
DefaultPermissions: DefaultPermissions{
|
||||||
|
Connect: true,
|
||||||
|
FileUpload: true,
|
||||||
|
FileDownload: true,
|
||||||
|
Copy: true,
|
||||||
|
Paste: true,
|
||||||
|
Share: false, // Share is disabled by default for security
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -5,6 +5,7 @@ var (
|
|||||||
DefaultAsset = &Asset{}
|
DefaultAsset = &Asset{}
|
||||||
DefaultAuthorization = &Authorization{}
|
DefaultAuthorization = &Authorization{}
|
||||||
DefaultCommand = &Command{}
|
DefaultCommand = &Command{}
|
||||||
|
DefaultCommandTemplate = &CommandTemplate{}
|
||||||
DefaultConfig = &Config{}
|
DefaultConfig = &Config{}
|
||||||
DefaultFileHistory = &FileHistory{}
|
DefaultFileHistory = &FileHistory{}
|
||||||
DefaultGateway = &Gateway{}
|
DefaultGateway = &Gateway{}
|
||||||
@@ -18,4 +19,5 @@ var (
|
|||||||
DefaultUserPreference = &UserPreference{}
|
DefaultUserPreference = &UserPreference{}
|
||||||
DefaultStorageConfig = &StorageConfig{}
|
DefaultStorageConfig = &StorageConfig{}
|
||||||
DefaultStorageMetrics = &StorageMetrics{}
|
DefaultStorageMetrics = &StorageMetrics{}
|
||||||
|
DefaultMigrationRecord = &MigrationRecord{}
|
||||||
)
|
)
|
||||||
|
36
backend/internal/model/migration.go
Normal file
36
backend/internal/model/migration.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MigrationRecord tracks the status of different migrations
|
||||||
|
type MigrationRecord struct {
|
||||||
|
Id int `json:"id" gorm:"column:id;primarykey;autoIncrement"`
|
||||||
|
MigrationName string `json:"migration_name" gorm:"column:migration_name;uniqueIndex;size:128;not null"`
|
||||||
|
Status string `json:"status" gorm:"column:status;size:32;not null"` // pending, running, completed, failed
|
||||||
|
StartedAt *time.Time `json:"started_at" gorm:"column:started_at"`
|
||||||
|
CompletedAt *time.Time `json:"completed_at" gorm:"column:completed_at"`
|
||||||
|
ErrorMessage string `json:"error_message" gorm:"column:error_message;type:text"`
|
||||||
|
RecordsCount int `json:"records_count" gorm:"column:records_count;default:0"` // Number of records migrated
|
||||||
|
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MigrationRecord) TableName() string {
|
||||||
|
return "migration_records"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migration constants
|
||||||
|
const (
|
||||||
|
MigrationAuthV1ToV2 = "auth_v1_to_v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Migration status constants
|
||||||
|
const (
|
||||||
|
MigrationStatusPending = "pending"
|
||||||
|
MigrationStatusRunning = "running"
|
||||||
|
MigrationStatusCompleted = "completed"
|
||||||
|
MigrationStatusFailed = "failed"
|
||||||
|
)
|
165
backend/internal/model/time_template.go
Normal file
165
backend/internal/model/time_template.go
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/plugin/soft_delete"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TimeTemplate defines predefined time access templates
|
||||||
|
type TimeTemplate struct {
|
||||||
|
Id int `json:"id" gorm:"column:id;primarykey;autoIncrement"`
|
||||||
|
Name string `json:"name" gorm:"column:name;uniqueIndex:name_del;size:128;not null"`
|
||||||
|
Description string `json:"description" gorm:"column:description"`
|
||||||
|
Category string `json:"category" gorm:"column:category;size:64"` // work, maintenance, emergency, etc.
|
||||||
|
|
||||||
|
// Time configuration
|
||||||
|
TimeRanges TimeRanges `json:"time_ranges" gorm:"column:time_ranges;type:json"`
|
||||||
|
Timezone string `json:"timezone" gorm:"column:timezone;size:64;default:'Asia/Shanghai'"`
|
||||||
|
|
||||||
|
// Status and metadata
|
||||||
|
IsBuiltIn bool `json:"is_builtin" gorm:"column:is_builtin;default:false"` // System built-in templates
|
||||||
|
IsActive bool `json:"is_active" gorm:"column:is_active;default:true"`
|
||||||
|
|
||||||
|
// Usage statistics
|
||||||
|
UsageCount int `json:"usage_count" gorm:"column:usage_count;default:0"`
|
||||||
|
|
||||||
|
// Standard fields
|
||||||
|
ResourceId int `json:"resource_id" gorm:"column:resource_id"`
|
||||||
|
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
|
||||||
|
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
|
||||||
|
DeletedAt soft_delete.DeletedAt `json:"-" gorm:"column:deleted_at;uniqueIndex:name_del"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TimeTemplate) TableName() string {
|
||||||
|
return "time_template"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TimeTemplate) GetName() string {
|
||||||
|
return m.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TimeTemplate) GetId() int {
|
||||||
|
return m.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TimeTemplate) SetId(id int) {
|
||||||
|
m.Id = id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TimeTemplate) SetCreatorId(creatorId int) {
|
||||||
|
m.CreatorId = creatorId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TimeTemplate) SetUpdaterId(updaterId int) {
|
||||||
|
m.UpdaterId = updaterId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TimeTemplate) SetResourceId(resourceId int) {
|
||||||
|
m.ResourceId = resourceId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TimeTemplate) GetResourceId() int {
|
||||||
|
return m.ResourceId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TimeTemplate) SetPerms(perms []string) {}
|
||||||
|
|
||||||
|
// TimeTemplateReference defines how authorization rules reference time templates
|
||||||
|
type TimeTemplateReference struct {
|
||||||
|
TemplateId int `json:"template_id" gorm:"column:template_id"`
|
||||||
|
TemplateName string `json:"template_name" gorm:"column:template_name"` // For display
|
||||||
|
CustomRanges TimeRanges `json:"custom_ranges" gorm:"column:custom_ranges;type:json"` // Additional custom time ranges
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TimeTemplateReference) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(value.([]byte), t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TimeTemplateReference) Value() (driver.Value, error) {
|
||||||
|
return json.Marshal(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuiltInTimeTemplates defines system built-in time templates
|
||||||
|
var BuiltInTimeTemplates = []TimeTemplate{
|
||||||
|
{
|
||||||
|
Name: "Business Hours",
|
||||||
|
Description: "Standard business hours: Monday to Friday 9:00-18:00",
|
||||||
|
Category: "work",
|
||||||
|
TimeRanges: TimeRanges{
|
||||||
|
{
|
||||||
|
StartTime: "09:00",
|
||||||
|
EndTime: "18:00",
|
||||||
|
Weekdays: Slice[int]{1, 2, 3, 4, 5}, // Monday to Friday
|
||||||
|
},
|
||||||
|
},
|
||||||
|
IsBuiltIn: true,
|
||||||
|
IsActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Weekend Duty",
|
||||||
|
Description: "Weekend duty hours: Saturday and Sunday 10:00-16:00",
|
||||||
|
Category: "duty",
|
||||||
|
TimeRanges: TimeRanges{
|
||||||
|
{
|
||||||
|
StartTime: "10:00",
|
||||||
|
EndTime: "16:00",
|
||||||
|
Weekdays: Slice[int]{6, 7}, // Saturday and Sunday
|
||||||
|
},
|
||||||
|
},
|
||||||
|
IsBuiltIn: true,
|
||||||
|
IsActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Maintenance Window",
|
||||||
|
Description: "System maintenance window: Sunday 2:00-6:00",
|
||||||
|
Category: "maintenance",
|
||||||
|
TimeRanges: TimeRanges{
|
||||||
|
{
|
||||||
|
StartTime: "02:00",
|
||||||
|
EndTime: "06:00",
|
||||||
|
Weekdays: Slice[int]{7}, // Sunday
|
||||||
|
},
|
||||||
|
},
|
||||||
|
IsBuiltIn: true,
|
||||||
|
IsActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "24x7 Access",
|
||||||
|
Description: "24x7 around the clock access",
|
||||||
|
Category: "always",
|
||||||
|
TimeRanges: TimeRanges{
|
||||||
|
{
|
||||||
|
StartTime: "00:00",
|
||||||
|
EndTime: "23:59",
|
||||||
|
Weekdays: Slice[int]{1, 2, 3, 4, 5, 6, 7}, // All days
|
||||||
|
},
|
||||||
|
},
|
||||||
|
IsBuiltIn: true,
|
||||||
|
IsActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Emergency Response",
|
||||||
|
Description: "Emergency response hours: weekdays 18:00-22:00",
|
||||||
|
Category: "emergency",
|
||||||
|
TimeRanges: TimeRanges{
|
||||||
|
{
|
||||||
|
StartTime: "18:00",
|
||||||
|
EndTime: "22:00",
|
||||||
|
Weekdays: Slice[int]{1, 2, 3, 4, 5}, // Monday to Friday
|
||||||
|
},
|
||||||
|
},
|
||||||
|
IsBuiltIn: true,
|
||||||
|
IsActive: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultTimeTemplate for caching
|
||||||
|
var DefaultTimeTemplate = &TimeTemplate{}
|
@@ -27,7 +27,6 @@ const (
|
|||||||
type AssetRepository interface {
|
type AssetRepository interface {
|
||||||
GetById(ctx context.Context, id int) (*model.Asset, error)
|
GetById(ctx context.Context, id int) (*model.Asset, error)
|
||||||
AttachNodeChain(ctx context.Context, assets []*model.Asset) error
|
AttachNodeChain(ctx context.Context, assets []*model.Asset) error
|
||||||
ApplyAuthorizationFilters(ctx *gin.Context, assets []*model.Asset, authorizationIds []*model.AuthorizationIds, nodeIds, accountIds []int)
|
|
||||||
BuildQuery(ctx *gin.Context) (*gorm.DB, error)
|
BuildQuery(ctx *gin.Context) (*gorm.DB, error)
|
||||||
FilterByParentId(db *gorm.DB, parentId int) (*gorm.DB, error)
|
FilterByParentId(db *gorm.DB, parentId int) (*gorm.DB, error)
|
||||||
GetAssetIdsByAuthorization(ctx *gin.Context, authorizationIds []*model.AuthorizationIds) ([]int, []int, []int, error)
|
GetAssetIdsByAuthorization(ctx *gin.Context, authorizationIds []*model.AuthorizationIds) ([]int, []int, []int, error)
|
||||||
@@ -109,45 +108,6 @@ func (r *assetRepository) AttachNodeChain(ctx context.Context, assets []*model.A
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyAuthorizationFilters applies authorization filters to assets
|
|
||||||
func (r *assetRepository) ApplyAuthorizationFilters(ctx *gin.Context, assets []*model.Asset, authorizationIds []*model.AuthorizationIds, nodeIds, accountIds []int) {
|
|
||||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
||||||
if acl.IsAdmin(currentUser) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
info := cast.ToBool(ctx.Query("info"))
|
|
||||||
noInfoIds := make([]int, 0)
|
|
||||||
|
|
||||||
if !info {
|
|
||||||
t := dbpkg.DB.Model(model.DefaultAsset)
|
|
||||||
assetResIds, _ := acl.GetRoleResourceIds(ctx, currentUser.GetRid(), config.RESOURCE_ASSET)
|
|
||||||
t, _ = r.handleAssetIds(ctx, t, assetResIds)
|
|
||||||
t.Pluck("id", &noInfoIds)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, a := range assets {
|
|
||||||
if lo.Contains(nodeIds, a.ParentId) || lo.Contains(noInfoIds, a.Id) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if lo.ContainsBy(authorizationIds, func(item *model.AuthorizationIds) bool {
|
|
||||||
return item.AssetId == a.Id && item.NodeId == 0 && item.AccountId == 0
|
|
||||||
}) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ids := lo.Map(lo.Filter(authorizationIds, func(item *model.AuthorizationIds, _ int) bool {
|
|
||||||
return item.AssetId == a.Id && item.AccountId != 0 && item.NodeId == 0
|
|
||||||
}),
|
|
||||||
func(item *model.AuthorizationIds, _ int) int { return item.AccountId })
|
|
||||||
|
|
||||||
for k := range a.Authorization {
|
|
||||||
if !lo.Contains(ids, k) && !lo.Contains(accountIds, k) {
|
|
||||||
delete(a.Authorization, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAssetIdsByAuthorization gets asset IDs by authorization
|
// GetAssetIdsByAuthorization gets asset IDs by authorization
|
||||||
func (r *assetRepository) GetAssetIdsByAuthorization(ctx *gin.Context, authorizationIds []*model.AuthorizationIds) ([]int, []int, []int, error) {
|
func (r *assetRepository) GetAssetIdsByAuthorization(ctx *gin.Context, authorizationIds []*model.AuthorizationIds) ([]int, []int, []int, error) {
|
||||||
ctx.Set(kAuthorizationIds, authorizationIds)
|
ctx.Set(kAuthorizationIds, authorizationIds)
|
||||||
|
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/veops/oneterm/internal/model"
|
"github.com/veops/oneterm/internal/model"
|
||||||
@@ -50,7 +49,6 @@ func (r *AuthorizationRepository) UpsertAuthorization(ctx context.Context, auth
|
|||||||
return r.db.Save(auth).Error
|
return r.db.Save(auth).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAuthorizationById 根据ID获取授权
|
|
||||||
func (r *AuthorizationRepository) GetAuthorizationById(ctx context.Context, id int) (*model.Authorization, error) {
|
func (r *AuthorizationRepository) GetAuthorizationById(ctx context.Context, id int) (*model.Authorization, error) {
|
||||||
auth := &model.Authorization{}
|
auth := &model.Authorization{}
|
||||||
err := r.db.Model(auth).Where("id = ?", id).First(auth).Error
|
err := r.db.Model(auth).Where("id = ?", id).First(auth).Error
|
||||||
@@ -60,7 +58,6 @@ func (r *AuthorizationRepository) GetAuthorizationById(ctx context.Context, id i
|
|||||||
return auth, nil
|
return auth, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAuthorizationByFields 根据字段获取授权
|
|
||||||
func (r *AuthorizationRepository) GetAuthorizationByFields(ctx context.Context, nodeId, assetId, accountId int) (*model.Authorization, error) {
|
func (r *AuthorizationRepository) GetAuthorizationByFields(ctx context.Context, nodeId, assetId, accountId int) (*model.Authorization, error) {
|
||||||
auth := &model.Authorization{}
|
auth := &model.Authorization{}
|
||||||
err := r.db.Model(auth).
|
err := r.db.Model(auth).
|
||||||
@@ -104,8 +101,21 @@ func (r *AuthorizationRepository) GetAuthorizations(ctx context.Context, nodeId,
|
|||||||
|
|
||||||
func (r *AuthorizationRepository) GetAuthsByAsset(ctx context.Context, asset *model.Asset) ([]*model.Authorization, error) {
|
func (r *AuthorizationRepository) GetAuthsByAsset(ctx context.Context, asset *model.Asset) ([]*model.Authorization, error) {
|
||||||
var data []*model.Authorization
|
var data []*model.Authorization
|
||||||
|
|
||||||
|
// Extract account IDs from the new Authorization structure
|
||||||
|
accountIds := make([]int, 0, len(asset.Authorization))
|
||||||
|
for accountId := range asset.Authorization {
|
||||||
|
if accountId != 0 {
|
||||||
|
accountIds = append(accountIds, accountId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(accountIds) == 0 {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
err := r.db.Model(&model.Authorization{}).
|
err := r.db.Model(&model.Authorization{}).
|
||||||
Where("asset_id=? AND account_id IN ? AND node_id=0", asset.Id, lo.Without(lo.Keys(asset.Authorization), 0)).
|
Where("asset_id=? AND account_id IN ? AND node_id=0", asset.Id, accountIds).
|
||||||
Find(&data).Error
|
Find(&data).Error
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
90
backend/internal/repository/authorization_v2.go
Normal file
90
backend/internal/repository/authorization_v2.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/veops/oneterm/internal/model"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IAuthorizationV2Repository defines the interface for authorization V2 repository
|
||||||
|
type IAuthorizationV2Repository interface {
|
||||||
|
Create(ctx context.Context, auth *model.AuthorizationV2) error
|
||||||
|
GetById(ctx context.Context, id int) (*model.AuthorizationV2, error)
|
||||||
|
Update(ctx context.Context, auth *model.AuthorizationV2) error
|
||||||
|
Delete(ctx context.Context, id int) error
|
||||||
|
GetUserRules(ctx context.Context, userRids []int) ([]*model.AuthorizationV2, error)
|
||||||
|
GetAll(ctx context.Context) ([]*model.AuthorizationV2, error)
|
||||||
|
GetByResourceIds(ctx context.Context, resourceIds []int) ([]*model.AuthorizationV2, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizationV2Repository implements IAuthorizationV2Repository
|
||||||
|
type AuthorizationV2Repository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthorizationV2Repository creates a new authorization V2 repository
|
||||||
|
func NewAuthorizationV2Repository(db *gorm.DB) IAuthorizationV2Repository {
|
||||||
|
return &AuthorizationV2Repository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create creates a new authorization V2 rule
|
||||||
|
func (r *AuthorizationV2Repository) Create(ctx context.Context, auth *model.AuthorizationV2) error {
|
||||||
|
return r.db.Create(auth).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetById retrieves an authorization V2 rule by ID
|
||||||
|
func (r *AuthorizationV2Repository) GetById(ctx context.Context, id int) (*model.AuthorizationV2, error) {
|
||||||
|
auth := &model.AuthorizationV2{}
|
||||||
|
err := r.db.Where("id = ?", id).First(auth).Error
|
||||||
|
return auth, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates an authorization V2 rule
|
||||||
|
func (r *AuthorizationV2Repository) Update(ctx context.Context, auth *model.AuthorizationV2) error {
|
||||||
|
return r.db.Model(auth).Where("id = ?", auth.Id).Updates(auth).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes an authorization V2 rule
|
||||||
|
func (r *AuthorizationV2Repository) Delete(ctx context.Context, id int) error {
|
||||||
|
return r.db.Where("id = ?", id).Delete(&model.AuthorizationV2{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserRules retrieves authorization rules for specific user role IDs
|
||||||
|
func (r *AuthorizationV2Repository) GetUserRules(ctx context.Context, userRids []int) ([]*model.AuthorizationV2, error) {
|
||||||
|
if len(userRids) == 0 {
|
||||||
|
return []*model.AuthorizationV2{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonRids, err := json.Marshal(userRids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var rules []*model.AuthorizationV2
|
||||||
|
err = r.db.Where("enabled = ? AND JSON_OVERLAPS(rids, ?)", true, string(jsonRids)).
|
||||||
|
Order("id ASC").
|
||||||
|
Find(&rules).Error
|
||||||
|
return rules, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAll retrieves all authorization V2 rules
|
||||||
|
func (r *AuthorizationV2Repository) GetAll(ctx context.Context) ([]*model.AuthorizationV2, error) {
|
||||||
|
var rules []*model.AuthorizationV2
|
||||||
|
err := r.db.Order("id ASC").Find(&rules).Error
|
||||||
|
return rules, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByResourceIds retrieves authorization rules by resource IDs
|
||||||
|
func (r *AuthorizationV2Repository) GetByResourceIds(ctx context.Context, resourceIds []int) ([]*model.AuthorizationV2, error) {
|
||||||
|
if len(resourceIds) == 0 {
|
||||||
|
return []*model.AuthorizationV2{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var rules []*model.AuthorizationV2
|
||||||
|
err := r.db.Where("resource_id IN ? AND enabled = ?", resourceIds, true).
|
||||||
|
Order("id ASC").
|
||||||
|
Find(&rules).Error
|
||||||
|
return rules, err
|
||||||
|
}
|
155
backend/internal/repository/time_template.go
Normal file
155
backend/internal/repository/time_template.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/veops/oneterm/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ITimeTemplateRepository defines the interface for time template data access
|
||||||
|
type ITimeTemplateRepository interface {
|
||||||
|
Create(ctx context.Context, template *model.TimeTemplate) error
|
||||||
|
GetByID(ctx context.Context, id int) (*model.TimeTemplate, error)
|
||||||
|
GetByName(ctx context.Context, name string) (*model.TimeTemplate, error)
|
||||||
|
List(ctx context.Context, offset, limit int, category string, active *bool) ([]*model.TimeTemplate, int64, error)
|
||||||
|
Update(ctx context.Context, template *model.TimeTemplate) error
|
||||||
|
Delete(ctx context.Context, id int) error
|
||||||
|
IncrementUsage(ctx context.Context, id int) error
|
||||||
|
GetBuiltInTemplates(ctx context.Context) ([]*model.TimeTemplate, error)
|
||||||
|
InitBuiltInTemplates(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimeTemplateRepository implements ITimeTemplateRepository
|
||||||
|
type TimeTemplateRepository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTimeTemplateRepository creates a new time template repository
|
||||||
|
func NewTimeTemplateRepository(db *gorm.DB) ITimeTemplateRepository {
|
||||||
|
return &TimeTemplateRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create creates a new time template
|
||||||
|
func (r *TimeTemplateRepository) Create(ctx context.Context, template *model.TimeTemplate) error {
|
||||||
|
return r.db.WithContext(ctx).Create(template).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID retrieves a time template by ID
|
||||||
|
func (r *TimeTemplateRepository) GetByID(ctx context.Context, id int) (*model.TimeTemplate, error) {
|
||||||
|
var template model.TimeTemplate
|
||||||
|
err := r.db.WithContext(ctx).Where("id = ?", id).First(&template).Error
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &template, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByName retrieves a time template by name
|
||||||
|
func (r *TimeTemplateRepository) GetByName(ctx context.Context, name string) (*model.TimeTemplate, error) {
|
||||||
|
var template model.TimeTemplate
|
||||||
|
err := r.db.WithContext(ctx).Where("name = ?", name).First(&template).Error
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &template, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List retrieves time templates with pagination and filters
|
||||||
|
func (r *TimeTemplateRepository) List(ctx context.Context, offset, limit int, category string, active *bool) ([]*model.TimeTemplate, int64, error) {
|
||||||
|
query := r.db.WithContext(ctx).Model(&model.TimeTemplate{})
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if category != "" {
|
||||||
|
query = query.Where("category = ?", category)
|
||||||
|
}
|
||||||
|
if active != nil {
|
||||||
|
query = query.Where("is_active = ?", *active)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total count
|
||||||
|
var total int64
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply pagination and ordering
|
||||||
|
var templates []*model.TimeTemplate
|
||||||
|
err := query.Order("is_builtin DESC, usage_count DESC, created_at DESC").
|
||||||
|
Offset(offset).
|
||||||
|
Limit(limit).
|
||||||
|
Find(&templates).Error
|
||||||
|
|
||||||
|
return templates, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates an existing time template
|
||||||
|
func (r *TimeTemplateRepository) Update(ctx context.Context, template *model.TimeTemplate) error {
|
||||||
|
// Don't allow updating built-in templates
|
||||||
|
if template.IsBuiltIn {
|
||||||
|
return errors.New("cannot update built-in time template")
|
||||||
|
}
|
||||||
|
return r.db.WithContext(ctx).Save(template).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete soft deletes a time template
|
||||||
|
func (r *TimeTemplateRepository) Delete(ctx context.Context, id int) error {
|
||||||
|
// Check if it's a built-in template
|
||||||
|
var template model.TimeTemplate
|
||||||
|
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&template).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if template.IsBuiltIn {
|
||||||
|
return errors.New("cannot delete built-in time template")
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.db.WithContext(ctx).Delete(&model.TimeTemplate{}, id).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementUsage increments the usage count of a time template
|
||||||
|
func (r *TimeTemplateRepository) IncrementUsage(ctx context.Context, id int) error {
|
||||||
|
return r.db.WithContext(ctx).Model(&model.TimeTemplate{}).
|
||||||
|
Where("id = ?", id).
|
||||||
|
UpdateColumn("usage_count", gorm.Expr("usage_count + 1")).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBuiltInTemplates retrieves all built-in time templates
|
||||||
|
func (r *TimeTemplateRepository) GetBuiltInTemplates(ctx context.Context) ([]*model.TimeTemplate, error) {
|
||||||
|
var templates []*model.TimeTemplate
|
||||||
|
err := r.db.WithContext(ctx).Where("is_builtin = ?", true).
|
||||||
|
Order("category, id").
|
||||||
|
Find(&templates).Error
|
||||||
|
return templates, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitBuiltInTemplates initializes built-in time templates
|
||||||
|
func (r *TimeTemplateRepository) InitBuiltInTemplates(ctx context.Context) error {
|
||||||
|
// Check if built-in templates already exist
|
||||||
|
var count int64
|
||||||
|
r.db.WithContext(ctx).Model(&model.TimeTemplate{}).Where("is_builtin = ?", true).Count(&count)
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
// Built-in templates already exist, skip initialization
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create built-in templates
|
||||||
|
for _, template := range model.BuiltInTimeTemplates {
|
||||||
|
// Create a copy to avoid modifying the original
|
||||||
|
newTemplate := template
|
||||||
|
if err := r.db.WithContext(ctx).Create(&newTemplate).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@@ -83,7 +83,8 @@ func getAssetsToCheck(ids ...int) ([]*model.Asset, error) {
|
|||||||
time.Now().Add(-checkInterval).Add(-time.Second*30), false)
|
time.Now().Add(-checkInterval).Add(-time.Second*30), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.Find(&assets).Error; err != nil {
|
// Only select fields needed for connectivity check, exclude authorization to avoid V1/V2 compatibility issues
|
||||||
|
if err := db.Select("id", "name", "ip", "protocols", "gateway_id", "connectable", "updated_at").Find(&assets).Error; err != nil {
|
||||||
logger.L().Error("Failed to get assets for connectivity check", zap.Error(err))
|
logger.L().Error("Failed to get assets for connectivity check", zap.Error(err))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/veops/oneterm/internal/acl"
|
||||||
"github.com/veops/oneterm/internal/model"
|
"github.com/veops/oneterm/internal/model"
|
||||||
"github.com/veops/oneterm/internal/repository"
|
"github.com/veops/oneterm/internal/repository"
|
||||||
"github.com/veops/oneterm/pkg/utils"
|
"github.com/veops/oneterm/pkg/utils"
|
||||||
@@ -78,3 +79,27 @@ func (s *AccountService) FilterByAssetIds(db *gorm.DB, assetIds []int) *gorm.DB
|
|||||||
func (s *AccountService) GetAccountIdsByAuthorization(ctx context.Context, assetIds []int, authorizationIds []int) ([]int, error) {
|
func (s *AccountService) GetAccountIdsByAuthorization(ctx context.Context, assetIds []int, authorizationIds []int) ([]int, error) {
|
||||||
return s.repo.GetAccountIdsByAuthorization(ctx, assetIds, authorizationIds)
|
return s.repo.GetAccountIdsByAuthorization(ctx, assetIds, authorizationIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildQueryWithAuthorization builds query with integrated V2 authorization filter
|
||||||
|
func (s *AccountService) BuildQueryWithAuthorization(ctx *gin.Context) (*gorm.DB, error) {
|
||||||
|
// Start with base query
|
||||||
|
db := s.repo.BuildQuery(ctx)
|
||||||
|
|
||||||
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||||
|
|
||||||
|
// Administrators have access to all accounts
|
||||||
|
if acl.IsAdmin(currentUser) {
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply V2 authorization filter: get authorized asset IDs using V2 system
|
||||||
|
authV2Service := NewAuthorizationV2Service()
|
||||||
|
_, assetIds, _, err := authV2Service.GetAuthorizationScopeByACL(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the same filtering logic as before, but with V2 authorized assets
|
||||||
|
// This maintains the original logic: find accounts that can access the authorized assets
|
||||||
|
return s.FilterByAssetIds(db, assetIds), nil
|
||||||
|
}
|
||||||
|
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
"github.com/veops/oneterm/internal/acl"
|
||||||
"github.com/veops/oneterm/internal/model"
|
"github.com/veops/oneterm/internal/model"
|
||||||
"github.com/veops/oneterm/internal/repository"
|
"github.com/veops/oneterm/internal/repository"
|
||||||
"github.com/veops/oneterm/internal/schedule"
|
"github.com/veops/oneterm/internal/schedule"
|
||||||
@@ -34,7 +35,32 @@ func (s *AssetService) PreprocessAssetData(asset *model.Asset) {
|
|||||||
asset.Ip = strings.TrimSpace(asset.Ip)
|
asset.Ip = strings.TrimSpace(asset.Ip)
|
||||||
asset.Protocols = lo.Map(asset.Protocols, func(s string, _ int) string { return strings.TrimSpace(s) })
|
asset.Protocols = lo.Map(asset.Protocols, func(s string, _ int) string { return strings.TrimSpace(s) })
|
||||||
if asset.Authorization == nil {
|
if asset.Authorization == nil {
|
||||||
asset.Authorization = make(model.Map[int, model.Slice[int]])
|
asset.Authorization = make(model.AuthorizationMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle backward compatibility: convert old format to new format
|
||||||
|
// This handles cases where frontend still sends old format: Map[int, Slice[int]]
|
||||||
|
s.ensureAuthorizationFormat(asset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureAuthorizationFormat ensures asset.Authorization is in the correct V2 format
|
||||||
|
// Handles backward compatibility with old V1 format
|
||||||
|
func (s *AssetService) ensureAuthorizationFormat(asset *model.Asset) {
|
||||||
|
// Check if we need to convert from old format to new format
|
||||||
|
// This is needed for backward compatibility
|
||||||
|
for accountId, authData := range asset.Authorization {
|
||||||
|
// If permissions is nil, set default permissions (connect only for V1 compatibility)
|
||||||
|
if authData.Permissions == nil {
|
||||||
|
authData.Permissions = &model.AuthPermissions{
|
||||||
|
Connect: true, // Default: allow connect (V1 behavior)
|
||||||
|
FileUpload: false, // Default: deny file upload
|
||||||
|
FileDownload: false, // Default: deny file download
|
||||||
|
Copy: false, // Default: deny copy
|
||||||
|
Paste: false, // Default: deny paste
|
||||||
|
Share: false, // Default: deny share
|
||||||
|
}
|
||||||
|
asset.Authorization[accountId] = authData
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,11 +69,6 @@ func (s *AssetService) AttachNodeChain(ctx context.Context, assets []*model.Asse
|
|||||||
return s.repo.AttachNodeChain(ctx, assets)
|
return s.repo.AttachNodeChain(ctx, assets)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyAuthorizationFilters applies authorization filters to assets
|
|
||||||
func (s *AssetService) ApplyAuthorizationFilters(ctx *gin.Context, assets []*model.Asset, authorizationIds []*model.AuthorizationIds, nodeIds, accountIds []int) {
|
|
||||||
s.repo.ApplyAuthorizationFilters(ctx, assets, authorizationIds, nodeIds, accountIds)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildQuery constructs asset query with basic filters
|
// BuildQuery constructs asset query with basic filters
|
||||||
func (s *AssetService) BuildQuery(ctx *gin.Context) (*gorm.DB, error) {
|
func (s *AssetService) BuildQuery(ctx *gin.Context) (*gorm.DB, error) {
|
||||||
return s.repo.BuildQuery(ctx)
|
return s.repo.BuildQuery(ctx)
|
||||||
@@ -58,14 +79,11 @@ func (s *AssetService) FilterByParentId(db *gorm.DB, parentId int) (*gorm.DB, er
|
|||||||
return s.repo.FilterByParentId(db, parentId)
|
return s.repo.FilterByParentId(db, parentId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAssetIdsByAuthorization gets asset IDs by authorization
|
// GetAssetIdsByAuthorization gets asset IDs by authorization using efficient V2 method
|
||||||
func (s *AssetService) GetAssetIdsByAuthorization(ctx *gin.Context) ([]int, []int, []int, error) {
|
func (s *AssetService) GetAssetIdsByAuthorization(ctx *gin.Context) ([]int, []int, []int, error) {
|
||||||
authorizationIds, err := DefaultAuthService.GetAuthorizationIds(ctx)
|
// Use efficient V2 method: get authorized resource IDs from ACL, then find V2 rules
|
||||||
if err != nil {
|
authV2Service := NewAuthorizationV2Service()
|
||||||
return nil, nil, nil, err
|
return authV2Service.GetAuthorizationScopeByACL(ctx)
|
||||||
}
|
|
||||||
|
|
||||||
return s.repo.GetAssetIdsByAuthorization(ctx, authorizationIds)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIdsByAuthorizationIds extracts node IDs, asset IDs, and account IDs from authorization IDs
|
// GetIdsByAuthorizationIds extracts node IDs, asset IDs, and account IDs from authorization IDs
|
||||||
@@ -82,3 +100,36 @@ func (s *AssetService) GetAssetIdsByNodeAccount(ctx context.Context, nodeIds, ac
|
|||||||
func (s *AssetService) UpdateConnectables(ids ...int) error {
|
func (s *AssetService) UpdateConnectables(ids ...int) error {
|
||||||
return schedule.UpdateAssetConnectables(ids...)
|
return schedule.UpdateAssetConnectables(ids...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildQueryWithAuthorization constructs asset query with integrated V2 authorization filter
|
||||||
|
func (s *AssetService) BuildQueryWithAuthorization(ctx *gin.Context) (*gorm.DB, error) {
|
||||||
|
// Start with base query
|
||||||
|
db, err := s.repo.BuildQuery(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||||
|
|
||||||
|
// Administrators have access to all assets
|
||||||
|
if acl.IsAdmin(currentUser) {
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply V2 authorization filter directly at database level
|
||||||
|
authV2Service := NewAuthorizationV2Service()
|
||||||
|
_, assetIds, _, err := authV2Service.GetAuthorizationScopeByACL(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by authorized asset IDs at database level (much more efficient)
|
||||||
|
if len(assetIds) == 0 {
|
||||||
|
// No access to any assets
|
||||||
|
db = db.Where("1 = 0") // Returns empty result set efficiently
|
||||||
|
} else {
|
||||||
|
db = db.Where("id IN ?", assetIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
@@ -3,6 +3,9 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
@@ -31,10 +34,23 @@ var (
|
|||||||
// InitAuthorizationService initializes the global authorization service
|
// InitAuthorizationService initializes the global authorization service
|
||||||
func InitAuthorizationService() {
|
func InitAuthorizationService() {
|
||||||
repo := repository.NewAuthorizationRepository(dbpkg.DB)
|
repo := repository.NewAuthorizationRepository(dbpkg.DB)
|
||||||
DefaultAuthService = NewAuthorizationService(repo, dbpkg.DB)
|
v2Repo := repository.NewAuthorizationV2Repository(dbpkg.DB)
|
||||||
|
matcher := NewAuthorizationMatcher(v2Repo)
|
||||||
|
|
||||||
|
// Perform V1 to V2 migration if needed
|
||||||
|
migrationService := NewAuthorizationMigrationService(dbpkg.DB, repo, v2Repo)
|
||||||
|
ctx := context.Background()
|
||||||
|
if err := migrationService.MigrateV1ToV2(ctx); err != nil {
|
||||||
|
logger.L().Error("Failed to migrate V1 authorization rules to V2", zap.Error(err))
|
||||||
|
// Continue with service initialization even if migration fails
|
||||||
|
// This allows the system to start with existing V2 rules
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultAuthService = NewAuthorizationService(repo, dbpkg.DB, matcher) // Use V2 by default
|
||||||
}
|
}
|
||||||
|
|
||||||
type IAuthorizationService interface {
|
type IAuthorizationService interface {
|
||||||
|
// V1 methods
|
||||||
UpsertAuthorization(ctx context.Context, auth *model.Authorization) error
|
UpsertAuthorization(ctx context.Context, auth *model.Authorization) error
|
||||||
UpsertAuthorizationWithTx(ctx context.Context, auth *model.Authorization) error
|
UpsertAuthorizationWithTx(ctx context.Context, auth *model.Authorization) error
|
||||||
DeleteAuthorization(ctx context.Context, auth *model.Authorization) error
|
DeleteAuthorization(ctx context.Context, auth *model.Authorization) error
|
||||||
@@ -46,16 +62,22 @@ type IAuthorizationService interface {
|
|||||||
HandleAuthorization(ctx context.Context, tx *gorm.DB, action int, asset *model.Asset, auths ...*model.Authorization) error
|
HandleAuthorization(ctx context.Context, tx *gorm.DB, action int, asset *model.Asset, auths ...*model.Authorization) error
|
||||||
GetNodeAssetAccountIdsByAction(ctx context.Context, action string) (nodeIds, assetIds, accountIds []int, err error)
|
GetNodeAssetAccountIdsByAction(ctx context.Context, action string) (nodeIds, assetIds, accountIds []int, err error)
|
||||||
GetAuthorizationIds(ctx *gin.Context) ([]*model.AuthorizationIds, error)
|
GetAuthorizationIds(ctx *gin.Context) ([]*model.AuthorizationIds, error)
|
||||||
|
|
||||||
|
// V2 methods
|
||||||
|
HasAuthorizationV2(ctx *gin.Context, sess *gsession.Session, actions ...model.AuthAction) (*model.BatchAuthResult, error)
|
||||||
|
CheckPermission(ctx *gin.Context, nodeId, assetId, accountId int, action model.AuthAction) (*model.AuthResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthorizationService struct {
|
type AuthorizationService struct {
|
||||||
repo repository.IAuthorizationRepository
|
repo repository.IAuthorizationRepository
|
||||||
db *gorm.DB // Add database field for transaction processing
|
matcher IAuthorizationMatcher
|
||||||
|
db *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthorizationService(repo repository.IAuthorizationRepository, db *gorm.DB) IAuthorizationService {
|
func NewAuthorizationService(repo repository.IAuthorizationRepository, db *gorm.DB, matcher IAuthorizationMatcher) IAuthorizationService {
|
||||||
return &AuthorizationService{
|
return &AuthorizationService{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
|
matcher: matcher,
|
||||||
db: db,
|
db: db,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,7 +108,7 @@ func (s *AuthorizationService) UpsertAuthorizationWithTx(ctx context.Context, au
|
|||||||
action := lo.Ternary(auth.Id > 0, model.ACTION_UPDATE, model.ACTION_CREATE)
|
action := lo.Ternary(auth.Id > 0, model.ACTION_UPDATE, model.ACTION_CREATE)
|
||||||
|
|
||||||
// Create a temporary Service for transaction handling
|
// Create a temporary Service for transaction handling
|
||||||
txService := &AuthorizationService{repo: txRepo, db: s.db}
|
txService := &AuthorizationService{repo: txRepo, db: s.db, matcher: s.matcher}
|
||||||
|
|
||||||
return txService.HandleAuthorization(ctx, tx, action, nil, auth)
|
return txService.HandleAuthorization(ctx, tx, action, nil, auth)
|
||||||
})
|
})
|
||||||
@@ -100,7 +122,7 @@ func (s *AuthorizationService) GetAuthorizationById(ctx context.Context, id int)
|
|||||||
func (s *AuthorizationService) DeleteAuthorization(ctx context.Context, auth *model.Authorization) error {
|
func (s *AuthorizationService) DeleteAuthorization(ctx context.Context, auth *model.Authorization) error {
|
||||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||||
txRepo := repository.NewAuthorizationRepository(tx)
|
txRepo := repository.NewAuthorizationRepository(tx)
|
||||||
txService := &AuthorizationService{repo: txRepo, db: s.db}
|
txService := &AuthorizationService{repo: txRepo, db: s.db, matcher: s.matcher}
|
||||||
return txService.HandleAuthorization(ctx, tx, model.ACTION_DELETE, nil, auth)
|
return txService.HandleAuthorization(ctx, tx, model.ACTION_DELETE, nil, auth)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -205,6 +227,7 @@ func (s *AuthorizationService) GetAuthsByAsset(ctx context.Context, asset *model
|
|||||||
return auths, err
|
return auths, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleAuthorization handles authorization operations
|
||||||
func (s *AuthorizationService) HandleAuthorization(ctx context.Context, tx *gorm.DB, action int, asset *model.Asset, auths ...*model.Authorization) (err error) {
|
func (s *AuthorizationService) HandleAuthorization(ctx context.Context, tx *gorm.DB, action int, asset *model.Asset, auths ...*model.Authorization) (err error) {
|
||||||
defer repository.DeleteAllFromCacheDb(ctx, model.DefaultAuthorization)
|
defer repository.DeleteAllFromCacheDb(ctx, model.DefaultAuthorization)
|
||||||
|
|
||||||
@@ -213,46 +236,29 @@ func (s *AuthorizationService) HandleAuthorization(ctx context.Context, tx *gorm
|
|||||||
eg := &errgroup.Group{}
|
eg := &errgroup.Group{}
|
||||||
|
|
||||||
if asset != nil && asset.Id > 0 {
|
if asset != nil && asset.Id > 0 {
|
||||||
var pres []*model.Authorization
|
|
||||||
pres, err = s.GetAuthsByAsset(ctx, asset)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch action {
|
switch action {
|
||||||
case model.ACTION_CREATE:
|
case model.ACTION_CREATE:
|
||||||
auths = lo.Map(lo.Keys(asset.Authorization), func(id int, _ int) *model.Authorization {
|
// V2: Create authorization rules instead of V1 authorization records
|
||||||
return &model.Authorization{AssetId: asset.Id, AccountId: id, Rids: asset.Authorization[id]}
|
err = s.createV2AuthorizationRulesForAsset(ctx, tx, asset, currentUser)
|
||||||
})
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
case model.ACTION_DELETE:
|
case model.ACTION_DELETE:
|
||||||
auths = pres
|
// V2: Delete authorization rules for this asset
|
||||||
|
err = s.deleteV2AuthorizationRulesForAsset(ctx, tx, asset)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
case model.ACTION_UPDATE:
|
case model.ACTION_UPDATE:
|
||||||
for _, pre := range pres {
|
// V2: Update authorization rules for this asset
|
||||||
p := pre
|
err = s.updateV2AuthorizationRulesForAsset(ctx, tx, asset, currentUser)
|
||||||
if v, ok := asset.Authorization[p.AccountId]; ok {
|
if err != nil {
|
||||||
p.Rids = v
|
return err
|
||||||
auths = append(auths, p)
|
|
||||||
} else {
|
|
||||||
eg.Go(func() (err error) {
|
|
||||||
if err = acl.DeleteResource(ctx, currentUser.GetUid(), p.ResourceId); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = tx.Delete(p).Error; err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
preAccountsIds := lo.Map(pres, func(p *model.Authorization, _ int) int { return p.AccountId })
|
|
||||||
for k, v := range asset.Authorization {
|
|
||||||
if !lo.Contains(preAccountsIds, k) {
|
|
||||||
auths = append(auths, &model.Authorization{AssetId: asset.Id, AccountId: k, Rids: v})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle individual authorization records (V1 compatibility)
|
||||||
for _, a := 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
|
auth := a
|
||||||
switch action {
|
switch action {
|
||||||
@@ -344,56 +350,237 @@ func (s *AuthorizationService) GetAuthorizationIds(ctx *gin.Context) (authIds []
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasAuthorization checks if the current user has permission to connect to the specified asset with the given account.
|
||||||
func (s *AuthorizationService) HasAuthorization(ctx *gin.Context, sess *gsession.Session) (ok bool, err error) {
|
func (s *AuthorizationService) HasAuthorization(ctx *gin.Context, sess *gsession.Session) (ok bool, err error) {
|
||||||
|
result, err := s.HasAuthorizationV2(ctx, sess, model.ActionConnect)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
// Check if connect action is allowed in the batch result
|
||||||
|
return result.IsAllowed(model.ActionConnect), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasAuthorizationV2 implements the new V2 authorization logic
|
||||||
|
func (s *AuthorizationService) HasAuthorizationV2(ctx *gin.Context, sess *gsession.Session, actions ...model.AuthAction) (*model.BatchAuthResult, error) {
|
||||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||||
|
|
||||||
|
// Helper function to create batch result for all actions
|
||||||
|
createBatchResult := func(allowed bool, reason string) *model.BatchAuthResult {
|
||||||
|
results := make(map[model.AuthAction]*model.AuthResult)
|
||||||
|
for _, action := range actions {
|
||||||
|
results[action] = &model.AuthResult{
|
||||||
|
Allowed: allowed,
|
||||||
|
Reason: reason,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &model.BatchAuthResult{
|
||||||
|
Results: results,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Share sessions are always allowed
|
||||||
if sess.ShareId != 0 {
|
if sess.ShareId != 0 {
|
||||||
return true, nil
|
return createBatchResult(true, "Share session"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok = acl.IsAdmin(currentUser); ok {
|
// 2. Administrators have access to all resources
|
||||||
return
|
if acl.IsAdmin(currentUser) {
|
||||||
|
return createBatchResult(true, "Administrator access"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. Get user's authorized V2 rule IDs from ACL (like V1's AuthorizationIds)
|
||||||
|
authV2ResourceIds, err := s.getAuthorizedV2ResourceIds(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return createBatchResult(false, "Failed to get authorized rules"), err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(authV2ResourceIds) == 0 {
|
||||||
|
return createBatchResult(false, "No authorization rules available"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load asset if not already loaded
|
||||||
if sess.Session.Asset == nil {
|
if sess.Session.Asset == nil {
|
||||||
if err := s.db.Model(sess.Session.Asset).Where("id=?", sess.AssetId).First(&sess.Session.Asset).Error; err != nil {
|
if err := s.db.Model(sess.Session.Asset).Where("id=?", sess.AssetId).First(&sess.Session.Asset).Error; err != nil {
|
||||||
return false, err
|
return createBatchResult(false, "Asset not found"), err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authIds, err := s.GetAuthorizationIds(ctx)
|
// Create base authorization request (without action, will be added per action)
|
||||||
if err != nil {
|
clientIP := s.getClientIP(ctx)
|
||||||
return false, err
|
baseReq := &model.BatchAuthRequest{
|
||||||
}
|
UserId: currentUser.GetUid(),
|
||||||
if ok = lo.ContainsBy(authIds, func(item *model.AuthorizationIds) bool {
|
NodeId: sess.Session.Asset.ParentId,
|
||||||
return item.NodeId == 0 && item.AssetId == sess.AssetId && item.AccountId == sess.AccountId
|
AssetId: sess.AssetId,
|
||||||
}); ok {
|
AccountId: sess.AccountId,
|
||||||
return true, nil
|
Actions: actions,
|
||||||
}
|
ClientIP: clientIP,
|
||||||
ctx.Set(kAuthorizationIds, authIds)
|
Timestamp: time.Now(),
|
||||||
|
|
||||||
authorizationIds, ok := ctx.Value(kAuthorizationIds).([]*model.AuthorizationIds)
|
|
||||||
if !ok || len(authorizationIds) == 0 {
|
|
||||||
return false, errors.New("authorizationIds not found")
|
|
||||||
}
|
|
||||||
assetService := NewAssetService()
|
|
||||||
nodeIds, assetIds, accountIds := assetService.GetIdsByAuthorizationIds(ctx, authorizationIds)
|
|
||||||
tmp, err := repository.HandleSelfChild(ctx, nodeIds...)
|
|
||||||
if err != nil {
|
|
||||||
logger.L().Error("", zap.Error(err))
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
nodeIds = append(nodeIds, tmp...)
|
|
||||||
if ok = lo.Contains(nodeIds, sess.Session.Asset.ParentId) || lo.Contains(assetIds, sess.AssetId) || lo.Contains(accountIds, sess.AccountId); ok {
|
|
||||||
return true, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ids, err := assetService.GetAssetIdsByNodeAccount(ctx, nodeIds, accountIds)
|
// Use V2 matcher with filtered rule scope
|
||||||
if err != nil {
|
return s.matcher.MatchBatchWithScope(ctx, baseReq, authV2ResourceIds)
|
||||||
logger.L().Error("", zap.Error(err))
|
}
|
||||||
return false, err
|
|
||||||
}
|
// getAuthorizedV2ResourceIds gets V2 authorization rule resource IDs that user has permission to (like V1's AuthorizationIds)
|
||||||
|
func (s *AuthorizationService) getAuthorizedV2ResourceIds(ctx *gin.Context) ([]int, error) {
|
||||||
return lo.Contains(ids, sess.AssetId), nil
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||||
|
|
||||||
|
// Get ACL resources for authorization_v2 that this user's role has access to
|
||||||
|
res, err := acl.GetRoleResources(ctx, currentUser.GetRid(), config.RESOURCE_AUTHORIZATION)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract resource IDs (these are the V2 rule IDs user can access)
|
||||||
|
resourceIds := lo.Map(res, func(r *acl.Resource, _ int) int { return r.ResourceId })
|
||||||
|
return resourceIds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckPermission checks permission for specific node/asset/account combination
|
||||||
|
func (s *AuthorizationService) CheckPermission(ctx *gin.Context, nodeId, assetId, accountId int, action model.AuthAction) (*model.AuthResult, error) {
|
||||||
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||||
|
|
||||||
|
// Administrators have access to all resources
|
||||||
|
if acl.IsAdmin(currentUser) {
|
||||||
|
return &model.AuthResult{
|
||||||
|
Allowed: true,
|
||||||
|
Reason: "Administrator access",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create authorization request
|
||||||
|
clientIP := s.getClientIP(ctx)
|
||||||
|
req := &model.AuthRequest{
|
||||||
|
UserId: currentUser.GetUid(),
|
||||||
|
NodeId: nodeId,
|
||||||
|
AssetId: assetId,
|
||||||
|
AccountId: accountId,
|
||||||
|
Action: action,
|
||||||
|
ClientIP: clientIP,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use V2 matcher
|
||||||
|
return s.matcher.Match(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getClientIP extracts client IP from gin context
|
||||||
|
func (s *AuthorizationService) getClientIP(ctx *gin.Context) string {
|
||||||
|
// Try to get real IP from headers first
|
||||||
|
clientIP := ctx.GetHeader("X-Forwarded-For")
|
||||||
|
if clientIP == "" {
|
||||||
|
clientIP = ctx.GetHeader("X-Real-IP")
|
||||||
|
}
|
||||||
|
if clientIP == "" {
|
||||||
|
clientIP = ctx.ClientIP()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and validate IP
|
||||||
|
if ip := net.ParseIP(clientIP); ip != nil {
|
||||||
|
return clientIP
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// createV2AuthorizationRulesForAsset creates V2 authorization rules for an asset
|
||||||
|
func (s *AuthorizationService) createV2AuthorizationRulesForAsset(ctx context.Context, tx *gorm.DB, asset *model.Asset, currentUser *acl.Session) error {
|
||||||
|
if len(asset.Authorization) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for accountId, authData := range asset.Authorization {
|
||||||
|
// Create a V2 authorization rule for this asset-account combination
|
||||||
|
rule := &model.AuthorizationV2{
|
||||||
|
Name: fmt.Sprintf("Asset-%d-Account-%d", asset.Id, accountId),
|
||||||
|
Description: fmt.Sprintf("Auto-generated rule for asset %s and account %d", asset.Name, accountId),
|
||||||
|
Enabled: true,
|
||||||
|
|
||||||
|
// Target selectors - specific asset and account
|
||||||
|
AssetSelector: model.TargetSelector{
|
||||||
|
Type: model.SelectorTypeIds,
|
||||||
|
Values: []string{fmt.Sprintf("%d", asset.Id)},
|
||||||
|
ExcludeIds: []int{},
|
||||||
|
},
|
||||||
|
AccountSelector: model.TargetSelector{
|
||||||
|
Type: model.SelectorTypeIds,
|
||||||
|
Values: []string{fmt.Sprintf("%d", accountId)},
|
||||||
|
ExcludeIds: []int{},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Use permissions from asset.Authorization
|
||||||
|
Permissions: *authData.Permissions,
|
||||||
|
|
||||||
|
// Role IDs for ACL integration
|
||||||
|
Rids: authData.Rids,
|
||||||
|
|
||||||
|
// Standard fields
|
||||||
|
CreatorId: currentUser.GetUid(),
|
||||||
|
UpdaterId: currentUser.GetUid(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ACL resource for this rule
|
||||||
|
resourceId, err := acl.CreateAcl(ctx, currentUser, config.RESOURCE_AUTHORIZATION, rule.Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create ACL resource: %w", err)
|
||||||
|
}
|
||||||
|
rule.ResourceId = resourceId
|
||||||
|
|
||||||
|
// Create the V2 rule
|
||||||
|
if err := tx.Create(rule).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to create V2 authorization rule: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grant permissions to roles
|
||||||
|
if len(authData.Rids) > 0 {
|
||||||
|
if err := acl.BatchGrantRoleResource(ctx, currentUser.GetUid(), authData.Rids, resourceId, []string{acl.READ}); err != nil {
|
||||||
|
return fmt.Errorf("failed to grant role permissions: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteV2AuthorizationRulesForAsset deletes V2 authorization rules for an asset
|
||||||
|
func (s *AuthorizationService) deleteV2AuthorizationRulesForAsset(ctx context.Context, tx *gorm.DB, asset *model.Asset) error {
|
||||||
|
// Find all V2 rules that target this specific asset
|
||||||
|
var rules []*model.AuthorizationV2
|
||||||
|
if err := tx.Where("asset_selector->>'$.values' LIKE ?", fmt.Sprintf("%%\"%d\"%%", asset.Id)).Find(&rules).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to find V2 rules for asset: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete each rule and its ACL resource
|
||||||
|
for _, rule := range rules {
|
||||||
|
// Delete ACL resource
|
||||||
|
if err := acl.DeleteResource(ctx, 0, rule.ResourceId); err != nil {
|
||||||
|
logger.L().Error("Failed to delete ACL resource", zap.Int("resourceId", rule.ResourceId), zap.Error(err))
|
||||||
|
// Continue with database deletion even if ACL deletion fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the rule from database
|
||||||
|
if err := tx.Delete(rule).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to delete V2 authorization rule: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateV2AuthorizationRulesForAsset updates V2 authorization rules for an asset
|
||||||
|
func (s *AuthorizationService) updateV2AuthorizationRulesForAsset(ctx context.Context, tx *gorm.DB, asset *model.Asset, currentUser *acl.Session) error {
|
||||||
|
// For simplicity, we'll delete existing rules and create new ones
|
||||||
|
// This ensures consistency and handles complex permission changes
|
||||||
|
|
||||||
|
// First, delete existing rules for this asset
|
||||||
|
if err := s.deleteV2AuthorizationRulesForAsset(ctx, tx, asset); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete existing V2 rules: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then create new rules based on current asset.Authorization
|
||||||
|
if err := s.createV2AuthorizationRulesForAsset(ctx, tx, asset, currentUser); err != nil {
|
||||||
|
return fmt.Errorf("failed to create new V2 rules: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
637
backend/internal/service/authorization_matcher.go
Normal file
637
backend/internal/service/authorization_matcher.go
Normal file
@@ -0,0 +1,637 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
|
||||||
|
"github.com/veops/oneterm/internal/acl"
|
||||||
|
"github.com/veops/oneterm/internal/model"
|
||||||
|
"github.com/veops/oneterm/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IAuthorizationMatcher defines the interface for authorization matching
|
||||||
|
type IAuthorizationMatcher interface {
|
||||||
|
// Match checks if a request is authorized using all available rules
|
||||||
|
Match(ctx *gin.Context, req *model.AuthRequest) (*model.AuthResult, error)
|
||||||
|
|
||||||
|
// MatchWithScope checks if a request is authorized using only specified rule IDs
|
||||||
|
MatchWithScope(ctx *gin.Context, req *model.AuthRequest, ruleIds []int) (*model.AuthResult, error)
|
||||||
|
|
||||||
|
// MatchBatchWithScope checks if batch requests are authorized using only specified rule IDs
|
||||||
|
MatchBatchWithScope(ctx *gin.Context, req *model.BatchAuthRequest, ruleIds []int) (*model.BatchAuthResult, error)
|
||||||
|
|
||||||
|
GetTargetName(targetType string, targetId int) (string, error)
|
||||||
|
GetTargetTags(targetType string, targetId int) ([]string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizationMatcher implements IAuthorizationMatcher
|
||||||
|
type AuthorizationMatcher struct {
|
||||||
|
repo repository.IAuthorizationV2Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthorizationMatcher creates a new authorization matcher
|
||||||
|
func NewAuthorizationMatcher(repo repository.IAuthorizationV2Repository) IAuthorizationMatcher {
|
||||||
|
return &AuthorizationMatcher{
|
||||||
|
repo: repo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match performs authorization matching against rules
|
||||||
|
func (m *AuthorizationMatcher) Match(ctx *gin.Context, req *model.AuthRequest) (*model.AuthResult, error) {
|
||||||
|
// Get cache key for this request
|
||||||
|
cacheKey := m.getCacheKey(req)
|
||||||
|
|
||||||
|
// Try to get result from cache first
|
||||||
|
if cached := m.getCachedResult(cacheKey); cached != nil {
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user's role IDs from context (assuming it's passed through ctx)
|
||||||
|
userRids := m.getUserRoleIds(ctx, req.UserId)
|
||||||
|
|
||||||
|
// Get user's authorization rules
|
||||||
|
rules, err := m.repo.GetUserRules(ctx, userRids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get user rules: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check all matching rules - if any rule allows the action, grant permission
|
||||||
|
var matchedRules []*model.AuthorizationV2
|
||||||
|
for _, rule := range rules {
|
||||||
|
if m.matchRule(ctx, rule, req) {
|
||||||
|
matchedRules = append(matchedRules, rule)
|
||||||
|
// If this rule allows the requested action, grant permission immediately
|
||||||
|
if rule.Permissions.HasPermission(req.Action) {
|
||||||
|
result := &model.AuthResult{
|
||||||
|
Allowed: true,
|
||||||
|
Permissions: rule.Permissions,
|
||||||
|
Reason: fmt.Sprintf("Allowed by rule: %s", rule.Name),
|
||||||
|
RuleId: rule.Id,
|
||||||
|
RuleName: rule.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
m.cacheResult(cacheKey, result)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found matching rules but none allowed the action, deny with specific reason
|
||||||
|
if len(matchedRules) > 0 {
|
||||||
|
result := &model.AuthResult{
|
||||||
|
Allowed: false,
|
||||||
|
Reason: fmt.Sprintf("Action '%s' denied by %d matching rule(s)", req.Action, len(matchedRules)),
|
||||||
|
}
|
||||||
|
m.cacheResult(cacheKey, result)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// No matching rule found
|
||||||
|
result := &model.AuthResult{
|
||||||
|
Allowed: false,
|
||||||
|
Reason: "No matching authorization rule found",
|
||||||
|
}
|
||||||
|
|
||||||
|
m.cacheResult(cacheKey, result)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUserRoleIds extracts user role IDs from context or fetches them
|
||||||
|
func (m *AuthorizationMatcher) getUserRoleIds(ctx context.Context, userId int) []int {
|
||||||
|
// Try to get from gin context if available
|
||||||
|
if ginCtx, ok := ctx.(*gin.Context); ok {
|
||||||
|
if currentUser, err := acl.GetSessionFromCtx(ginCtx); err == nil {
|
||||||
|
return []int{currentUser.GetRid()}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try to get role from ACL service
|
||||||
|
// This is a simplified approach - in production you might want to implement
|
||||||
|
// a more robust user role resolution mechanism
|
||||||
|
return []int{} // Return empty for now - should be implemented based on your ACL system
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchRule checks if a rule matches the request
|
||||||
|
func (m *AuthorizationMatcher) matchRule(ctx context.Context, rule *model.AuthorizationV2, req *model.AuthRequest) bool {
|
||||||
|
// First check if the rule is currently valid (enabled and within validity period)
|
||||||
|
if !rule.IsValid(req.Timestamp) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check node selector
|
||||||
|
if !m.matchSelector(ctx, rule.NodeSelector, "node", req.NodeId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check asset selector
|
||||||
|
if !m.matchSelector(ctx, rule.AssetSelector, "asset", req.AssetId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check account selector
|
||||||
|
if !m.matchSelector(ctx, rule.AccountSelector, "account", req.AccountId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check access control restrictions
|
||||||
|
if !m.checkAccessControl(rule.AccessControl, req) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchSelector checks if a target selector matches the given target
|
||||||
|
func (m *AuthorizationMatcher) matchSelector(ctx context.Context, selector model.TargetSelector, targetType string, targetId int) bool {
|
||||||
|
// Handle zero ID based on selector type
|
||||||
|
if targetId == 0 {
|
||||||
|
return selector.Type == model.SelectorTypeAll
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if target is in exclude list
|
||||||
|
if lo.Contains(selector.ExcludeIds, targetId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch selector.Type {
|
||||||
|
case model.SelectorTypeAll:
|
||||||
|
return true
|
||||||
|
|
||||||
|
case model.SelectorTypeIds:
|
||||||
|
targetIds := lo.Map(selector.Values, func(v string, _ int) int {
|
||||||
|
return cast.ToInt(v)
|
||||||
|
})
|
||||||
|
return lo.Contains(targetIds, targetId)
|
||||||
|
|
||||||
|
case model.SelectorTypeRegex:
|
||||||
|
targetName, err := m.GetTargetName(targetType, targetId)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return m.matchRegexPatterns(selector.Values, targetName)
|
||||||
|
|
||||||
|
case model.SelectorTypeTags:
|
||||||
|
targetTags, err := m.GetTargetTags(targetType, targetId)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return len(lo.Intersect(selector.Values, targetTags)) > 0
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchRegexPatterns checks if any regex pattern matches the target name
|
||||||
|
func (m *AuthorizationMatcher) matchRegexPatterns(patterns []string, targetName string) bool {
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
if matched, err := regexp.MatchString(pattern, targetName); err == nil && matched {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAccessControl validates access control restrictions
|
||||||
|
func (m *AuthorizationMatcher) checkAccessControl(accessControl model.AccessControl, req *model.AuthRequest) bool {
|
||||||
|
// Check IP whitelist
|
||||||
|
if len(accessControl.IPWhitelist) > 0 && !m.checkIPWhitelist(accessControl.IPWhitelist, req.ClientIP) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get asset for asset-level restrictions
|
||||||
|
asset, err := m.getAssetById(req.AssetId)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check time restrictions with time template support
|
||||||
|
if !m.checkUpdatedTimeRestrictions(asset.AccessTimeControl, &accessControl, req.Timestamp) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Check max sessions and session timeout (requires session management)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkUpdatedTimeRestrictions implements time restriction logic with template support
|
||||||
|
func (m *AuthorizationMatcher) checkUpdatedTimeRestrictions(assetTimeControl *model.AccessTimeControl, accessControl *model.AccessControl, timestamp time.Time) bool {
|
||||||
|
if timestamp.IsZero() {
|
||||||
|
timestamp = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Check asset-level V2 time restrictions (base constraint)
|
||||||
|
if assetTimeControl != nil && assetTimeControl.Enabled {
|
||||||
|
if !m.checkAssetTimeRanges(assetTimeControl, timestamp) {
|
||||||
|
return false // Asset-level restriction failed, deny access
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check authorization rule's time template restrictions if configured
|
||||||
|
if accessControl.TimeTemplate != nil {
|
||||||
|
if !m.checkTimeTemplateAccess(accessControl.TimeTemplate, accessControl.Timezone, timestamp) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check authorization rule's custom time ranges if configured
|
||||||
|
if len(accessControl.CustomTimeRanges) > 0 {
|
||||||
|
if !m.checkTimeRanges(accessControl.CustomTimeRanges, timestamp) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. If no restrictions are configured or all pass, allow access
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkTimeTemplateAccess checks if current time is within template's allowed ranges
|
||||||
|
func (m *AuthorizationMatcher) checkTimeTemplateAccess(templateRef *model.TimeTemplateReference, timezone string, timestamp time.Time) bool {
|
||||||
|
// Get the time template service for validation
|
||||||
|
timeTemplateService := NewTimeTemplateService()
|
||||||
|
|
||||||
|
// Get the template
|
||||||
|
ctx := context.Background()
|
||||||
|
template, err := timeTemplateService.GetTimeTemplate(ctx, templateRef.TemplateId)
|
||||||
|
if err != nil || template == nil {
|
||||||
|
// If template cannot be found, deny access for safety
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the specified timezone or template's timezone
|
||||||
|
checkTimezone := timezone
|
||||||
|
if checkTimezone == "" {
|
||||||
|
checkTimezone = template.Timezone
|
||||||
|
}
|
||||||
|
if checkTimezone == "" {
|
||||||
|
checkTimezone = "Asia/Shanghai" // Default timezone
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if current time is within template ranges
|
||||||
|
if m.isTimeInTemplateRanges(template.TimeRanges, checkTimezone, timestamp) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check custom ranges in the template reference
|
||||||
|
if len(templateRef.CustomRanges) > 0 {
|
||||||
|
if m.isTimeInTemplateRanges(templateRef.CustomRanges, checkTimezone, timestamp) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTimeInTemplateRanges checks if timestamp is within any of the template time ranges
|
||||||
|
func (m *AuthorizationMatcher) isTimeInTemplateRanges(ranges model.TimeRanges, timezone string, timestamp time.Time) bool {
|
||||||
|
// Load timezone location
|
||||||
|
loc, err := time.LoadLocation(timezone)
|
||||||
|
if err != nil {
|
||||||
|
// Fall back to UTC if timezone is invalid
|
||||||
|
loc = time.UTC
|
||||||
|
}
|
||||||
|
|
||||||
|
targetTime := timestamp.In(loc)
|
||||||
|
currentWeekday := int(targetTime.Weekday())
|
||||||
|
if currentWeekday == 0 {
|
||||||
|
currentWeekday = 7 // Convert Sunday from 0 to 7
|
||||||
|
}
|
||||||
|
|
||||||
|
timeStr := targetTime.Format("15:04")
|
||||||
|
|
||||||
|
// Check each time range
|
||||||
|
for _, timeRange := range ranges {
|
||||||
|
// Check if current weekday is in the allowed weekdays
|
||||||
|
weekdayMatch := false
|
||||||
|
for _, day := range timeRange.Weekdays {
|
||||||
|
if day == currentWeekday {
|
||||||
|
weekdayMatch = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !weekdayMatch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if current time is in the allowed time range
|
||||||
|
if m.isTimeInRange(timeStr, timeRange.StartTime, timeRange.EndTime) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAssetTimeRanges validates asset-level time restrictions with timezone support
|
||||||
|
func (m *AuthorizationMatcher) checkAssetTimeRanges(assetTimeControl *model.AccessTimeControl, timestamp time.Time) bool {
|
||||||
|
if len(assetTimeControl.TimeRanges) == 0 {
|
||||||
|
return true // No time ranges specified, allow access
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle timezone conversion
|
||||||
|
targetTime := timestamp
|
||||||
|
if assetTimeControl.Timezone != "" {
|
||||||
|
if loc, err := time.LoadLocation(assetTimeControl.Timezone); err == nil {
|
||||||
|
targetTime = timestamp.In(loc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
weekday := int(targetTime.Weekday())
|
||||||
|
if weekday == 0 {
|
||||||
|
weekday = 7 // Convert Sunday from 0 to 7
|
||||||
|
}
|
||||||
|
|
||||||
|
timeStr := targetTime.Format("15:04")
|
||||||
|
|
||||||
|
for _, tr := range assetTimeControl.TimeRanges {
|
||||||
|
// Check if current weekday is allowed
|
||||||
|
if len(tr.Weekdays) > 0 && !lo.Contains(tr.Weekdays, weekday) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if current time is within allowed range
|
||||||
|
if tr.StartTime != "" && tr.EndTime != "" {
|
||||||
|
if m.isTimeInRange(timeStr, tr.StartTime, tr.EndTime) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true // No time restriction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAssetById retrieves asset by ID (with caching)
|
||||||
|
func (m *AuthorizationMatcher) getAssetById(assetId int) (*model.Asset, error) {
|
||||||
|
assets, err := repository.GetAllFromCacheDb(context.Background(), model.DefaultAsset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, asset := range assets {
|
||||||
|
if asset.Id == assetId {
|
||||||
|
return asset, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("asset not found: %d", assetId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkIPWhitelist validates if client IP is in whitelist
|
||||||
|
func (m *AuthorizationMatcher) checkIPWhitelist(whitelist []string, clientIP string) bool {
|
||||||
|
if clientIP == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
clientIPNet := net.ParseIP(clientIP)
|
||||||
|
if clientIPNet == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ipRange := range whitelist {
|
||||||
|
if strings.Contains(ipRange, "/") {
|
||||||
|
// CIDR notation
|
||||||
|
_, cidr, err := net.ParseCIDR(ipRange)
|
||||||
|
if err == nil && cidr.Contains(clientIPNet) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Single IP
|
||||||
|
if ipRange == clientIP {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkTimeRanges validates if current time is within allowed ranges
|
||||||
|
func (m *AuthorizationMatcher) checkTimeRanges(timeRanges model.TimeRanges, timestamp time.Time) bool {
|
||||||
|
if timestamp.IsZero() {
|
||||||
|
timestamp = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
weekday := int(timestamp.Weekday())
|
||||||
|
if weekday == 0 {
|
||||||
|
weekday = 7 // Convert Sunday from 0 to 7
|
||||||
|
}
|
||||||
|
|
||||||
|
timeStr := timestamp.Format("15:04")
|
||||||
|
|
||||||
|
for _, tr := range timeRanges {
|
||||||
|
// Check if current weekday is allowed
|
||||||
|
if len(tr.Weekdays) > 0 && !lo.Contains(tr.Weekdays, weekday) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if current time is within allowed range
|
||||||
|
if tr.StartTime != "" && tr.EndTime != "" {
|
||||||
|
if m.isTimeInRange(timeStr, tr.StartTime, tr.EndTime) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true // No time restriction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(timeRanges) == 0 // Allow if no time ranges specified
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTimeInRange checks if time is within the specified range
|
||||||
|
func (m *AuthorizationMatcher) isTimeInRange(timeStr, startTime, endTime string) bool {
|
||||||
|
return timeStr >= startTime && timeStr <= endTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTargetName retrieves the name of a target by type and ID
|
||||||
|
func (m *AuthorizationMatcher) GetTargetName(targetType string, targetId int) (string, error) {
|
||||||
|
switch targetType {
|
||||||
|
case "node":
|
||||||
|
nodes, err := repository.GetAllFromCacheDb(context.Background(), model.DefaultNode)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
for _, node := range nodes {
|
||||||
|
if node.Id == targetId {
|
||||||
|
return node.Name, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("node not found: %d", targetId)
|
||||||
|
|
||||||
|
case "asset":
|
||||||
|
assets, err := repository.GetAllFromCacheDb(context.Background(), model.DefaultAsset)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
for _, asset := range assets {
|
||||||
|
if asset.Id == targetId {
|
||||||
|
return asset.Name, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("asset not found: %d", targetId)
|
||||||
|
|
||||||
|
case "account":
|
||||||
|
accounts, err := repository.GetAllFromCacheDb(context.Background(), model.DefaultAccount)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
for _, account := range accounts {
|
||||||
|
if account.Id == targetId {
|
||||||
|
return account.Name, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("account not found: %d", targetId)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unknown target type: %s", targetType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTargetTags retrieves tags for a target (placeholder implementation)
|
||||||
|
func (m *AuthorizationMatcher) GetTargetTags(targetType string, targetId int) ([]string, error) {
|
||||||
|
// TODO: Implement tag system for nodes, assets, and accounts
|
||||||
|
// For now, return empty tags
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCacheKey generates a cache key for the request
|
||||||
|
func (m *AuthorizationMatcher) getCacheKey(req *model.AuthRequest) string {
|
||||||
|
return fmt.Sprintf("auth_v2:%d:%d:%d:%d:%s",
|
||||||
|
req.UserId, req.NodeId, req.AssetId, req.AccountId, req.Action)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCachedResult retrieves cached authorization result
|
||||||
|
func (m *AuthorizationMatcher) getCachedResult(cacheKey string) *model.AuthResult {
|
||||||
|
// TODO: Implement Redis caching
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cacheResult caches the authorization result
|
||||||
|
func (m *AuthorizationMatcher) cacheResult(cacheKey string, result *model.AuthResult) {
|
||||||
|
// TODO: Implement Redis caching with TTL
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchWithScope checks if a request is authorized using only specified rule IDs (like V1's AuthorizationIds filtering)
|
||||||
|
func (m *AuthorizationMatcher) MatchWithScope(ctx *gin.Context, req *model.AuthRequest, ruleIds []int) (*model.AuthResult, error) {
|
||||||
|
if len(ruleIds) == 0 {
|
||||||
|
return &model.AuthResult{
|
||||||
|
Allowed: false,
|
||||||
|
Reason: "No rule IDs provided",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get only the rules that user has permission to access (already filtered by enabled=true)
|
||||||
|
enabledRules, err := m.repo.GetByResourceIds(ctx, ruleIds)
|
||||||
|
if err != nil {
|
||||||
|
return &model.AuthResult{
|
||||||
|
Allowed: false,
|
||||||
|
Reason: "Failed to load authorization rules",
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each rule in the filtered scope
|
||||||
|
for _, rule := range enabledRules {
|
||||||
|
if m.matchRule(ctx, rule, req) {
|
||||||
|
// If this rule allows the requested action, grant permission immediately
|
||||||
|
if rule.Permissions.HasPermission(req.Action) {
|
||||||
|
return &model.AuthResult{
|
||||||
|
Allowed: true,
|
||||||
|
Permissions: rule.Permissions,
|
||||||
|
Reason: fmt.Sprintf("Allowed by rule: %s", rule.Name),
|
||||||
|
RuleId: rule.Id,
|
||||||
|
RuleName: rule.Name,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.AuthResult{
|
||||||
|
Allowed: false,
|
||||||
|
Reason: "No matching authorization rule found in scope",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchBatchWithScope checks if batch requests are authorized using only specified rule IDs
|
||||||
|
func (m *AuthorizationMatcher) MatchBatchWithScope(ctx *gin.Context, req *model.BatchAuthRequest, ruleIds []int) (*model.BatchAuthResult, error) {
|
||||||
|
results := make(map[model.AuthAction]*model.AuthResult)
|
||||||
|
|
||||||
|
// Initialize all actions as denied
|
||||||
|
for _, action := range req.Actions {
|
||||||
|
results[action] = &model.AuthResult{
|
||||||
|
Allowed: false,
|
||||||
|
Reason: "No matching authorization rule found in scope",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ruleIds) == 0 {
|
||||||
|
for action := range results {
|
||||||
|
results[action].Reason = "No rule IDs provided"
|
||||||
|
}
|
||||||
|
return &model.BatchAuthResult{Results: results}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single database query for all rules
|
||||||
|
enabledRules, err := m.repo.GetByResourceIds(ctx, ruleIds)
|
||||||
|
if err != nil {
|
||||||
|
for action := range results {
|
||||||
|
results[action] = &model.AuthResult{
|
||||||
|
Allowed: false,
|
||||||
|
Reason: "Failed to load authorization rules",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &model.BatchAuthResult{Results: results}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base request for rule matching
|
||||||
|
baseReq := &model.AuthRequest{
|
||||||
|
UserId: req.UserId,
|
||||||
|
NodeId: req.NodeId,
|
||||||
|
AssetId: req.AssetId,
|
||||||
|
AccountId: req.AccountId,
|
||||||
|
ClientIP: req.ClientIP,
|
||||||
|
UserAgent: req.UserAgent,
|
||||||
|
Timestamp: req.Timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each rule once and validate all actions
|
||||||
|
for _, rule := range enabledRules {
|
||||||
|
if m.matchRule(ctx, rule, baseReq) {
|
||||||
|
for _, action := range req.Actions {
|
||||||
|
if rule.Permissions.HasPermission(action) && !results[action].Allowed {
|
||||||
|
results[action] = &model.AuthResult{
|
||||||
|
Allowed: true,
|
||||||
|
Permissions: rule.Permissions,
|
||||||
|
Reason: fmt.Sprintf("Allowed by rule: %s", rule.Name),
|
||||||
|
RuleId: rule.Id,
|
||||||
|
RuleName: rule.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Early exit if all actions are allowed
|
||||||
|
allAllowed := true
|
||||||
|
for _, action := range req.Actions {
|
||||||
|
if !results[action].Allowed {
|
||||||
|
allAllowed = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if allAllowed {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.BatchAuthResult{Results: results}, nil
|
||||||
|
}
|
625
backend/internal/service/authorization_migration.go
Normal file
625
backend/internal/service/authorization_migration.go
Normal file
@@ -0,0 +1,625 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/veops/oneterm/internal/model"
|
||||||
|
"github.com/veops/oneterm/internal/repository"
|
||||||
|
dbpkg "github.com/veops/oneterm/pkg/db"
|
||||||
|
"github.com/veops/oneterm/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthorizationMigrationService handles V1 to V2 authorization migration
|
||||||
|
type AuthorizationMigrationService struct {
|
||||||
|
db *gorm.DB
|
||||||
|
v1Repo repository.IAuthorizationRepository
|
||||||
|
v2Repo repository.IAuthorizationV2Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthorizationMigrationService creates a new migration service
|
||||||
|
func NewAuthorizationMigrationService(
|
||||||
|
db *gorm.DB,
|
||||||
|
v1Repo repository.IAuthorizationRepository,
|
||||||
|
v2Repo repository.IAuthorizationV2Repository,
|
||||||
|
) *AuthorizationMigrationService {
|
||||||
|
return &AuthorizationMigrationService{
|
||||||
|
db: db,
|
||||||
|
v1Repo: v1Repo,
|
||||||
|
v2Repo: v2Repo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MigrateV1ToV2 performs the complete migration from V1 to V2
|
||||||
|
func (s *AuthorizationMigrationService) MigrateV1ToV2(ctx context.Context) error {
|
||||||
|
logger.L().Info("Starting V1 to V2 authorization migration")
|
||||||
|
|
||||||
|
// Check if migration is already completed
|
||||||
|
if completed, err := s.IsMigrationCompleted(ctx); err != nil {
|
||||||
|
return fmt.Errorf("failed to check migration status: %w", err)
|
||||||
|
} else if completed {
|
||||||
|
logger.L().Info("Migration already completed, skipping")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all V1 authorization rules
|
||||||
|
v1Rules, err := s.getAllV1Rules(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get V1 rules: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(v1Rules) == 0 {
|
||||||
|
logger.L().Info("No V1 authorization rules found, marking migration as completed")
|
||||||
|
return s.markMigrationCompleted(ctx, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.L().Info("Found V1 authorization rules", zap.Int("count", len(v1Rules)))
|
||||||
|
|
||||||
|
// Mark migration as running
|
||||||
|
if err := s.markMigrationRunning(ctx); err != nil {
|
||||||
|
return fmt.Errorf("failed to mark migration as running: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start transaction
|
||||||
|
tx := s.db.Begin()
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
s.markMigrationFailed(ctx, fmt.Sprintf("panic during migration: %v", r))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Migrate each V1 rule to V2
|
||||||
|
migratedCount := 0
|
||||||
|
for _, v1Rule := range v1Rules {
|
||||||
|
v2Rule := s.convertV1ToV2(v1Rule)
|
||||||
|
if err := s.v2Repo.Create(ctx, v2Rule); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
s.markMigrationFailed(ctx, fmt.Sprintf("failed to create V2 rule for V1 rule %d: %v", v1Rule.Id, err))
|
||||||
|
return fmt.Errorf("failed to create V2 rule for V1 rule %d: %w", v1Rule.Id, err)
|
||||||
|
}
|
||||||
|
migratedCount++
|
||||||
|
logger.L().Debug("Migrated V1 rule", zap.Int("v1_id", v1Rule.Id), zap.Int("v2_id", v2Rule.Id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also migrate asset authorization fields from V1 to V2 format
|
||||||
|
if err := s.migrateAssetAuthorizationFields(ctx, tx); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
s.markMigrationFailed(ctx, fmt.Sprintf("failed to migrate asset authorization fields: %v", err))
|
||||||
|
return fmt.Errorf("failed to migrate asset authorization fields: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark migration as completed
|
||||||
|
if err := s.markMigrationCompleted(ctx, migratedCount); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("failed to mark migration as completed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit transaction
|
||||||
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to commit migration transaction: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.L().Info("V1 to V2 migration completed successfully",
|
||||||
|
zap.Int("migrated_count", migratedCount))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsMigrationCompleted checks if the migration has been completed
|
||||||
|
func (s *AuthorizationMigrationService) IsMigrationCompleted(ctx context.Context) (bool, error) {
|
||||||
|
var record model.MigrationRecord
|
||||||
|
err := s.db.Where("migration_name = ?", model.MigrationAuthV1ToV2).First(&record).Error
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return record.Status == model.MigrationStatusCompleted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// markMigrationCompleted marks the migration as completed
|
||||||
|
func (s *AuthorizationMigrationService) markMigrationCompleted(ctx context.Context, recordsCount int) error {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// Try to update existing record, or create new one
|
||||||
|
var record model.MigrationRecord
|
||||||
|
err := s.db.Where("migration_name = ?", model.MigrationAuthV1ToV2).First(&record).Error
|
||||||
|
if err != nil && err != gorm.ErrRecordNotFound {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
// Create new record
|
||||||
|
record = model.MigrationRecord{
|
||||||
|
MigrationName: model.MigrationAuthV1ToV2,
|
||||||
|
Status: model.MigrationStatusCompleted,
|
||||||
|
StartedAt: &now,
|
||||||
|
CompletedAt: &now,
|
||||||
|
RecordsCount: recordsCount,
|
||||||
|
}
|
||||||
|
return s.db.Create(&record).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing record
|
||||||
|
return s.db.Model(&record).Updates(map[string]interface{}{
|
||||||
|
"status": model.MigrationStatusCompleted,
|
||||||
|
"completed_at": &now,
|
||||||
|
"error_message": "",
|
||||||
|
"records_count": recordsCount,
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAllV1Rules retrieves all V1 authorization rules
|
||||||
|
func (s *AuthorizationMigrationService) getAllV1Rules(ctx context.Context) ([]*model.Authorization, error) {
|
||||||
|
var rules []*model.Authorization
|
||||||
|
err := s.db.Find(&rules).Error
|
||||||
|
return rules, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertV1ToV2 converts a V1 authorization rule to V2 format
|
||||||
|
func (s *AuthorizationMigrationService) convertV1ToV2(v1 *model.Authorization) *model.AuthorizationV2 {
|
||||||
|
v2 := &model.AuthorizationV2{
|
||||||
|
Name: s.generateRuleName(v1),
|
||||||
|
Description: s.generateRuleDescription(v1),
|
||||||
|
Enabled: true,
|
||||||
|
Rids: v1.Rids,
|
||||||
|
|
||||||
|
// Copy standard fields
|
||||||
|
ResourceId: v1.ResourceId,
|
||||||
|
CreatorId: v1.CreatorId,
|
||||||
|
UpdaterId: v1.UpdaterId,
|
||||||
|
CreatedAt: v1.CreatedAt,
|
||||||
|
UpdatedAt: v1.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert specific IDs to selectors
|
||||||
|
if v1.NodeId > 0 {
|
||||||
|
v2.NodeSelector = model.TargetSelector{
|
||||||
|
Type: model.SelectorTypeIds,
|
||||||
|
Values: []string{fmt.Sprintf("%d", v1.NodeId)},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
v2.NodeSelector = model.TargetSelector{
|
||||||
|
Type: model.SelectorTypeAll,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v1.AssetId > 0 {
|
||||||
|
v2.AssetSelector = model.TargetSelector{
|
||||||
|
Type: model.SelectorTypeIds,
|
||||||
|
Values: []string{fmt.Sprintf("%d", v1.AssetId)},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
v2.AssetSelector = model.TargetSelector{
|
||||||
|
Type: model.SelectorTypeAll,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v1.AccountId > 0 {
|
||||||
|
v2.AccountSelector = model.TargetSelector{
|
||||||
|
Type: model.SelectorTypeIds,
|
||||||
|
Values: []string{fmt.Sprintf("%d", v1.AccountId)},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
v2.AccountSelector = model.TargetSelector{
|
||||||
|
Type: model.SelectorTypeAll,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default permissions - V1 only had connect permission
|
||||||
|
v2.Permissions = model.AuthPermissions{
|
||||||
|
Connect: true,
|
||||||
|
FileUpload: s.getDefaultFileUploadPermission(),
|
||||||
|
FileDownload: s.getDefaultFileDownloadPermission(),
|
||||||
|
Copy: s.getDefaultCopyPermission(),
|
||||||
|
Paste: s.getDefaultPastePermission(),
|
||||||
|
Share: false, // Default to false for security
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default access control
|
||||||
|
v2.AccessControl = model.AccessControl{
|
||||||
|
IPWhitelist: []string{}, // No IP restrictions by default
|
||||||
|
MaxSessions: 0, // No session limit by default
|
||||||
|
SessionTimeout: 0, // Use system default
|
||||||
|
}
|
||||||
|
|
||||||
|
return v2
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateRuleName generates a name for the migrated rule
|
||||||
|
func (s *AuthorizationMigrationService) generateRuleName(v1 *model.Authorization) string {
|
||||||
|
parts := []string{"Migrated"}
|
||||||
|
|
||||||
|
if v1.NodeId > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("Node-%d", v1.NodeId))
|
||||||
|
}
|
||||||
|
if v1.AssetId > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("Asset-%d", v1.AssetId))
|
||||||
|
}
|
||||||
|
if v1.AccountId > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("Account-%d", v1.AccountId))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s-Rule-V1-%d", joinParts(parts), v1.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateRuleDescription generates a description for the migrated rule
|
||||||
|
func (s *AuthorizationMigrationService) generateRuleDescription(v1 *model.Authorization) string {
|
||||||
|
return fmt.Sprintf("Automatically migrated from V1 authorization rule (ID: %d). "+
|
||||||
|
"Node: %d, Asset: %d, Account: %d, Roles: %v",
|
||||||
|
v1.Id, v1.NodeId, v1.AssetId, v1.AccountId, v1.Rids)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions to get default permissions from system config
|
||||||
|
func (s *AuthorizationMigrationService) getDefaultFileUploadPermission() bool {
|
||||||
|
// In a real implementation, you might check system configuration
|
||||||
|
// For now, return true as a safe default
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthorizationMigrationService) getDefaultFileDownloadPermission() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthorizationMigrationService) getDefaultCopyPermission() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthorizationMigrationService) getDefaultPastePermission() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// joinParts joins string parts with "-"
|
||||||
|
func joinParts(parts []string) string {
|
||||||
|
result := ""
|
||||||
|
for i, part := range parts {
|
||||||
|
if i > 0 {
|
||||||
|
result += "-"
|
||||||
|
}
|
||||||
|
result += part
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// markMigrationRunning marks the migration as running
|
||||||
|
func (s *AuthorizationMigrationService) markMigrationRunning(ctx context.Context) error {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// Try to update existing record, or create new one
|
||||||
|
var record model.MigrationRecord
|
||||||
|
err := s.db.Where("migration_name = ?", model.MigrationAuthV1ToV2).First(&record).Error
|
||||||
|
if err != nil && err != gorm.ErrRecordNotFound {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
// Create new record
|
||||||
|
record = model.MigrationRecord{
|
||||||
|
MigrationName: model.MigrationAuthV1ToV2,
|
||||||
|
Status: model.MigrationStatusRunning,
|
||||||
|
StartedAt: &now,
|
||||||
|
}
|
||||||
|
return s.db.Create(&record).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing record
|
||||||
|
return s.db.Model(&record).Updates(map[string]interface{}{
|
||||||
|
"status": model.MigrationStatusRunning,
|
||||||
|
"started_at": &now,
|
||||||
|
"completed_at": nil,
|
||||||
|
"error_message": "",
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// markMigrationFailed marks the migration as failed with error message
|
||||||
|
func (s *AuthorizationMigrationService) markMigrationFailed(ctx context.Context, errorMsg string) error {
|
||||||
|
var record model.MigrationRecord
|
||||||
|
err := s.db.Where("migration_name = ?", model.MigrationAuthV1ToV2).First(&record).Error
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
// Create new record with failed status
|
||||||
|
record = model.MigrationRecord{
|
||||||
|
MigrationName: model.MigrationAuthV1ToV2,
|
||||||
|
Status: model.MigrationStatusFailed,
|
||||||
|
ErrorMessage: errorMsg,
|
||||||
|
}
|
||||||
|
return s.db.Create(&record).Error
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing record
|
||||||
|
return s.db.Model(&record).Updates(map[string]interface{}{
|
||||||
|
"status": model.MigrationStatusFailed,
|
||||||
|
"error_message": errorMsg,
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrateAssetAuthorizationFields migrates asset authorization fields from V1 to V2 format
|
||||||
|
func (s *AuthorizationMigrationService) migrateAssetAuthorizationFields(ctx context.Context, tx *gorm.DB) error {
|
||||||
|
logger.L().Info("Starting asset authorization field migration")
|
||||||
|
|
||||||
|
// Query assets with potential V1 authorization data
|
||||||
|
var assets []*model.Asset
|
||||||
|
if err := tx.Select("id", "authorization").Find(&assets).Error; err != nil {
|
||||||
|
logger.L().Error("Failed to get assets for authorization field migration", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationCount := 0
|
||||||
|
for _, asset := range assets {
|
||||||
|
if asset.Id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if authorization field needs migration
|
||||||
|
needsMigration, err := s.checkAssetAuthorizationNeedsMigration(tx, asset.Id)
|
||||||
|
if err != nil {
|
||||||
|
logger.L().Error("Failed to check asset authorization migration need", zap.Int("assetId", asset.Id), zap.Error(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !needsMigration {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate this asset
|
||||||
|
if err := s.migrateAssetAuthorizationField(ctx, tx, asset.Id); err != nil {
|
||||||
|
logger.L().Error("Failed to migrate asset authorization field", zap.Int("assetId", asset.Id), zap.Error(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.L().Info("Asset authorization field migration completed", zap.Int("migrated", migrationCount))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MigrateV1AuthorizationData migrates V1 authorization data format to V2
|
||||||
|
func MigrateV1AuthorizationData(ctx context.Context) error {
|
||||||
|
logger.L().Info("Starting V1 to V2 authorization data migration")
|
||||||
|
|
||||||
|
// Query assets with potential V1 authorization data
|
||||||
|
var assets []*model.Asset
|
||||||
|
if err := dbpkg.DB.Select("id", "authorization").Find(&assets).Error; err != nil {
|
||||||
|
logger.L().Error("Failed to get assets for migration", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationCount := 0
|
||||||
|
for _, asset := range assets {
|
||||||
|
if asset.Id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if authorization field needs migration
|
||||||
|
needsMigration, err := checkNeedsMigration(asset.Id)
|
||||||
|
if err != nil {
|
||||||
|
logger.L().Error("Failed to check migration need", zap.Int("assetId", asset.Id), zap.Error(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !needsMigration {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate this asset
|
||||||
|
if err := migrateAssetAuthorization(ctx, asset.Id); err != nil {
|
||||||
|
logger.L().Error("Failed to migrate asset authorization", zap.Int("assetId", asset.Id), zap.Error(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.L().Info("V1 to V2 authorization migration completed", zap.Int("migrated", migrationCount))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkNeedsMigration checks if an asset's authorization data needs migration
|
||||||
|
func checkNeedsMigration(assetId int) (bool, error) {
|
||||||
|
var rawAuth json.RawMessage
|
||||||
|
if err := dbpkg.DB.Model(&model.Asset{}).
|
||||||
|
Where("id = ?", assetId).
|
||||||
|
Select("authorization").
|
||||||
|
Scan(&rawAuth).Error; err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rawAuth) == 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse as V2 format first
|
||||||
|
var v2Auth map[int]model.AccountAuthorization
|
||||||
|
if err := json.Unmarshal(rawAuth, &v2Auth); err == nil {
|
||||||
|
// Successfully parsed as V2, no migration needed
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse as V1 format
|
||||||
|
var v1Auth map[int][]int
|
||||||
|
if err := json.Unmarshal(rawAuth, &v1Auth); err == nil {
|
||||||
|
// Successfully parsed as V1, needs migration
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot parse as either format, skip
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrateAssetAuthorization migrates a single asset's authorization data
|
||||||
|
func migrateAssetAuthorization(ctx context.Context, assetId int) error {
|
||||||
|
// Get raw authorization data
|
||||||
|
var rawAuth json.RawMessage
|
||||||
|
if err := dbpkg.DB.Model(&model.Asset{}).
|
||||||
|
Where("id = ?", assetId).
|
||||||
|
Select("authorization").
|
||||||
|
Scan(&rawAuth).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rawAuth) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse as V1 format
|
||||||
|
var v1Auth map[int][]int
|
||||||
|
if err := json.Unmarshal(rawAuth, &v1Auth); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse V1 authorization data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get default permissions from config
|
||||||
|
defaultPermissions := getDefaultAuthPermissions()
|
||||||
|
|
||||||
|
// Convert V1 to V2 format
|
||||||
|
v2Auth := make(map[int]model.AccountAuthorization)
|
||||||
|
for accountId, roleIds := range v1Auth {
|
||||||
|
v2Auth[accountId] = model.AccountAuthorization{
|
||||||
|
Rids: roleIds,
|
||||||
|
Permissions: &defaultPermissions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the database
|
||||||
|
if err := dbpkg.DB.Model(&model.Asset{}).
|
||||||
|
Where("id = ?", assetId).
|
||||||
|
Update("authorization", v2Auth).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to update authorization data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.L().Debug("Migrated asset authorization data",
|
||||||
|
zap.Int("assetId", assetId),
|
||||||
|
zap.Int("accounts", len(v1Auth)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultAuthPermissions returns default permissions for migration
|
||||||
|
func getDefaultAuthPermissions() model.AuthPermissions {
|
||||||
|
// Get from config if available
|
||||||
|
if config := model.GlobalConfig.Load(); config != nil {
|
||||||
|
return config.GetDefaultPermissionsAsAuthPermissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to connect-only permissions
|
||||||
|
return model.AuthPermissions{
|
||||||
|
Connect: true,
|
||||||
|
FileUpload: false,
|
||||||
|
FileDownload: false,
|
||||||
|
Copy: false,
|
||||||
|
Paste: false,
|
||||||
|
Share: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAssetAuthorizationNeedsMigration checks if an asset's authorization data needs migration
|
||||||
|
func (s *AuthorizationMigrationService) checkAssetAuthorizationNeedsMigration(tx *gorm.DB, assetId int) (bool, error) {
|
||||||
|
var rawAuth json.RawMessage
|
||||||
|
if err := tx.Model(&model.Asset{}).
|
||||||
|
Where("id = ?", assetId).
|
||||||
|
Select("authorization").
|
||||||
|
Scan(&rawAuth).Error; err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rawAuth) == 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse as V2 format first
|
||||||
|
var v2Auth map[int]model.AccountAuthorization
|
||||||
|
if err := json.Unmarshal(rawAuth, &v2Auth); err == nil {
|
||||||
|
// Successfully parsed as V2, no migration needed
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse as V1 format
|
||||||
|
var v1Auth map[int][]int
|
||||||
|
if err := json.Unmarshal(rawAuth, &v1Auth); err == nil {
|
||||||
|
// Successfully parsed as V1, needs migration
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot parse as either format, skip
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrateAssetAuthorizationField migrates a single asset's authorization data
|
||||||
|
func (s *AuthorizationMigrationService) migrateAssetAuthorizationField(ctx context.Context, tx *gorm.DB, assetId int) error {
|
||||||
|
// Get raw authorization data
|
||||||
|
var rawAuth json.RawMessage
|
||||||
|
if err := tx.Model(&model.Asset{}).
|
||||||
|
Where("id = ?", assetId).
|
||||||
|
Select("authorization").
|
||||||
|
Scan(&rawAuth).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rawAuth) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse as V1 format
|
||||||
|
var v1Auth map[int][]int
|
||||||
|
if err := json.Unmarshal(rawAuth, &v1Auth); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse V1 authorization data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get default permissions
|
||||||
|
defaultPermissions := s.getDefaultAuthPermissions()
|
||||||
|
|
||||||
|
// Convert V1 to V2 format
|
||||||
|
v2Auth := make(map[int]model.AccountAuthorization)
|
||||||
|
for accountId, roleIds := range v1Auth {
|
||||||
|
v2Auth[accountId] = model.AccountAuthorization{
|
||||||
|
Rids: roleIds,
|
||||||
|
Permissions: &defaultPermissions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the database
|
||||||
|
if err := tx.Model(&model.Asset{}).
|
||||||
|
Where("id = ?", assetId).
|
||||||
|
Update("authorization", v2Auth).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to update authorization data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.L().Debug("Migrated asset authorization data",
|
||||||
|
zap.Int("assetId", assetId),
|
||||||
|
zap.Int("accounts", len(v1Auth)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultAuthPermissions returns default permissions for migration service
|
||||||
|
func (s *AuthorizationMigrationService) getDefaultAuthPermissions() model.AuthPermissions {
|
||||||
|
// Get from config if available
|
||||||
|
if config := model.GlobalConfig.Load(); config != nil {
|
||||||
|
return config.GetDefaultPermissionsAsAuthPermissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to connect-only permissions
|
||||||
|
return model.AuthPermissions{
|
||||||
|
Connect: true,
|
||||||
|
FileUpload: false,
|
||||||
|
FileDownload: false,
|
||||||
|
Copy: false,
|
||||||
|
Paste: false,
|
||||||
|
Share: false,
|
||||||
|
}
|
||||||
|
}
|
1160
backend/internal/service/authorization_v2.go
Normal file
1160
backend/internal/service/authorization_v2.go
Normal file
File diff suppressed because it is too large
Load Diff
265
backend/internal/service/command_analyzer.go
Normal file
265
backend/internal/service/command_analyzer.go
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/veops/oneterm/internal/model"
|
||||||
|
"github.com/veops/oneterm/internal/repository"
|
||||||
|
gsession "github.com/veops/oneterm/internal/session"
|
||||||
|
"github.com/veops/oneterm/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommandAnalyzer handles command analysis for sessions
|
||||||
|
type CommandAnalyzer struct {
|
||||||
|
authService IAuthorizationService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCommandAnalyzer creates a new command analyzer
|
||||||
|
func NewCommandAnalyzer() *CommandAnalyzer {
|
||||||
|
return &CommandAnalyzer{
|
||||||
|
authService: DefaultAuthService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnalyzeSessionCommands analyzes and builds the final command list for a session
|
||||||
|
// This combines asset-level and authorization-level command controls
|
||||||
|
func (ca *CommandAnalyzer) AnalyzeSessionCommands(ctx *gin.Context, sess *gsession.Session) ([]*model.Command, error) {
|
||||||
|
// Get all available commands from cache
|
||||||
|
allCommands, err := repository.GetAllFromCacheDb(ctx, model.DefaultCommand)
|
||||||
|
if err != nil {
|
||||||
|
logger.L().Error("Failed to get commands from cache", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter enabled commands
|
||||||
|
enabledCommands := lo.Filter(allCommands, func(cmd *model.Command, _ int) bool {
|
||||||
|
return cmd.Enable
|
||||||
|
})
|
||||||
|
|
||||||
|
// Analyze asset-level command control
|
||||||
|
assetCommands := ca.analyzeAssetCommands(sess.Session.Asset, enabledCommands)
|
||||||
|
|
||||||
|
// Analyze authorization-level command control
|
||||||
|
authCommands, err := ca.analyzeAuthorizationCommands(ctx, sess, enabledCommands)
|
||||||
|
if err != nil {
|
||||||
|
logger.L().Error("Failed to analyze authorization commands", zap.Error(err))
|
||||||
|
// Continue with asset-level commands only
|
||||||
|
authCommands = []*model.Command{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge and deduplicate command lists
|
||||||
|
finalCommands := ca.mergeCommands(assetCommands, authCommands)
|
||||||
|
|
||||||
|
// Compile regex patterns for performance
|
||||||
|
for _, cmd := range finalCommands {
|
||||||
|
if cmd.IsRe {
|
||||||
|
if re, err := regexp.Compile(cmd.Cmd); err == nil {
|
||||||
|
cmd.Re = re
|
||||||
|
} else {
|
||||||
|
logger.L().Warn("Invalid regex pattern in command",
|
||||||
|
zap.String("cmd", cmd.Cmd),
|
||||||
|
zap.Int("id", cmd.Id),
|
||||||
|
zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.L().Info("Command analysis completed",
|
||||||
|
zap.String("sessionId", sess.SessionId),
|
||||||
|
zap.Int("assetCommands", len(assetCommands)),
|
||||||
|
zap.Int("authCommands", len(authCommands)),
|
||||||
|
zap.Int("finalCommands", len(finalCommands)))
|
||||||
|
|
||||||
|
return finalCommands, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// analyzeAssetCommands analyzes asset-level command controls from V2 system
|
||||||
|
func (ca *CommandAnalyzer) analyzeAssetCommands(asset *model.Asset, allCommands []*model.Command) []*model.Command {
|
||||||
|
var result []*model.Command
|
||||||
|
|
||||||
|
// V2 asset command control
|
||||||
|
if asset.AssetCommandControl != nil && asset.AssetCommandControl.Enabled {
|
||||||
|
var v2Commands []*model.Command
|
||||||
|
|
||||||
|
// Process direct command IDs
|
||||||
|
if len(asset.AssetCommandControl.CmdIds) > 0 {
|
||||||
|
cmdMap := make(map[int]*model.Command)
|
||||||
|
for _, cmd := range allCommands {
|
||||||
|
cmdMap[cmd.Id] = cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cmdId := range asset.AssetCommandControl.CmdIds {
|
||||||
|
if cmd, exists := cmdMap[cmdId]; exists {
|
||||||
|
v2Commands = append(v2Commands, cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process command template IDs
|
||||||
|
if len(asset.AssetCommandControl.TemplateIds) > 0 {
|
||||||
|
templateCommands := ca.expandCommandTemplates(asset.AssetCommandControl.TemplateIds, allCommands)
|
||||||
|
v2Commands = append(v2Commands, templateCommands...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// All configured commands are intercepted
|
||||||
|
result = append(result, v2Commands...)
|
||||||
|
|
||||||
|
logger.L().Debug("Asset V2 command control applied",
|
||||||
|
zap.Int("assetId", asset.Id),
|
||||||
|
zap.Int("cmdCount", len(v2Commands)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return lo.UniqBy(result, func(cmd *model.Command) int { return cmd.Id })
|
||||||
|
}
|
||||||
|
|
||||||
|
// analyzeAuthorizationCommands analyzes authorization-level command controls from V2 rules
|
||||||
|
func (ca *CommandAnalyzer) analyzeAuthorizationCommands(ctx *gin.Context, sess *gsession.Session, allCommands []*model.Command) ([]*model.Command, error) {
|
||||||
|
// Get user's authorized V2 rules
|
||||||
|
authV2ResourceIds, err := ca.getAuthorizedV2ResourceIds(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(authV2ResourceIds) == 0 {
|
||||||
|
return []*model.Command{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get V2 rules that apply to this session
|
||||||
|
authV2Service := NewAuthorizationV2Service()
|
||||||
|
rules, err := authV2Service.repo.GetByResourceIds(ctx, authV2ResourceIds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []*model.Command
|
||||||
|
|
||||||
|
// Analyze each applicable rule
|
||||||
|
for _, rule := range rules {
|
||||||
|
if !rule.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if rule matches this session (simplified matching)
|
||||||
|
if ca.ruleMatchesSession(rule, sess) {
|
||||||
|
ruleCommands := ca.extractCommandsFromRule(rule, allCommands)
|
||||||
|
result = append(result, ruleCommands...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lo.UniqBy(result, func(cmd *model.Command) int { return cmd.Id }), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ruleMatchesSession checks if a V2 rule matches the current session (simplified)
|
||||||
|
func (ca *CommandAnalyzer) ruleMatchesSession(rule *model.AuthorizationV2, sess *gsession.Session) bool {
|
||||||
|
// Quick check for asset selector
|
||||||
|
if rule.AssetSelector.Type == model.SelectorTypeIds {
|
||||||
|
assetIds := lo.FilterMap(rule.AssetSelector.Values, func(v string, _ int) (int, bool) {
|
||||||
|
if id, err := strconv.Atoi(v); err == nil {
|
||||||
|
return id, true
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
})
|
||||||
|
if !lo.Contains(assetIds, sess.AssetId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if rule.AssetSelector.Type != model.SelectorTypeAll {
|
||||||
|
// For regex/tags selectors, we'd need more complex matching
|
||||||
|
// For now, assume they match (could be optimized later)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quick check for account selector
|
||||||
|
if rule.AccountSelector.Type == model.SelectorTypeIds {
|
||||||
|
accountIds := lo.FilterMap(rule.AccountSelector.Values, func(v string, _ int) (int, bool) {
|
||||||
|
if id, err := strconv.Atoi(v); err == nil {
|
||||||
|
return id, true
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
})
|
||||||
|
if !lo.Contains(accountIds, sess.AccountId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractCommandsFromRule extracts commands from a V2 authorization rule
|
||||||
|
func (ca *CommandAnalyzer) extractCommandsFromRule(rule *model.AuthorizationV2, allCommands []*model.Command) []*model.Command {
|
||||||
|
var result []*model.Command
|
||||||
|
|
||||||
|
// Process direct command IDs
|
||||||
|
if len(rule.AccessControl.CmdIds) > 0 {
|
||||||
|
cmdMap := make(map[int]*model.Command)
|
||||||
|
for _, cmd := range allCommands {
|
||||||
|
cmdMap[cmd.Id] = cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cmdId := range rule.AccessControl.CmdIds {
|
||||||
|
if cmd, exists := cmdMap[cmdId]; exists {
|
||||||
|
result = append(result, cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process command template IDs
|
||||||
|
if len(rule.AccessControl.TemplateIds) > 0 {
|
||||||
|
templateCommands := ca.expandCommandTemplates(rule.AccessControl.TemplateIds, allCommands)
|
||||||
|
result = append(result, templateCommands...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// All configured commands are intercepted
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// expandCommandTemplates expands command template IDs to actual commands
|
||||||
|
func (ca *CommandAnalyzer) expandCommandTemplates(templateIds []int, allCommands []*model.Command) []*model.Command {
|
||||||
|
// Get command templates from database
|
||||||
|
commandTemplateService := NewCommandTemplateService()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var expandedCommands []*model.Command
|
||||||
|
|
||||||
|
for _, templateId := range templateIds {
|
||||||
|
template, err := commandTemplateService.GetCommandTemplate(ctx, templateId)
|
||||||
|
if err != nil {
|
||||||
|
logger.L().Warn("Failed to get command template",
|
||||||
|
zap.Int("templateId", templateId),
|
||||||
|
zap.Error(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if template == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get commands from template
|
||||||
|
templateCmdIds := lo.Map(template.CmdIds, func(id int, _ int) int { return id })
|
||||||
|
templateCommands := lo.Filter(allCommands, func(cmd *model.Command, _ int) bool {
|
||||||
|
return lo.Contains(templateCmdIds, cmd.Id)
|
||||||
|
})
|
||||||
|
|
||||||
|
expandedCommands = append(expandedCommands, templateCommands...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return expandedCommands
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeCommands merges and deduplicates command lists
|
||||||
|
func (ca *CommandAnalyzer) mergeCommands(assetCommands, authCommands []*model.Command) []*model.Command {
|
||||||
|
// Combine all commands
|
||||||
|
allCommands := append(assetCommands, authCommands...)
|
||||||
|
|
||||||
|
// Deduplicate by ID
|
||||||
|
return lo.UniqBy(allCommands, func(cmd *model.Command) int { return cmd.Id })
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAuthorizedV2ResourceIds gets V2 authorization rule resource IDs that user has permission to
|
||||||
|
func (ca *CommandAnalyzer) getAuthorizedV2ResourceIds(ctx *gin.Context) ([]int, error) {
|
||||||
|
return ca.authService.(*AuthorizationService).getAuthorizedV2ResourceIds(ctx)
|
||||||
|
}
|
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/veops/oneterm/internal/model"
|
"github.com/veops/oneterm/internal/model"
|
||||||
"github.com/veops/oneterm/internal/repository"
|
"github.com/veops/oneterm/internal/repository"
|
||||||
dbpkg "github.com/veops/oneterm/pkg/db"
|
dbpkg "github.com/veops/oneterm/pkg/db"
|
||||||
@@ -26,8 +27,25 @@ func NewCommandTemplateService() *CommandTemplateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BuildQuery builds the base query for command templates
|
// BuildQuery builds the base query for command templates
|
||||||
func (s *CommandTemplateService) BuildQuery(ctx context.Context) (*gorm.DB, error) {
|
func (s *CommandTemplateService) BuildQuery(ctx *gin.Context) (*gorm.DB, error) {
|
||||||
db := dbpkg.DB.Model(model.DefaultCommandTemplate)
|
db := dbpkg.DB.Model(model.DefaultCommandTemplate)
|
||||||
|
|
||||||
|
// Apply search filter
|
||||||
|
if search := ctx.Query("search"); search != "" {
|
||||||
|
db = db.Where("name LIKE ? OR description LIKE ?", "%"+search+"%", "%"+search+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply category filter
|
||||||
|
if category := ctx.Query("category"); category != "" {
|
||||||
|
db = db.Where("category = ?", category)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply builtin filter
|
||||||
|
if builtinStr := ctx.Query("builtin"); builtinStr != "" {
|
||||||
|
builtin := builtinStr == "true"
|
||||||
|
db = db.Where("is_builtin = ?", builtin)
|
||||||
|
}
|
||||||
|
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -8,6 +8,8 @@ import (
|
|||||||
"github.com/veops/oneterm/internal/repository"
|
"github.com/veops/oneterm/internal/repository"
|
||||||
"github.com/veops/oneterm/pkg/cache"
|
"github.com/veops/oneterm/pkg/cache"
|
||||||
dbpkg "github.com/veops/oneterm/pkg/db"
|
dbpkg "github.com/veops/oneterm/pkg/db"
|
||||||
|
"github.com/veops/oneterm/pkg/logger"
|
||||||
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,3 +49,168 @@ func (s *ConfigService) SaveConfig(ctx context.Context, cfg *model.Config) error
|
|||||||
func (s *ConfigService) GetConfig(ctx context.Context) (*model.Config, error) {
|
func (s *ConfigService) GetConfig(ctx context.Context) (*model.Config, error) {
|
||||||
return s.repo.GetConfig(ctx)
|
return s.repo.GetConfig(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MigrateConfigStructure migrates the config table structure from protocol-specific to unified permissions
|
||||||
|
func (s *ConfigService) MigrateConfigStructure(ctx context.Context) error {
|
||||||
|
logger.L().Info("Starting config table structure migration")
|
||||||
|
|
||||||
|
// Check if migration is needed by looking for old columns
|
||||||
|
var columnExists bool
|
||||||
|
if err := dbpkg.DB.Raw("SELECT COUNT(*) > 0 FROM information_schema.columns WHERE table_name = 'config' AND column_name = 'ssh_copy'").Scan(&columnExists).Error; err != nil {
|
||||||
|
logger.L().Error("Failed to check for old columns", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !columnExists {
|
||||||
|
logger.L().Info("Config table already migrated, skipping")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.L().Info("Config table migration needed, starting migration")
|
||||||
|
|
||||||
|
// Migrate existing config data
|
||||||
|
if err := dbpkg.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
// Read existing config with old structure
|
||||||
|
var oldConfig struct {
|
||||||
|
ID int `gorm:"column:id"`
|
||||||
|
Timeout int `gorm:"column:timeout"`
|
||||||
|
SSHCopy bool `gorm:"column:ssh_copy"`
|
||||||
|
SSHPaste bool `gorm:"column:ssh_paste"`
|
||||||
|
RDPCopy bool `gorm:"column:rdp_copy"`
|
||||||
|
RDPPaste bool `gorm:"column:rdp_paste"`
|
||||||
|
VNCCopy bool `gorm:"column:vnc_copy"`
|
||||||
|
VNCPaste bool `gorm:"column:vnc_paste"`
|
||||||
|
CreatorId int `gorm:"column:creator_id"`
|
||||||
|
UpdaterId int `gorm:"column:updater_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Table("config").Where("deleted_at = 0").First(&oldConfig).Error; err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
// No existing config, create default
|
||||||
|
logger.L().Info("No existing config found, will create default after migration")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert old config to new structure
|
||||||
|
// Use logical OR for permissions - if any protocol allowed it, then allow it
|
||||||
|
newConfig := &model.Config{
|
||||||
|
Id: oldConfig.ID,
|
||||||
|
Timeout: oldConfig.Timeout,
|
||||||
|
DefaultPermissions: model.DefaultPermissions{
|
||||||
|
Connect: true, // Always allow connect
|
||||||
|
FileUpload: true, // Default to allow file operations
|
||||||
|
FileDownload: true,
|
||||||
|
Copy: oldConfig.SSHCopy || oldConfig.RDPCopy || oldConfig.VNCCopy,
|
||||||
|
Paste: oldConfig.SSHPaste || oldConfig.RDPPaste || oldConfig.VNCPaste,
|
||||||
|
Share: false, // Default to deny share for security
|
||||||
|
},
|
||||||
|
CreatorId: oldConfig.CreatorId,
|
||||||
|
UpdaterId: oldConfig.UpdaterId,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store converted config data temporarily
|
||||||
|
if err := tx.Exec("CREATE TEMPORARY TABLE temp_config AS SELECT * FROM config WHERE deleted_at = 0").Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.L().Info("Migrated config permissions",
|
||||||
|
zap.Bool("copy", newConfig.DefaultPermissions.Copy),
|
||||||
|
zap.Bool("paste", newConfig.DefaultPermissions.Paste))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
logger.L().Error("Failed to migrate config data", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop old columns and add new ones
|
||||||
|
if err := dbpkg.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
// Add new columns
|
||||||
|
alterSQL := `
|
||||||
|
ALTER TABLE config
|
||||||
|
ADD COLUMN default_connect BOOLEAN DEFAULT TRUE,
|
||||||
|
ADD COLUMN default_file_upload BOOLEAN DEFAULT TRUE,
|
||||||
|
ADD COLUMN default_file_download BOOLEAN DEFAULT TRUE,
|
||||||
|
ADD COLUMN default_copy BOOLEAN DEFAULT TRUE,
|
||||||
|
ADD COLUMN default_paste BOOLEAN DEFAULT TRUE,
|
||||||
|
ADD COLUMN default_share BOOLEAN DEFAULT FALSE
|
||||||
|
`
|
||||||
|
if err := tx.Exec(alterSQL).Error; err != nil {
|
||||||
|
logger.L().Error("Failed to add new columns", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update data from temporary table if it exists
|
||||||
|
updateSQL := `
|
||||||
|
UPDATE config SET
|
||||||
|
default_connect = TRUE,
|
||||||
|
default_file_upload = TRUE,
|
||||||
|
default_file_download = TRUE,
|
||||||
|
default_copy = COALESCE((SELECT (ssh_copy OR rdp_copy OR vnc_copy) FROM temp_config WHERE temp_config.id = config.id), TRUE),
|
||||||
|
default_paste = COALESCE((SELECT (ssh_paste OR rdp_paste OR vnc_paste) FROM temp_config WHERE temp_config.id = config.id), TRUE),
|
||||||
|
default_share = FALSE
|
||||||
|
WHERE deleted_at = 0
|
||||||
|
`
|
||||||
|
if err := tx.Exec(updateSQL).Error; err != nil {
|
||||||
|
logger.L().Error("Failed to update config data", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop old columns
|
||||||
|
dropSQL := `
|
||||||
|
ALTER TABLE config
|
||||||
|
DROP COLUMN ssh_copy,
|
||||||
|
DROP COLUMN ssh_paste,
|
||||||
|
DROP COLUMN rdp_copy,
|
||||||
|
DROP COLUMN rdp_paste,
|
||||||
|
DROP COLUMN rdp_enable_drive,
|
||||||
|
DROP COLUMN rdp_drive_path,
|
||||||
|
DROP COLUMN rdp_create_drive_path,
|
||||||
|
DROP COLUMN rdp_disable_upload,
|
||||||
|
DROP COLUMN rdp_disable_download,
|
||||||
|
DROP COLUMN vnc_copy,
|
||||||
|
DROP COLUMN vnc_paste
|
||||||
|
`
|
||||||
|
if err := tx.Exec(dropSQL).Error; err != nil {
|
||||||
|
logger.L().Error("Failed to drop old columns", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop temporary table
|
||||||
|
if err := tx.Exec("DROP TEMPORARY TABLE IF EXISTS temp_config").Error; err != nil {
|
||||||
|
logger.L().Warn("Failed to drop temporary table", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.L().Info("Config table structure migration completed successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureDefaultConfig ensures there's a default config in the database
|
||||||
|
func (s *ConfigService) EnsureDefaultConfig(ctx context.Context) error {
|
||||||
|
cfg, err := s.GetConfig(ctx)
|
||||||
|
if err != nil && err != gorm.ErrRecordNotFound {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg == nil {
|
||||||
|
// Create default config
|
||||||
|
defaultCfg := model.GetDefaultConfig()
|
||||||
|
defaultCfg.CreatorId = 1 // System user
|
||||||
|
defaultCfg.UpdaterId = 1
|
||||||
|
|
||||||
|
if err := s.SaveConfig(ctx, defaultCfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.L().Info("Created default config")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@@ -125,6 +125,82 @@ func (s *NodeService) BuildQuery(ctx *gin.Context, currentUser interface{}, info
|
|||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildQueryWithAuthorization builds query with integrated V2 authorization filter
|
||||||
|
func (s *NodeService) BuildQueryWithAuthorization(ctx *gin.Context) (*gorm.DB, error) {
|
||||||
|
// Start with base query filters (without authorization)
|
||||||
|
db := dbpkg.DB.Model(model.DefaultNode)
|
||||||
|
|
||||||
|
db = dbpkg.FilterEqual(ctx, db, "parent_id", "id")
|
||||||
|
db = dbpkg.FilterLike(ctx, db, "name")
|
||||||
|
db = dbpkg.FilterSearch(ctx, db, "name", "id")
|
||||||
|
|
||||||
|
// Handle IDs filter
|
||||||
|
if q, ok := ctx.GetQuery("ids"); ok {
|
||||||
|
db = db.Where("id IN ?", lo.Map(strings.Split(q, ","), func(s string, _ int) int { return cast.ToInt(s) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle no_self_child filter
|
||||||
|
if id, ok := ctx.GetQuery("no_self_child"); ok {
|
||||||
|
ids, err := s.handleNoSelfChild(ctx, cast.ToInt(id))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
db = db.Where("id IN ?", ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle self_parent filter
|
||||||
|
if id, ok := ctx.GetQuery("self_parent"); ok {
|
||||||
|
ids, err := repository.HandleSelfParent(ctx, cast.ToInt(id))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
db = db.Where("id IN ?", ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||||
|
|
||||||
|
// Administrators have access to all nodes
|
||||||
|
if acl.IsAdmin(currentUser) {
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply V2 authorization filter: get authorized asset IDs using V2 system
|
||||||
|
authV2Service := NewAuthorizationV2Service()
|
||||||
|
_, assetIds, _, err := authV2Service.GetAuthorizationScopeByACL(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the same logic as GetNodeIdsByAuthorization but more efficiently
|
||||||
|
if len(assetIds) == 0 {
|
||||||
|
// No access to any assets = no access to any nodes
|
||||||
|
db = db.Where("1 = 0")
|
||||||
|
} else {
|
||||||
|
// Get parent node IDs from authorized assets (same logic as before)
|
||||||
|
var parentIds []int
|
||||||
|
err = dbpkg.DB.Model(model.DefaultAsset).
|
||||||
|
Where("id IN ?", assetIds).
|
||||||
|
Distinct("parent_id").
|
||||||
|
Pluck("parent_id", &parentIds).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parentIds) == 0 {
|
||||||
|
db = db.Where("1 = 0")
|
||||||
|
} else {
|
||||||
|
// Include self and parent hierarchy for proper tree navigation
|
||||||
|
allNodeIds, err := repository.HandleSelfParent(ctx, parentIds...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
db = db.Where("id IN ?", allNodeIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
// AttachAssetCount attaches asset count to nodes
|
// AttachAssetCount attaches asset count to nodes
|
||||||
func (s *NodeService) AttachAssetCount(ctx *gin.Context, data []*model.Node) error {
|
func (s *NodeService) AttachAssetCount(ctx *gin.Context, data []*model.Node) error {
|
||||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||||
@@ -136,7 +212,9 @@ func (s *NodeService) AttachAssetCount(ctx *gin.Context, data []*model.Node) err
|
|||||||
if !acl.IsAdmin(currentUser) {
|
if !acl.IsAdmin(currentUser) {
|
||||||
info := cast.ToBool(ctx.Query("info"))
|
info := cast.ToBool(ctx.Query("info"))
|
||||||
if info {
|
if info {
|
||||||
_, assetIds, _, err := NewAssetService().GetAssetIdsByAuthorization(ctx)
|
// Use V2 authorization system for asset filtering
|
||||||
|
authV2Service := NewAuthorizationV2Service()
|
||||||
|
_, assetIds, _, err := authV2Service.GetAuthorizationScopeByACL(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -215,7 +293,9 @@ func (s *NodeService) AttachHasChild(ctx *gin.Context, data []*model.Node) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
if info {
|
if info {
|
||||||
_, assetIds, _, err := NewAssetService().GetAssetIdsByAuthorization(ctx)
|
// Use V2 authorization system for asset filtering
|
||||||
|
authV2Service := NewAuthorizationV2Service()
|
||||||
|
_, assetIds, _, err := authV2Service.GetAuthorizationScopeByACL(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -311,7 +391,9 @@ func (s *NodeService) CheckDependencies(ctx context.Context, id int) (string, er
|
|||||||
|
|
||||||
// GetNodeIdsByAuthorization gets node IDs that the user is authorized to access
|
// GetNodeIdsByAuthorization gets node IDs that the user is authorized to access
|
||||||
func (s *NodeService) GetNodeIdsByAuthorization(ctx *gin.Context) ([]int, error) {
|
func (s *NodeService) GetNodeIdsByAuthorization(ctx *gin.Context) ([]int, error) {
|
||||||
_, assetIds, _, err := NewAssetService().GetAssetIdsByAuthorization(ctx)
|
// Use V2 authorization system for asset filtering
|
||||||
|
authV2Service := NewAuthorizationV2Service()
|
||||||
|
_, assetIds, _, err := authV2Service.GetAuthorizationScopeByACL(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -425,8 +507,7 @@ func (s *NodeService) GetNodesTree(ctx *gin.Context, dbQuery *gorm.DB, needAcl b
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply postHooks only if info=false
|
// Apply postHooks - now always apply permission handling regardless of info mode
|
||||||
if !info {
|
|
||||||
if err := s.AttachAssetCount(ctx, allNodes); err != nil {
|
if err := s.AttachAssetCount(ctx, allNodes); err != nil {
|
||||||
logger.L().Error("failed to attach asset count", zap.Error(err))
|
logger.L().Error("failed to attach asset count", zap.Error(err))
|
||||||
}
|
}
|
||||||
@@ -435,10 +516,10 @@ func (s *NodeService) GetNodesTree(ctx *gin.Context, dbQuery *gorm.DB, needAcl b
|
|||||||
logger.L().Error("failed to attach has_child flag", zap.Error(err))
|
logger.L().Error("failed to attach has_child flag", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always handle permissions, regardless of info mode
|
||||||
if err := s.handleNodePermissions(ctx, allNodes, resourceType); err != nil {
|
if err := s.handleNodePermissions(ctx, allNodes, resourceType); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Build tree structure
|
// Build tree structure
|
||||||
return s.buildNodeTree(allNodes, nodeIds), nil
|
return s.buildNodeTree(allNodes, nodeIds), nil
|
||||||
@@ -487,10 +568,6 @@ func (s *NodeService) buildNodeTree(nodes []*model.Node, rootIds []int) []any {
|
|||||||
|
|
||||||
// handleNodePermissions handles node permissions
|
// handleNodePermissions handles node permissions
|
||||||
func (s *NodeService) handleNodePermissions(ctx *gin.Context, nodes []*model.Node, resourceType string) error {
|
func (s *NodeService) handleNodePermissions(ctx *gin.Context, nodes []*model.Node, resourceType string) error {
|
||||||
if info := cast.ToBool(ctx.Query("info")); info {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||||
|
|
||||||
if !lo.Contains(config.PermResource, resourceType) {
|
if !lo.Contains(config.PermResource, resourceType) {
|
||||||
|
333
backend/internal/service/time_template.go
Normal file
333
backend/internal/service/time_template.go
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/veops/oneterm/internal/model"
|
||||||
|
"github.com/veops/oneterm/internal/repository"
|
||||||
|
dbpkg "github.com/veops/oneterm/pkg/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TimeTemplateService handles business logic for time templates
|
||||||
|
type TimeTemplateService struct {
|
||||||
|
repo repository.ITimeTemplateRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTimeTemplateService creates a new time template service
|
||||||
|
func NewTimeTemplateService() *TimeTemplateService {
|
||||||
|
repo := repository.NewTimeTemplateRepository(dbpkg.DB)
|
||||||
|
return &TimeTemplateService{
|
||||||
|
repo: repo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildQuery builds the base query for time templates
|
||||||
|
func (s *TimeTemplateService) BuildQuery(ctx *gin.Context) (*gorm.DB, error) {
|
||||||
|
db := dbpkg.DB.Model(model.DefaultTimeTemplate)
|
||||||
|
|
||||||
|
// Apply search filter
|
||||||
|
if search := ctx.Query("search"); search != "" {
|
||||||
|
db = db.Where("name LIKE ? OR description LIKE ?", "%"+search+"%", "%"+search+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply category filter
|
||||||
|
if category := ctx.Query("category"); category != "" {
|
||||||
|
db = db.Where("category = ?", category)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply active filter
|
||||||
|
if activeStr := ctx.Query("active"); activeStr != "" {
|
||||||
|
active := activeStr == "true"
|
||||||
|
db = db.Where("is_active = ?", active)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTimeTemplate creates a new time template
|
||||||
|
func (s *TimeTemplateService) CreateTimeTemplate(ctx context.Context, template *model.TimeTemplate) error {
|
||||||
|
// Validate the template
|
||||||
|
if err := s.ValidateTimeTemplate(template); err != nil {
|
||||||
|
return fmt.Errorf("validation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate names
|
||||||
|
existing, err := s.repo.GetByName(ctx, template.Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check existing template: %w", err)
|
||||||
|
}
|
||||||
|
if existing != nil {
|
||||||
|
return errors.New("template with this name already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default values
|
||||||
|
if template.Timezone == "" {
|
||||||
|
template.Timezone = "Asia/Shanghai"
|
||||||
|
}
|
||||||
|
template.IsBuiltIn = false
|
||||||
|
template.UsageCount = 0
|
||||||
|
|
||||||
|
return s.repo.Create(ctx, template)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimeTemplate retrieves a time template by ID
|
||||||
|
func (s *TimeTemplateService) GetTimeTemplate(ctx context.Context, id int) (*model.TimeTemplate, error) {
|
||||||
|
return s.repo.GetByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimeTemplateByName retrieves a time template by name
|
||||||
|
func (s *TimeTemplateService) GetTimeTemplateByName(ctx context.Context, name string) (*model.TimeTemplate, error) {
|
||||||
|
return s.repo.GetByName(ctx, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListTimeTemplates retrieves time templates with pagination and filters
|
||||||
|
func (s *TimeTemplateService) ListTimeTemplates(ctx context.Context, offset, limit int, category string, active *bool) ([]*model.TimeTemplate, int64, error) {
|
||||||
|
return s.repo.List(ctx, offset, limit, category, active)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTimeTemplate updates an existing time template
|
||||||
|
func (s *TimeTemplateService) UpdateTimeTemplate(ctx context.Context, template *model.TimeTemplate) error {
|
||||||
|
// Validate the template
|
||||||
|
if err := s.ValidateTimeTemplate(template); err != nil {
|
||||||
|
return fmt.Errorf("validation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if template exists
|
||||||
|
existing, err := s.repo.GetByID(ctx, template.Id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get existing template: %w", err)
|
||||||
|
}
|
||||||
|
if existing == nil {
|
||||||
|
return errors.New("time template not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow changing built-in status
|
||||||
|
template.IsBuiltIn = existing.IsBuiltIn
|
||||||
|
template.UsageCount = existing.UsageCount
|
||||||
|
|
||||||
|
return s.repo.Update(ctx, template)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTimeTemplate deletes a time template
|
||||||
|
func (s *TimeTemplateService) DeleteTimeTemplate(ctx context.Context, id int) error {
|
||||||
|
return s.repo.Delete(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseTimeTemplate increments usage count and returns the template
|
||||||
|
func (s *TimeTemplateService) UseTimeTemplate(ctx context.Context, id int) (*model.TimeTemplate, error) {
|
||||||
|
template, err := s.repo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if template == nil {
|
||||||
|
return nil, errors.New("time template not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment usage count
|
||||||
|
if err := s.repo.IncrementUsage(ctx, id); err != nil {
|
||||||
|
// Log error but don't fail the operation
|
||||||
|
fmt.Printf("Failed to increment usage count for template %d: %v\n", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return template, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBuiltInTemplates retrieves all built-in time templates
|
||||||
|
func (s *TimeTemplateService) GetBuiltInTemplates(ctx context.Context) ([]*model.TimeTemplate, error) {
|
||||||
|
return s.repo.GetBuiltInTemplates(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitializeBuiltInTemplates initializes built-in time templates
|
||||||
|
func (s *TimeTemplateService) InitializeBuiltInTemplates(ctx context.Context) error {
|
||||||
|
return s.repo.InitBuiltInTemplates(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateTimeTemplate validates a time template
|
||||||
|
func (s *TimeTemplateService) ValidateTimeTemplate(template *model.TimeTemplate) error {
|
||||||
|
if template.Name == "" {
|
||||||
|
return errors.New("template name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(template.Name) > 128 {
|
||||||
|
return errors.New("template name too long (max 128 characters)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if template.Category == "" {
|
||||||
|
return errors.New("template category is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate category
|
||||||
|
validCategories := []string{"work", "duty", "maintenance", "emergency", "always", "custom"}
|
||||||
|
categoryValid := false
|
||||||
|
for _, cat := range validCategories {
|
||||||
|
if template.Category == cat {
|
||||||
|
categoryValid = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !categoryValid {
|
||||||
|
return fmt.Errorf("invalid category: %s. Valid categories: %v", template.Category, validCategories)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate timezone
|
||||||
|
if template.Timezone != "" {
|
||||||
|
if _, err := time.LoadLocation(template.Timezone); err != nil {
|
||||||
|
return fmt.Errorf("invalid timezone: %s", template.Timezone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate time ranges
|
||||||
|
if len(template.TimeRanges) == 0 {
|
||||||
|
return errors.New("at least one time range is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, timeRange := range template.TimeRanges {
|
||||||
|
if err := s.validateTimeRange(timeRange); err != nil {
|
||||||
|
return fmt.Errorf("invalid time range %d: %w", i+1, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateTimeRange validates a single time range
|
||||||
|
func (s *TimeTemplateService) validateTimeRange(timeRange model.TimeRange) error {
|
||||||
|
// Validate time format (HH:MM)
|
||||||
|
timePattern := regexp.MustCompile(`^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$`)
|
||||||
|
|
||||||
|
if !timePattern.MatchString(timeRange.StartTime) {
|
||||||
|
return fmt.Errorf("invalid start time format: %s (expected HH:MM)", timeRange.StartTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !timePattern.MatchString(timeRange.EndTime) {
|
||||||
|
return fmt.Errorf("invalid end time format: %s (expected HH:MM)", timeRange.EndTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and validate time logic
|
||||||
|
startMinutes, err := s.parseTimeToMinutes(timeRange.StartTime)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid start time: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
endMinutes, err := s.parseTimeToMinutes(timeRange.EndTime)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid end time: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if startMinutes >= endMinutes {
|
||||||
|
return errors.New("start time must be before end time")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate weekdays
|
||||||
|
if len(timeRange.Weekdays) == 0 {
|
||||||
|
return errors.New("at least one weekday must be specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, day := range timeRange.Weekdays {
|
||||||
|
if day < 1 || day > 7 {
|
||||||
|
return fmt.Errorf("invalid weekday: %d (must be 1-7, where 1=Monday, 7=Sunday)", day)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseTimeToMinutes converts HH:MM format to minutes since midnight
|
||||||
|
func (s *TimeTemplateService) parseTimeToMinutes(timeStr string) (int, error) {
|
||||||
|
parts := strings.Split(timeStr, ":")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return 0, errors.New("invalid time format")
|
||||||
|
}
|
||||||
|
|
||||||
|
hour, err := strconv.Atoi(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
minute, err := strconv.Atoi(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return hour*60 + minute, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckTimeAccess checks if current time is within the template's allowed time ranges
|
||||||
|
func (s *TimeTemplateService) CheckTimeAccess(ctx context.Context, templateID int, timezone string) (bool, error) {
|
||||||
|
template, err := s.repo.GetByID(ctx, templateID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if template == nil {
|
||||||
|
return false, errors.New("time template not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.IsTimeInTemplate(template, timezone), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTimeInTemplate checks if current time matches any time range in the template
|
||||||
|
func (s *TimeTemplateService) IsTimeInTemplate(template *model.TimeTemplate, timezone string) bool {
|
||||||
|
// Use template's timezone if not specified
|
||||||
|
if timezone == "" {
|
||||||
|
timezone = template.Timezone
|
||||||
|
}
|
||||||
|
if timezone == "" {
|
||||||
|
timezone = "Asia/Shanghai" // Default timezone
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load timezone location
|
||||||
|
loc, err := time.LoadLocation(timezone)
|
||||||
|
if err != nil {
|
||||||
|
// Fall back to UTC if timezone is invalid
|
||||||
|
loc = time.UTC
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().In(loc)
|
||||||
|
currentWeekday := int(now.Weekday())
|
||||||
|
if currentWeekday == 0 {
|
||||||
|
currentWeekday = 7 // Convert Sunday from 0 to 7
|
||||||
|
}
|
||||||
|
|
||||||
|
currentMinutes := now.Hour()*60 + now.Minute()
|
||||||
|
|
||||||
|
// Check each time range
|
||||||
|
for _, timeRange := range template.TimeRanges {
|
||||||
|
// Check if current weekday is in the allowed weekdays
|
||||||
|
weekdayMatch := false
|
||||||
|
for _, day := range timeRange.Weekdays {
|
||||||
|
if day == currentWeekday {
|
||||||
|
weekdayMatch = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !weekdayMatch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if current time is in the allowed time range
|
||||||
|
startMinutes, err := s.parseTimeToMinutes(timeRange.StartTime)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
endMinutes, err := s.parseTimeToMinutes(timeRange.EndTime)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentMinutes >= startMinutes && currentMinutes <= endMinutes {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
@@ -243,7 +243,10 @@ func (m *view) refresh() {
|
|||||||
}
|
}
|
||||||
if !acl.IsAdmin(m.currentUser) {
|
if !acl.IsAdmin(m.currentUser) {
|
||||||
var assetIds, accountIds []int
|
var assetIds, accountIds []int
|
||||||
if _, assetIds, _, err = service.NewAssetService().GetAssetIdsByAuthorization(m.Ctx); err != nil {
|
|
||||||
|
// Use V2 authorization system for asset filtering
|
||||||
|
authV2Service := service.NewAuthorizationV2Service()
|
||||||
|
if _, assetIds, _, err = authV2Service.GetAuthorizationScopeByACL(m.Ctx); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
assets = lo.Filter(assets, func(a *model.Asset, _ int) bool { return lo.Contains(assetIds, a.Id) })
|
assets = lo.Filter(assets, func(a *model.Asset, _ int) bool { return lo.Contains(assetIds, a.Id) })
|
||||||
@@ -258,11 +261,17 @@ func (m *view) refresh() {
|
|||||||
|
|
||||||
m.combines = make(map[string][3]int)
|
m.combines = make(map[string][3]int)
|
||||||
for _, asset := range assets {
|
for _, asset := range assets {
|
||||||
for accountId := range asset.Authorization {
|
for accountId, authData := range asset.Authorization {
|
||||||
account, ok := accountMap[accountId]
|
account, ok := accountMap[accountId]
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this account has connect permission
|
||||||
|
if authData.Permissions == nil || !authData.Permissions.Connect {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
for _, p := range asset.Protocols {
|
for _, p := range asset.Protocols {
|
||||||
ss := strings.Split(p, ":")
|
ss := strings.Split(p, ":")
|
||||||
if len(ss) != 2 {
|
if len(ss) != 2 {
|
||||||
|
Reference in New Issue
Block a user