mirror of
https://github.com/veops/oneterm.git
synced 2025-10-05 15:27:01 +08:00
refactor(backend): authorization v2
This commit is contained in:
@@ -47,6 +47,8 @@ require (
|
||||
gorm.io/plugin/soft_delete v1.2.1
|
||||
)
|
||||
|
||||
require github.com/stretchr/testify v1.9.0
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-pipeline-go v0.2.3 // 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/windows v0.1.0 // 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/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // 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/cancelreader v0.2.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/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // 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) {
|
||||
token, err := remote.GetAclToken(ctx)
|
||||
if err != nil {
|
||||
|
@@ -28,11 +28,12 @@ func initDB() {
|
||||
cfg := db.ConfigFromGlobal()
|
||||
|
||||
if err := db.Init(cfg, true,
|
||||
model.DefaultAccount, model.DefaultAsset, model.DefaultAuthorization, model.DefaultCommand,
|
||||
model.DefaultConfig, model.DefaultFileHistory, model.DefaultGateway, model.DefaultHistory,
|
||||
model.DefaultNode, model.DefaultPublicKey, model.DefaultSession, model.DefaultSessionCmd,
|
||||
model.DefaultShare, model.DefaultQuickCommand, model.DefaultUserPreference,
|
||||
model.DefaultStorageConfig, model.DefaultStorageMetrics,
|
||||
model.DefaultAccount, model.DefaultAsset, model.DefaultAuthorization, model.DefaultAuthorizationV2,
|
||||
model.DefaultCommand, model.DefaultCommandTemplate, model.DefaultConfig, model.DefaultFileHistory,
|
||||
model.DefaultGateway, model.DefaultHistory, model.DefaultNode, model.DefaultPublicKey,
|
||||
model.DefaultSession, model.DefaultSessionCmd, model.DefaultShare, model.DefaultQuickCommand,
|
||||
model.DefaultUserPreference, model.DefaultStorageConfig, model.DefaultStorageMetrics,
|
||||
model.DefaultTimeTemplate, model.DefaultMigrationRecord,
|
||||
); err != nil {
|
||||
logger.L().Fatal("Failed to init database", zap.Error(err))
|
||||
}
|
||||
@@ -42,6 +43,7 @@ func initDB() {
|
||||
}
|
||||
|
||||
acl.MigrateNode()
|
||||
acl.MigrateCommand()
|
||||
|
||||
gsession.InitSessionCleanup()
|
||||
}
|
||||
@@ -49,6 +51,17 @@ func initDB() {
|
||||
func initServices() {
|
||||
service.InitAuthorizationService()
|
||||
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 {
|
||||
|
@@ -7,7 +7,6 @@ import (
|
||||
"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/service"
|
||||
"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}}
|
||||
// @Router /account [get]
|
||||
func (c *Controller) GetAccounts(ctx *gin.Context) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
info := cast.ToBool(ctx.Query("info"))
|
||||
|
||||
// Build base query using service layer
|
||||
db := accountService.BuildQuery(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)
|
||||
// Build query with integrated V2 authorization filter
|
||||
db, err := accountService.BuildQueryWithAuthorization(ctx)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
// Filter accounts by asset IDs
|
||||
db = accountService.FilterByAssetIds(db, assetIds)
|
||||
}
|
||||
// Apply info mode settings
|
||||
if info {
|
||||
db = db.Select("id", "name", "account")
|
||||
}
|
||||
|
||||
doGet(ctx, !info, db, config.RESOURCE_ACCOUNT, accountPostHooks...)
|
||||
|
@@ -2,12 +2,14 @@ package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/spf13/cast"
|
||||
"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/service"
|
||||
"github.com/veops/oneterm/pkg/config"
|
||||
@@ -39,19 +41,6 @@ var (
|
||||
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}}
|
||||
// @Router /asset [get]
|
||||
func (c *Controller) GetAssets(ctx *gin.Context) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
info := cast.ToBool(ctx.Query("info"))
|
||||
|
||||
// Build base query using service layer
|
||||
db, err := assetService.BuildQuery(ctx)
|
||||
// Build query with integrated V2 authorization filter
|
||||
db, err := assetService.BuildQueryWithAuthorization(ctx)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &errors.ApiError{Code: errors.ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
@@ -127,24 +115,116 @@ func (c *Controller) GetAssets(ctx *gin.Context) {
|
||||
|
||||
// Apply info mode settings
|
||||
if info {
|
||||
db = db.Select("id", "parent_id", "name", "ip", "protocols", "connectable", "authorization")
|
||||
|
||||
// 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)
|
||||
}
|
||||
db = db.Select("id", "parent_id", "name", "ip", "protocols", "connectable", "authorization", "resource_id", "access_time_control", "asset_command_control")
|
||||
}
|
||||
|
||||
doGet(ctx, !info, db, config.RESOURCE_ASSET, assetPostHooks...)
|
||||
doGet(ctx, false, db, config.RESOURCE_ASSET, assetPostHooks...)
|
||||
}
|
||||
|
||||
// GetAssetIdsByAuthorization gets asset IDs by authorization
|
||||
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
|
||||
}
|
||||
|
||||
// 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/model"
|
||||
"github.com/veops/oneterm/internal/service"
|
||||
gsession "github.com/veops/oneterm/internal/session"
|
||||
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...)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
authorizationIds, ok := ctx.Value(kAuthorizationIds).([]*model.AuthorizationIds)
|
||||
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) {
|
||||
if info := cast.ToBool(ctx.Query("info")); info {
|
||||
return
|
||||
}
|
||||
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
|
||||
if !lo.Contains(config.PermResource, resourceTypeName) {
|
||||
|
@@ -54,6 +54,7 @@ func (c *Controller) UpdateCommandTemplate(ctx *gin.Context) {
|
||||
// @Tags command_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 builtin query bool false "filter by builtin status"
|
||||
// @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) {
|
||||
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)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &pkgErrors.ApiError{Code: pkgErrors.ErrInternal, Data: map[string]any{"err": err}})
|
||||
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...)
|
||||
}
|
||||
|
||||
|
@@ -63,10 +63,14 @@ func (c *Controller) GetConfig(ctx *gin.Context) {
|
||||
|
||||
cfg, err := configService.GetConfig(ctx)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// Return default configuration if no config exists
|
||||
defaultCfg := model.GetDefaultConfig()
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(defaultCfg))
|
||||
return
|
||||
}
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(cfg))
|
||||
|
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/veops/oneterm/internal/acl"
|
||||
"github.com/veops/oneterm/internal/guacd"
|
||||
"github.com/veops/oneterm/internal/model"
|
||||
"github.com/veops/oneterm/internal/service"
|
||||
fileservice "github.com/veops/oneterm/internal/service/file"
|
||||
gsession "github.com/veops/oneterm/internal/session"
|
||||
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}})
|
||||
return
|
||||
} else if !ok {
|
||||
} else if !result.IsAllowed(model.ActionConnect) {
|
||||
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{}})
|
||||
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}})
|
||||
return
|
||||
} else if !ok {
|
||||
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{}})
|
||||
} else if !result.IsAllowed(model.ActionFileUpload) {
|
||||
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"message": "No file upload permission"}})
|
||||
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}})
|
||||
return
|
||||
} else if !ok {
|
||||
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{}})
|
||||
} else if !result.IsAllowed(model.ActionFileUpload) {
|
||||
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"message": "No file upload permission"}})
|
||||
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}})
|
||||
return
|
||||
} else if !ok {
|
||||
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{}})
|
||||
} else if !result.IsAllowed(model.ActionFileDownload) {
|
||||
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"message": "No file download permission"}})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -431,7 +436,7 @@ func (c *Controller) RDPFileList(ctx *gin.Context) {
|
||||
sessionId := ctx.Param("session_id")
|
||||
path := ctx.DefaultQuery("path", "/")
|
||||
|
||||
tunnel, err := c.validateRDPAccess(ctx, sessionId)
|
||||
tunnel, authResult, err := c.validateRDPAccess(ctx, sessionId)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "permission") {
|
||||
ctx.JSON(http.StatusForbidden, HttpResponse{
|
||||
@@ -447,6 +452,9 @@ func (c *Controller) RDPFileList(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Connect permission already validated in validateRDPAccess
|
||||
_ = authResult // Use the already validated result
|
||||
|
||||
// Check if RDP drive is enabled
|
||||
if !fileservice.IsRDPDriveEnabled(tunnel) {
|
||||
ctx.JSON(http.StatusBadRequest, HttpResponse{
|
||||
@@ -503,7 +511,7 @@ func (c *Controller) RDPFileUpload(ctx *gin.Context) {
|
||||
// Create progress record IMMEDIATELY when request starts
|
||||
fileservice.CreateTransferProgress(transferId, "rdp")
|
||||
|
||||
tunnel, err := c.validateRDPAccess(ctx, sessionId)
|
||||
tunnel, authResult, err := c.validateRDPAccess(ctx, sessionId)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "permission") {
|
||||
ctx.JSON(http.StatusForbidden, HttpResponse{
|
||||
@@ -519,6 +527,15 @@ func (c *Controller) RDPFileUpload(ctx *gin.Context) {
|
||||
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) {
|
||||
logger.L().Error("RDP drive is not enabled for session", zap.String("sessionId", sessionId))
|
||||
ctx.JSON(http.StatusBadRequest, HttpResponse{
|
||||
@@ -726,7 +743,7 @@ func (c *Controller) RDPFileUpload(ctx *gin.Context) {
|
||||
func (c *Controller) RDPFileDownload(ctx *gin.Context) {
|
||||
sessionId := ctx.Param("session_id")
|
||||
|
||||
tunnel, validationErr := c.validateRDPAccess(ctx, sessionId)
|
||||
tunnel, authResult, validationErr := c.validateRDPAccess(ctx, sessionId)
|
||||
if validationErr != nil {
|
||||
if strings.Contains(validationErr.Error(), "permission") {
|
||||
ctx.JSON(http.StatusForbidden, HttpResponse{
|
||||
@@ -742,6 +759,15 @@ func (c *Controller) RDPFileDownload(ctx *gin.Context) {
|
||||
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) {
|
||||
ctx.JSON(http.StatusForbidden, HttpResponse{
|
||||
Code: http.StatusForbidden,
|
||||
@@ -876,7 +902,7 @@ func (c *Controller) RDPFileMkdir(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
tunnel, validateErr := c.validateRDPAccess(ctx, sessionId)
|
||||
tunnel, authResult, validateErr := c.validateRDPAccess(ctx, sessionId)
|
||||
if validateErr != nil {
|
||||
if strings.Contains(validateErr.Error(), "permission") {
|
||||
ctx.JSON(http.StatusForbidden, HttpResponse{
|
||||
@@ -892,6 +918,15 @@ func (c *Controller) RDPFileMkdir(ctx *gin.Context) {
|
||||
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)
|
||||
if !fileservice.IsRDPUploadAllowed(tunnel) {
|
||||
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)
|
||||
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)
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
// Check authorization using the same logic as legacy API
|
||||
if ok, err := hasAuthorization(ctx, onlineSession); err != nil {
|
||||
// Check connect permission for SFTP file listing
|
||||
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}})
|
||||
return
|
||||
} else if !ok {
|
||||
} else if !result.IsAllowed(model.ActionConnect) {
|
||||
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{}})
|
||||
return
|
||||
}
|
||||
@@ -1073,12 +1122,12 @@ func (c *Controller) SftpFileMkdir(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check authorization using the same logic as legacy API
|
||||
if ok, err := hasAuthorization(ctx, onlineSession); err != nil {
|
||||
// Check file upload permission for SFTP mkdir
|
||||
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}})
|
||||
return
|
||||
} else if !ok {
|
||||
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{}})
|
||||
} else if !result.IsAllowed(model.ActionFileUpload) {
|
||||
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"message": "No file upload permission"}})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1239,11 +1288,12 @@ func (c *Controller) SftpFileUpload(ctx *gin.Context) {
|
||||
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}})
|
||||
return
|
||||
} else if !ok {
|
||||
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{}})
|
||||
} else if !result.IsAllowed(model.ActionFileUpload) {
|
||||
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"message": "No file upload permission"}})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1444,12 +1494,12 @@ func (c *Controller) SftpFileDownload(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check authorization using the same logic as legacy API
|
||||
if ok, err := hasAuthorization(ctx, onlineSession); err != nil {
|
||||
// Check file download permission for SFTP download
|
||||
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}})
|
||||
return
|
||||
} else if !ok {
|
||||
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{}})
|
||||
} else if !result.IsAllowed(model.ActionFileDownload) {
|
||||
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"message": "No file download permission"}})
|
||||
return
|
||||
}
|
||||
|
||||
|
@@ -111,7 +111,9 @@ func (c *Controller) GetGateways(ctx *gin.Context) {
|
||||
|
||||
// Apply authorization filter if needed
|
||||
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 {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &errors.ApiError{Code: errors.ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
|
@@ -8,7 +8,6 @@ import (
|
||||
"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"
|
||||
"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}}
|
||||
// @Router /node [get]
|
||||
func (c *Controller) GetNodes(ctx *gin.Context) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
info := cast.ToBool(ctx.Query("info"))
|
||||
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 {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &errors.ApiError{Code: errors.ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
// Apply info mode settings
|
||||
if info {
|
||||
db = db.Select("id", "parent_id", "name")
|
||||
}
|
||||
|
||||
if recursive {
|
||||
treeNodes, err := nodeService.GetNodesTree(ctx, db, !info, config.RESOURCE_NODE)
|
||||
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.PUT("/:id", c.UpdateAsset)
|
||||
asset.GET("", c.GetAssets)
|
||||
asset.GET("/:id/permissions", c.GetAssetPermissions)
|
||||
}
|
||||
|
||||
node := v1.Group("node")
|
||||
@@ -147,10 +148,21 @@ func SetupRouter(r *gin.Engine) {
|
||||
authorization := v1.Group("/authorization")
|
||||
{
|
||||
authorization.POST("", c.UpsertAuthorization)
|
||||
authorization.DELETE("/:id", c.DeleteAccount)
|
||||
authorization.DELETE("/:id", c.DeleteAuthorization)
|
||||
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.POST("", c.CreateQuickCommand)
|
||||
@@ -189,5 +201,28 @@ func SetupRouter(r *gin.Engine) {
|
||||
storage.PUT("/configs/:id/set-primary", c.SetPrimaryStorage)
|
||||
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"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"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.Protocol = sess.Protocol
|
||||
|
||||
sessionService := service.NewSessionService()
|
||||
cmds, err := sessionService.GetSshParserCommands(ctx, []int(asset.AccessAuth.CmdIds))
|
||||
// Use V2 command analyzer instead of legacy method
|
||||
commandAnalyzer := service.NewCommandAnalyzer()
|
||||
cmds, err := commandAnalyzer.AnalyzeSessionCommands(ctx, sess)
|
||||
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
|
||||
|
||||
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 {
|
||||
return sess, err
|
||||
}
|
||||
}
|
||||
if sess.SessionType == model.SESSIONTYPE_WEB {
|
||||
switch sess.SessionType {
|
||||
case model.SESSIONTYPE_WEB:
|
||||
sess.ClientIp = ctx.ClientIP()
|
||||
} else if sess.SessionType == model.SESSIONTYPE_CLIENT {
|
||||
case model.SESSIONTYPE_CLIENT:
|
||||
sess.ClientIp = ctx.RemoteIP()
|
||||
}
|
||||
|
||||
if !protocols.CheckTime(asset.AccessAuth) {
|
||||
err = &myErrors.ApiError{Code: myErrors.ErrAccessTime}
|
||||
return
|
||||
// V2 authorization check - determine required permissions based on protocol
|
||||
protocol := strings.Split(sess.Protocol, ":")[0]
|
||||
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}}
|
||||
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"}}
|
||||
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":
|
||||
go protocols.ConnectSsh(ctx, sess, asset, account, gateway)
|
||||
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.UpsertSession(sess)
|
||||
|
||||
// Initialize session-based file client for high-performance file operations
|
||||
// Only for SSH-based protocols that support SFTP
|
||||
protocol := strings.Split(sess.Protocol, ":")[0]
|
||||
if protocol == "ssh" {
|
||||
// Initialize session-based file client only for SSH and only if user has file permissions
|
||||
switch protocol {
|
||||
case "ssh":
|
||||
if hasFilePermissions {
|
||||
if err := fileservice.DefaultFileService.InitSessionFileClient(sess.SessionId, sess.AssetId, sess.AccountId); err != nil {
|
||||
logger.L().Warn("Failed to initialize session file client",
|
||||
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("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",
|
||||
zap.String("protocol", protocol),
|
||||
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"))
|
||||
|
||||
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 {
|
||||
logger.L().Error("guacd tunnel failed", zap.Error(err))
|
||||
return
|
||||
@@ -135,7 +156,8 @@ func MonitGuacd(ctx *gin.Context, sess *gsession.Session, chs *gsession.SessionC
|
||||
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 {
|
||||
logger.L().Error("guacd tunnel failed", zap.Error(err))
|
||||
return
|
||||
|
@@ -34,6 +34,14 @@ const (
|
||||
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 {
|
||||
Protocol string
|
||||
Parameters map[string]string
|
||||
@@ -57,7 +65,7 @@ type Tunnel struct {
|
||||
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
|
||||
if strings.Contains(config.Cfg.Guacd.Host, ":") {
|
||||
// IPv6 address
|
||||
@@ -70,9 +78,34 @@ func NewTunnel(connectionId, sessionId string, w, h, dpi int, protocol string, a
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ss := strings.Split(protocol, ":")
|
||||
protocol, port := ss[0], ss[1]
|
||||
cfg := model.GlobalConfig.Load()
|
||||
// Find the port for the protocol from asset.Protocols
|
||||
var port string
|
||||
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{
|
||||
conn: conn,
|
||||
reader: bufio.NewReader(conn),
|
||||
@@ -84,7 +117,7 @@ func NewTunnel(connectionId, sessionId string, w, h, dpi int, protocol string, a
|
||||
Parameters: lo.TernaryF(
|
||||
connectionId == "",
|
||||
func() map[string]string {
|
||||
return map[string]string{
|
||||
params := map[string]string{
|
||||
"version": VERSION,
|
||||
"client-name": "OneTerm",
|
||||
"recording-path": RECORDING_PATH,
|
||||
@@ -98,22 +131,36 @@ func NewTunnel(connectionId, sessionId string, w, h, dpi int, protocol string, a
|
||||
"port": port,
|
||||
"username": account.Account,
|
||||
"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",
|
||||
// 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 {
|
||||
return map[string]string{
|
||||
"width": cast.ToString(w),
|
||||
|
@@ -242,4 +242,370 @@ var (
|
||||
One: "\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]
|
||||
one = "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]
|
||||
hash = "sha1-fd11d7d098d05415f5ed082abdf31223cb2aeda9"
|
||||
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
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"gorm.io/plugin/soft_delete"
|
||||
@@ -10,6 +12,104 @@ const (
|
||||
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 {
|
||||
Id int `json:"id" gorm:"column:id;primarykey;autoIncrement"`
|
||||
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"`
|
||||
Protocols Slice[string] `json:"protocols" gorm:"column:protocols;type:text"`
|
||||
GatewayId int `json:"gateway_id" gorm:"column:gateway_id"`
|
||||
Authorization Map[int, Slice[int]] `json:"authorization" gorm:"column:authorization;type:text"`
|
||||
AccessAuth AccessAuth `json:"access_auth" gorm:"embedded;column:access_auth"`
|
||||
Authorization AuthorizationMap `json:"authorization" gorm:"column:authorization;type:text"`
|
||||
AccessAuth AccessAuth `json:"access_auth" gorm:"embedded;column:access_auth"` // Deprecated: Use V2 fields below
|
||||
Connectable bool `json:"connectable" gorm:"column:connectable"`
|
||||
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:"-"`
|
||||
ResourceId int `json:"resource_id" gorm:"column:resource_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"`
|
||||
}
|
||||
|
||||
// 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 {
|
||||
Start *time.Time `json:"start,omitempty" gorm:"column:start"`
|
||||
End *time.Time `json:"end,omitempty" gorm:"column:end"`
|
||||
@@ -40,6 +162,8 @@ type AccessAuth struct {
|
||||
Allow bool `json:"allow" gorm:"column:allow"`
|
||||
}
|
||||
|
||||
// AccessTimeControl and AssetCommandControl are defined in authorization_v2.go
|
||||
|
||||
type Range struct {
|
||||
Week int `json:"week" gorm:"column:week"`
|
||||
Times Slice[string] `json:"times" gorm:"column:times"`
|
||||
|
@@ -75,12 +75,12 @@ type AssetInfo struct {
|
||||
Protocols Slice[string] `json:"protocols" gorm:"column:protocols"`
|
||||
Connectable bool `json:"connectable" gorm:"column:connectable"`
|
||||
NodeChain string `json:"node_chain" gorm:"-"`
|
||||
*AccessAuth `json:"access_auth" gorm:"column:access_auth"`
|
||||
Authorization Map[int, Slice[int]] `json:"-" gorm:"column:authorization"`
|
||||
GatewayId int `json:"-" gorm:"column:gateway_id"`
|
||||
Gateway *GatewayInfo `json:"gateway,omitempty" gorm:"-"`
|
||||
Accounts []*AccountInfo `json:"accounts" gorm:"-"`
|
||||
Commands []*CmdInfo `json:"commands" gorm:"-"`
|
||||
*AccessAuth `json:"access_auth" gorm:"column:access_auth"`
|
||||
}
|
||||
|
||||
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]
|
||||
)
|
||||
|
||||
type SshConfig struct {
|
||||
Copy bool `json:"copy" gorm:"column:copy"`
|
||||
Paste bool `json:"paste" gorm:"column:paste"`
|
||||
}
|
||||
type RdpConfig struct {
|
||||
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 {
|
||||
// DefaultPermissions defines default permissions for authorization
|
||||
type DefaultPermissions 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"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Id int `json:"id" gorm:"column:id;primarykey;autoIncrement"`
|
||||
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"`
|
||||
VncConfig VncConfig `json:"vnc_config" gorm:"embedded;embeddedPrefix:vnc_;column:vnc_config"`
|
||||
|
||||
// Default permissions for authorization creation
|
||||
DefaultPermissions DefaultPermissions `json:"default_permissions" gorm:"embedded;embeddedPrefix:default_"`
|
||||
|
||||
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
|
||||
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
|
||||
@@ -47,6 +39,23 @@ func (m *Config) TableName() string {
|
||||
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
|
||||
type ScheduleConfig struct {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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{}
|
||||
DefaultAuthorization = &Authorization{}
|
||||
DefaultCommand = &Command{}
|
||||
DefaultCommandTemplate = &CommandTemplate{}
|
||||
DefaultConfig = &Config{}
|
||||
DefaultFileHistory = &FileHistory{}
|
||||
DefaultGateway = &Gateway{}
|
||||
@@ -18,4 +19,5 @@ var (
|
||||
DefaultUserPreference = &UserPreference{}
|
||||
DefaultStorageConfig = &StorageConfig{}
|
||||
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 {
|
||||
GetById(ctx context.Context, id int) (*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)
|
||||
FilterByParentId(db *gorm.DB, parentId int) (*gorm.DB, 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
|
||||
}
|
||||
|
||||
// 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
|
||||
func (r *assetRepository) GetAssetIdsByAuthorization(ctx *gin.Context, authorizationIds []*model.AuthorizationIds) ([]int, []int, []int, error) {
|
||||
ctx.Set(kAuthorizationIds, authorizationIds)
|
||||
|
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/veops/oneterm/internal/model"
|
||||
@@ -50,7 +49,6 @@ func (r *AuthorizationRepository) UpsertAuthorization(ctx context.Context, auth
|
||||
return r.db.Save(auth).Error
|
||||
}
|
||||
|
||||
// GetAuthorizationById 根据ID获取授权
|
||||
func (r *AuthorizationRepository) GetAuthorizationById(ctx context.Context, id int) (*model.Authorization, error) {
|
||||
auth := &model.Authorization{}
|
||||
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
|
||||
}
|
||||
|
||||
// GetAuthorizationByFields 根据字段获取授权
|
||||
func (r *AuthorizationRepository) GetAuthorizationByFields(ctx context.Context, nodeId, assetId, accountId int) (*model.Authorization, error) {
|
||||
auth := &model.Authorization{}
|
||||
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) {
|
||||
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{}).
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
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))
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/veops/oneterm/internal/acl"
|
||||
"github.com/veops/oneterm/internal/model"
|
||||
"github.com/veops/oneterm/internal/repository"
|
||||
"github.com/veops/oneterm/pkg/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) {
|
||||
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/samber/lo"
|
||||
"github.com/veops/oneterm/internal/acl"
|
||||
"github.com/veops/oneterm/internal/model"
|
||||
"github.com/veops/oneterm/internal/repository"
|
||||
"github.com/veops/oneterm/internal/schedule"
|
||||
@@ -34,7 +35,32 @@ func (s *AssetService) PreprocessAssetData(asset *model.Asset) {
|
||||
asset.Ip = strings.TrimSpace(asset.Ip)
|
||||
asset.Protocols = lo.Map(asset.Protocols, func(s string, _ int) string { return strings.TrimSpace(s) })
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
func (s *AssetService) BuildQuery(ctx *gin.Context) (*gorm.DB, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
authorizationIds, err := DefaultAuthService.GetAuthorizationIds(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return s.repo.GetAssetIdsByAuthorization(ctx, authorizationIds)
|
||||
// Use efficient V2 method: get authorized resource IDs from ACL, then find V2 rules
|
||||
authV2Service := NewAuthorizationV2Service()
|
||||
return authV2Service.GetAuthorizationScopeByACL(ctx)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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 (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
@@ -31,10 +34,23 @@ var (
|
||||
// InitAuthorizationService initializes the global authorization service
|
||||
func InitAuthorizationService() {
|
||||
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 {
|
||||
// V1 methods
|
||||
UpsertAuthorization(ctx context.Context, auth *model.Authorization) error
|
||||
UpsertAuthorizationWithTx(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
|
||||
GetNodeAssetAccountIdsByAction(ctx context.Context, action string) (nodeIds, assetIds, accountIds []int, err 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 {
|
||||
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{
|
||||
repo: repo,
|
||||
matcher: matcher,
|
||||
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)
|
||||
|
||||
// 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)
|
||||
})
|
||||
@@ -100,7 +122,7 @@ func (s *AuthorizationService) GetAuthorizationById(ctx context.Context, id int)
|
||||
func (s *AuthorizationService) DeleteAuthorization(ctx context.Context, auth *model.Authorization) error {
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -205,6 +227,7 @@ func (s *AuthorizationService) GetAuthsByAsset(ctx context.Context, asset *model
|
||||
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) {
|
||||
defer repository.DeleteAllFromCacheDb(ctx, model.DefaultAuthorization)
|
||||
|
||||
@@ -213,46 +236,29 @@ func (s *AuthorizationService) HandleAuthorization(ctx context.Context, tx *gorm
|
||||
eg := &errgroup.Group{}
|
||||
|
||||
if asset != nil && asset.Id > 0 {
|
||||
var pres []*model.Authorization
|
||||
pres, err = s.GetAuthsByAsset(ctx, asset)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
switch action {
|
||||
case model.ACTION_CREATE:
|
||||
auths = lo.Map(lo.Keys(asset.Authorization), func(id int, _ int) *model.Authorization {
|
||||
return &model.Authorization{AssetId: asset.Id, AccountId: id, Rids: asset.Authorization[id]}
|
||||
})
|
||||
// V2: Create authorization rules instead of V1 authorization records
|
||||
err = s.createV2AuthorizationRulesForAsset(ctx, tx, asset, currentUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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:
|
||||
for _, pre := range pres {
|
||||
p := pre
|
||||
if v, ok := asset.Authorization[p.AccountId]; ok {
|
||||
p.Rids = v
|
||||
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})
|
||||
}
|
||||
// V2: Update authorization rules for this asset
|
||||
err = s.updateV2AuthorizationRulesForAsset(ctx, tx, asset, currentUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle individual authorization records (V1 compatibility)
|
||||
for _, a := range lo.Filter(auths, func(item *model.Authorization, _ int) bool { return item != nil }) {
|
||||
auth := a
|
||||
switch action {
|
||||
@@ -344,56 +350,237 @@ func (s *AuthorizationService) GetAuthorizationIds(ctx *gin.Context) (authIds []
|
||||
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) {
|
||||
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)
|
||||
|
||||
// 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 {
|
||||
return true, nil
|
||||
return createBatchResult(true, "Share session"), nil
|
||||
}
|
||||
|
||||
if ok = acl.IsAdmin(currentUser); ok {
|
||||
return
|
||||
// 2. Administrators have access to all resources
|
||||
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 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)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if ok = lo.ContainsBy(authIds, func(item *model.AuthorizationIds) bool {
|
||||
return item.NodeId == 0 && item.AssetId == sess.AssetId && item.AccountId == sess.AccountId
|
||||
}); ok {
|
||||
return true, nil
|
||||
}
|
||||
ctx.Set(kAuthorizationIds, authIds)
|
||||
|
||||
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
|
||||
// Create base authorization request (without action, will be added per action)
|
||||
clientIP := s.getClientIP(ctx)
|
||||
baseReq := &model.BatchAuthRequest{
|
||||
UserId: currentUser.GetUid(),
|
||||
NodeId: sess.Session.Asset.ParentId,
|
||||
AssetId: sess.AssetId,
|
||||
AccountId: sess.AccountId,
|
||||
Actions: actions,
|
||||
ClientIP: clientIP,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
ids, err := assetService.GetAssetIdsByNodeAccount(ctx, nodeIds, accountIds)
|
||||
if err != nil {
|
||||
logger.L().Error("", zap.Error(err))
|
||||
return false, err
|
||||
}
|
||||
|
||||
return lo.Contains(ids, sess.AssetId), nil
|
||||
|
||||
// Use V2 matcher with filtered rule scope
|
||||
return s.matcher.MatchBatchWithScope(ctx, baseReq, authV2ResourceIds)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/veops/oneterm/internal/model"
|
||||
"github.com/veops/oneterm/internal/repository"
|
||||
dbpkg "github.com/veops/oneterm/pkg/db"
|
||||
@@ -26,8 +27,25 @@ func NewCommandTemplateService() *CommandTemplateService {
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
@@ -8,6 +8,8 @@ import (
|
||||
"github.com/veops/oneterm/internal/repository"
|
||||
"github.com/veops/oneterm/pkg/cache"
|
||||
dbpkg "github.com/veops/oneterm/pkg/db"
|
||||
"github.com/veops/oneterm/pkg/logger"
|
||||
"go.uber.org/zap"
|
||||
"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) {
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
func (s *NodeService) AttachAssetCount(ctx *gin.Context, data []*model.Node) error {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
@@ -136,7 +212,9 @@ func (s *NodeService) AttachAssetCount(ctx *gin.Context, data []*model.Node) err
|
||||
if !acl.IsAdmin(currentUser) {
|
||||
info := cast.ToBool(ctx.Query("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 {
|
||||
return err
|
||||
}
|
||||
@@ -215,7 +293,9 @@ func (s *NodeService) AttachHasChild(ctx *gin.Context, data []*model.Node) error
|
||||
}
|
||||
|
||||
if info {
|
||||
_, assetIds, _, err := NewAssetService().GetAssetIdsByAuthorization(ctx)
|
||||
// Use V2 authorization system for asset filtering
|
||||
authV2Service := NewAuthorizationV2Service()
|
||||
_, assetIds, _, err := authV2Service.GetAuthorizationScopeByACL(ctx)
|
||||
if err != nil {
|
||||
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
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -425,8 +507,7 @@ func (s *NodeService) GetNodesTree(ctx *gin.Context, dbQuery *gorm.DB, needAcl b
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Apply postHooks only if info=false
|
||||
if !info {
|
||||
// Apply postHooks - now always apply permission handling regardless of info mode
|
||||
if err := s.AttachAssetCount(ctx, allNodes); err != nil {
|
||||
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))
|
||||
}
|
||||
|
||||
// Always handle permissions, regardless of info mode
|
||||
if err := s.handleNodePermissions(ctx, allNodes, resourceType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Build tree structure
|
||||
return s.buildNodeTree(allNodes, nodeIds), nil
|
||||
@@ -487,10 +568,6 @@ func (s *NodeService) buildNodeTree(nodes []*model.Node, rootIds []int) []any {
|
||||
|
||||
// handleNodePermissions handles node permissions
|
||||
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)
|
||||
|
||||
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) {
|
||||
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
|
||||
}
|
||||
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)
|
||||
for _, asset := range assets {
|
||||
for accountId := range asset.Authorization {
|
||||
for accountId, authData := range asset.Authorization {
|
||||
account, ok := accountMap[accountId]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this account has connect permission
|
||||
if authData.Permissions == nil || !authData.Permissions.Connect {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, p := range asset.Protocols {
|
||||
ss := strings.Split(p, ":")
|
||||
if len(ss) != 2 {
|
||||
|
Reference in New Issue
Block a user