mirror of
https://github.com/veops/oneterm.git
synced 2025-10-05 23:37:03 +08:00
1161 lines
34 KiB
Go
1161 lines
34 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"reflect"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/samber/lo"
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/veops/oneterm/internal/acl"
|
|
"github.com/veops/oneterm/internal/model"
|
|
"github.com/veops/oneterm/internal/repository"
|
|
"github.com/veops/oneterm/pkg/config"
|
|
dbpkg "github.com/veops/oneterm/pkg/db"
|
|
"github.com/veops/oneterm/pkg/logger"
|
|
)
|
|
|
|
// AuthorizationV2Service handles business logic for authorization V2
|
|
type AuthorizationV2Service struct {
|
|
repo repository.IAuthorizationV2Repository
|
|
timeTemplateService *TimeTemplateService
|
|
matcher IAuthorizationMatcher
|
|
}
|
|
|
|
// NewAuthorizationV2Service creates a new authorization V2 service
|
|
func NewAuthorizationV2Service() *AuthorizationV2Service {
|
|
repo := repository.NewAuthorizationV2Repository(dbpkg.DB)
|
|
return &AuthorizationV2Service{
|
|
repo: repo,
|
|
timeTemplateService: NewTimeTemplateService(),
|
|
matcher: NewAuthorizationMatcher(repo),
|
|
}
|
|
}
|
|
|
|
// BuildQuery builds the base query for authorization V2 rules
|
|
func (s *AuthorizationV2Service) BuildQuery(ctx context.Context) (*gorm.DB, error) {
|
|
db := dbpkg.DB.Model(model.DefaultAuthorizationV2)
|
|
|
|
// Add any additional filters here if needed
|
|
// For example, filtering by enabled status, etc.
|
|
|
|
return db, nil
|
|
}
|
|
|
|
// ValidateRule validates an authorization rule
|
|
func (s *AuthorizationV2Service) ValidateRule(ctx context.Context, rule *model.AuthorizationV2) error {
|
|
// Validate selector types
|
|
if !s.isValidSelectorType(rule.NodeSelector.Type) {
|
|
return errors.New("invalid node selector type")
|
|
}
|
|
if !s.isValidSelectorType(rule.AssetSelector.Type) {
|
|
return errors.New("invalid asset selector type")
|
|
}
|
|
if !s.isValidSelectorType(rule.AccountSelector.Type) {
|
|
return errors.New("invalid account selector type")
|
|
}
|
|
// Note: UserSelector is handled via Rids field for ACL integration
|
|
|
|
// Validate regex patterns if type is regex
|
|
if rule.NodeSelector.Type == model.SelectorTypeRegex {
|
|
if err := s.validateRegexPatterns(rule.NodeSelector.Values); err != nil {
|
|
return fmt.Errorf("invalid node selector regex: %w", err)
|
|
}
|
|
}
|
|
if rule.AssetSelector.Type == model.SelectorTypeRegex {
|
|
if err := s.validateRegexPatterns(rule.AssetSelector.Values); err != nil {
|
|
return fmt.Errorf("invalid asset selector regex: %w", err)
|
|
}
|
|
}
|
|
if rule.AccountSelector.Type == model.SelectorTypeRegex {
|
|
if err := s.validateRegexPatterns(rule.AccountSelector.Values); err != nil {
|
|
return fmt.Errorf("invalid account selector regex: %w", err)
|
|
}
|
|
}
|
|
// Note: User selection is handled via Rids field, no regex validation needed
|
|
|
|
// Validate time template reference if present
|
|
if rule.AccessControl.TimeTemplate != nil {
|
|
if err := s.validateTimeTemplateReference(ctx, rule.AccessControl.TimeTemplate); err != nil {
|
|
return fmt.Errorf("invalid time template reference: %w", err)
|
|
}
|
|
}
|
|
|
|
// Validate custom time ranges if present
|
|
if len(rule.AccessControl.CustomTimeRanges) > 0 {
|
|
if err := s.validateTimeRanges(rule.AccessControl.CustomTimeRanges); err != nil {
|
|
return fmt.Errorf("invalid custom time ranges: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateTimeTemplateReference validates a time template reference
|
|
func (s *AuthorizationV2Service) validateTimeTemplateReference(ctx context.Context, ref *model.TimeTemplateReference) error {
|
|
if ref.TemplateId <= 0 {
|
|
return errors.New("template_id must be positive")
|
|
}
|
|
|
|
// Check if template exists
|
|
template, err := s.timeTemplateService.GetTimeTemplate(ctx, ref.TemplateId)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get time template: %w", err)
|
|
}
|
|
if template == nil {
|
|
return errors.New("time template not found")
|
|
}
|
|
|
|
// Validate custom ranges if present
|
|
if len(ref.CustomRanges) > 0 {
|
|
if err := s.validateTimeRanges(ref.CustomRanges); err != nil {
|
|
return fmt.Errorf("invalid custom ranges in template reference: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateTimeRanges validates time ranges
|
|
func (s *AuthorizationV2Service) validateTimeRanges(ranges model.TimeRanges) error {
|
|
for i, timeRange := range ranges {
|
|
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 *AuthorizationV2Service) 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 *AuthorizationV2Service) 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 allows access according to the access control
|
|
func (s *AuthorizationV2Service) CheckTimeAccess(ctx context.Context, accessControl *model.AccessControl, timezone string) (bool, error) {
|
|
// If no time restrictions are configured, allow access
|
|
if accessControl.TimeTemplate == nil && len(accessControl.CustomTimeRanges) == 0 {
|
|
return true, nil
|
|
}
|
|
|
|
// Check time template if configured
|
|
if accessControl.TimeTemplate != nil {
|
|
// Get the template
|
|
template, err := s.timeTemplateService.GetTimeTemplate(ctx, accessControl.TimeTemplate.TemplateId)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to get time template: %w", err)
|
|
}
|
|
if template == nil {
|
|
return false, errors.New("time template not found")
|
|
}
|
|
|
|
// Use template's timezone if not specified in access control
|
|
templateTimezone := timezone
|
|
if templateTimezone == "" {
|
|
templateTimezone = accessControl.Timezone
|
|
}
|
|
if templateTimezone == "" {
|
|
templateTimezone = template.Timezone
|
|
}
|
|
|
|
// Check if current time is within template ranges
|
|
if s.timeTemplateService.IsTimeInTemplate(template, templateTimezone) {
|
|
return true, nil
|
|
}
|
|
|
|
// Check custom ranges in the template reference
|
|
if len(accessControl.TimeTemplate.CustomRanges) > 0 {
|
|
if s.isTimeInRanges(accessControl.TimeTemplate.CustomRanges, templateTimezone) {
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check custom time ranges if configured
|
|
if len(accessControl.CustomTimeRanges) > 0 {
|
|
checkTimezone := timezone
|
|
if checkTimezone == "" {
|
|
checkTimezone = accessControl.Timezone
|
|
}
|
|
if checkTimezone == "" {
|
|
checkTimezone = "Asia/Shanghai" // Default timezone
|
|
}
|
|
|
|
if s.isTimeInRanges(accessControl.CustomTimeRanges, checkTimezone) {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// isTimeInRanges checks if current time is within any of the specified time ranges
|
|
func (s *AuthorizationV2Service) isTimeInRanges(ranges model.TimeRanges, timezone string) bool {
|
|
// 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 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
|
|
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
|
|
}
|
|
|
|
// isValidSelectorType checks if selector type is valid
|
|
func (s *AuthorizationV2Service) isValidSelectorType(selectorType model.SelectorType) bool {
|
|
switch selectorType {
|
|
case model.SelectorTypeAll, model.SelectorTypeIds, model.SelectorTypeRegex, model.SelectorTypeTags:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// validateRegexPatterns validates regex patterns
|
|
func (s *AuthorizationV2Service) validateRegexPatterns(patterns []string) error {
|
|
for _, pattern := range patterns {
|
|
if _, err := regexp.Compile(pattern); err != nil {
|
|
return fmt.Errorf("invalid regex pattern '%s': %w", pattern, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetAuthorizedAssetIds returns asset IDs that the user has permission to access using V2 authorization
|
|
func (s *AuthorizationV2Service) GetAuthorizedAssetIds(ctx *gin.Context, action model.AuthAction) ([]int, error) {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
|
|
// Administrators have access to all assets
|
|
if acl.IsAdmin(currentUser) {
|
|
assets, err := repository.GetAllFromCacheDb(ctx, model.DefaultAsset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return lo.Map(assets, func(a *model.Asset, _ int) int { return a.Id }), nil
|
|
}
|
|
|
|
// Get all assets from cache
|
|
assets, err := repository.GetAllFromCacheDb(ctx, model.DefaultAsset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check permission for each asset
|
|
authorizedAssetIds := make([]int, 0)
|
|
for _, asset := range assets {
|
|
// Create authorization request
|
|
req := &model.AuthRequest{
|
|
UserId: currentUser.GetUid(),
|
|
NodeId: asset.ParentId,
|
|
AssetId: asset.Id,
|
|
AccountId: 0, // Check asset-level permission (any account)
|
|
Action: action,
|
|
ClientIP: s.getClientIP(ctx),
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
// Use V2 matcher
|
|
result, err := s.matcher.Match(ctx, req)
|
|
if err != nil {
|
|
logger.L().Error("Failed to check asset permission", zap.Int("assetId", asset.Id), zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
if result.Allowed {
|
|
authorizedAssetIds = append(authorizedAssetIds, asset.Id)
|
|
}
|
|
}
|
|
|
|
return authorizedAssetIds, nil
|
|
}
|
|
|
|
// GetAuthorizedAccountIds returns account IDs that the user has permission to access for given assets
|
|
func (s *AuthorizationV2Service) GetAuthorizedAccountIds(ctx *gin.Context, assetIds []int, action model.AuthAction) ([]int, error) {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
|
|
// Administrators have access to all accounts
|
|
if acl.IsAdmin(currentUser) {
|
|
accounts, err := repository.GetAllFromCacheDb(ctx, model.DefaultAccount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return lo.Map(accounts, func(a *model.Account, _ int) int { return a.Id }), nil
|
|
}
|
|
|
|
// Get all accounts from cache
|
|
accounts, err := repository.GetAllFromCacheDb(ctx, model.DefaultAccount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get assets for the given asset IDs
|
|
assets, err := repository.GetAllFromCacheDb(ctx, model.DefaultAsset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
assetMap := lo.SliceToMap(assets, func(a *model.Asset) (int, *model.Asset) { return a.Id, a })
|
|
|
|
authorizedAccountIds := make([]int, 0)
|
|
|
|
// Check permission for each account against each asset
|
|
for _, account := range accounts {
|
|
hasPermission := false
|
|
for _, assetId := range assetIds {
|
|
asset, exists := assetMap[assetId]
|
|
if !exists {
|
|
continue
|
|
}
|
|
|
|
// Create authorization request
|
|
req := &model.AuthRequest{
|
|
UserId: currentUser.GetUid(),
|
|
NodeId: asset.ParentId,
|
|
AssetId: assetId,
|
|
AccountId: account.Id,
|
|
Action: action,
|
|
ClientIP: s.getClientIP(ctx),
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
// Use V2 matcher
|
|
result, err := s.matcher.Match(ctx, req)
|
|
if err != nil {
|
|
logger.L().Error("Failed to check account permission",
|
|
zap.Int("assetId", assetId),
|
|
zap.Int("accountId", account.Id),
|
|
zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
if result.Allowed {
|
|
hasPermission = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if hasPermission {
|
|
authorizedAccountIds = append(authorizedAccountIds, account.Id)
|
|
}
|
|
}
|
|
|
|
return lo.Uniq(authorizedAccountIds), nil
|
|
}
|
|
|
|
// GetAuthorizedNodeIds returns node IDs that the user has permission to access
|
|
func (s *AuthorizationV2Service) GetAuthorizedNodeIds(ctx *gin.Context, action model.AuthAction) ([]int, error) {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
|
|
// Administrators have access to all nodes
|
|
if acl.IsAdmin(currentUser) {
|
|
nodes, err := repository.GetAllFromCacheDb(ctx, model.DefaultNode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return lo.Map(nodes, func(n *model.Node, _ int) int { return n.Id }), nil
|
|
}
|
|
|
|
// Get all nodes from cache
|
|
nodes, err := repository.GetAllFromCacheDb(ctx, model.DefaultNode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
authorizedNodeIds := make([]int, 0)
|
|
for _, node := range nodes {
|
|
// Create authorization request
|
|
req := &model.AuthRequest{
|
|
UserId: currentUser.GetUid(),
|
|
NodeId: node.Id,
|
|
AssetId: 0, // Check node-level permission (any asset)
|
|
AccountId: 0, // Check node-level permission (any account)
|
|
Action: action,
|
|
ClientIP: s.getClientIP(ctx),
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
// Use V2 matcher
|
|
result, err := s.matcher.Match(ctx, req)
|
|
if err != nil {
|
|
logger.L().Error("Failed to check node permission", zap.Int("nodeId", node.Id), zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
if result.Allowed {
|
|
authorizedNodeIds = append(authorizedNodeIds, node.Id)
|
|
}
|
|
}
|
|
|
|
return authorizedNodeIds, nil
|
|
}
|
|
|
|
// ApplyAssetPermissionFilter filters assets based on V2 authorization
|
|
func (s *AuthorizationV2Service) ApplyAssetPermissionFilter(ctx *gin.Context, assets []*model.Asset, action model.AuthAction) []*model.Asset {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
|
|
// Administrators have access to all assets
|
|
if acl.IsAdmin(currentUser) {
|
|
return assets
|
|
}
|
|
|
|
filteredAssets := make([]*model.Asset, 0)
|
|
for _, asset := range assets {
|
|
// Create authorization request
|
|
req := &model.AuthRequest{
|
|
UserId: currentUser.GetUid(),
|
|
NodeId: asset.ParentId,
|
|
AssetId: asset.Id,
|
|
AccountId: 0, // Check asset-level permission
|
|
Action: action,
|
|
ClientIP: s.getClientIP(ctx),
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
// Use V2 matcher
|
|
result, err := s.matcher.Match(ctx, req)
|
|
if err != nil {
|
|
logger.L().Error("Failed to check asset permission", zap.Int("assetId", asset.Id), zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
if result.Allowed {
|
|
filteredAssets = append(filteredAssets, asset)
|
|
}
|
|
}
|
|
|
|
return filteredAssets
|
|
}
|
|
|
|
// getClientIP extracts client IP from gin context
|
|
func (s *AuthorizationV2Service) 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 ""
|
|
}
|
|
|
|
// GetAssetPermissions returns all permissions for a user on a specific asset
|
|
func (s *AuthorizationV2Service) GetAssetPermissions(ctx *gin.Context, assetId int, accountId int) (*model.BatchAuthResult, error) {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
|
|
// Helper function to create batch result for all actions
|
|
allActions := []model.AuthAction{
|
|
model.ActionConnect,
|
|
model.ActionFileUpload,
|
|
model.ActionFileDownload,
|
|
model.ActionCopy,
|
|
model.ActionPaste,
|
|
model.ActionShare,
|
|
}
|
|
|
|
createBatchResult := func(allowed bool, reason string, permissions *model.AuthPermissions) *model.BatchAuthResult {
|
|
results := make(map[model.AuthAction]*model.AuthResult)
|
|
for _, action := range allActions {
|
|
result := &model.AuthResult{
|
|
Allowed: allowed,
|
|
Reason: reason,
|
|
}
|
|
// Only set permissions if they are provided
|
|
if permissions != nil {
|
|
result.Permissions = *permissions
|
|
}
|
|
results[action] = result
|
|
}
|
|
return &model.BatchAuthResult{
|
|
Results: results,
|
|
}
|
|
}
|
|
|
|
// Administrators have access to all resources
|
|
if acl.IsAdmin(currentUser) {
|
|
// For administrators, permissions reflect their actual capabilities
|
|
adminPermissions := &model.AuthPermissions{
|
|
Connect: true,
|
|
FileUpload: true,
|
|
FileDownload: true,
|
|
Copy: true,
|
|
Paste: true,
|
|
Share: true,
|
|
}
|
|
return createBatchResult(true, "Administrator access", adminPermissions), nil
|
|
}
|
|
|
|
// Get the asset information
|
|
asset, err := s.getAssetById(ctx, assetId)
|
|
if err != nil {
|
|
return createBatchResult(false, "Asset not found", nil), err
|
|
}
|
|
|
|
// Get user's authorized V2 rule IDs from ACL
|
|
authV2ResourceIds, err := s.getAuthorizedV2ResourceIds(ctx)
|
|
if err != nil {
|
|
return createBatchResult(false, "Failed to get authorized rules", nil), err
|
|
}
|
|
|
|
if len(authV2ResourceIds) == 0 {
|
|
return createBatchResult(false, "No authorization rules available", nil), nil
|
|
}
|
|
|
|
// Create batch authorization request for all actions
|
|
clientIP := s.getClientIP(ctx)
|
|
batchReq := &model.BatchAuthRequest{
|
|
UserId: currentUser.GetUid(),
|
|
NodeId: asset.ParentId,
|
|
AssetId: assetId,
|
|
AccountId: accountId,
|
|
Actions: allActions,
|
|
ClientIP: clientIP,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
// Use V2 matcher with filtered rule scope
|
|
return s.matcher.MatchBatchWithScope(ctx, batchReq, authV2ResourceIds)
|
|
}
|
|
|
|
// getAssetById retrieves asset by ID from cache
|
|
func (s *AuthorizationV2Service) getAssetById(ctx context.Context, assetId int) (*model.Asset, error) {
|
|
assets, err := repository.GetAllFromCacheDb(ctx, 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)
|
|
}
|
|
|
|
// getAuthorizedV2ResourceIds gets V2 authorization rule resource IDs that user has permission to
|
|
func (s *AuthorizationV2Service) 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
|
|
}
|
|
|
|
// GetAuthorizationScopeByACL efficiently gets authorization scope using ACL + V2 rules
|
|
func (s *AuthorizationV2Service) GetAuthorizationScopeByACL(ctx *gin.Context) (nodeIds []int, assetIds []int, accountIds []int, err error) {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
|
|
// Administrators have access to all resources
|
|
if acl.IsAdmin(currentUser) {
|
|
nodes, err := repository.GetAllFromCacheDb(ctx, model.DefaultNode)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
nodeIds = lo.Map(nodes, func(n *model.Node, _ int) int { return n.Id })
|
|
|
|
assets, err := repository.GetAllFromCacheDb(ctx, model.DefaultAsset)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
assetIds = lo.Map(assets, func(a *model.Asset, _ int) int { return a.Id })
|
|
|
|
accounts, err := repository.GetAllFromCacheDb(ctx, model.DefaultAccount)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
accountIds = lo.Map(accounts, func(a *model.Account, _ int) int { return a.Id })
|
|
|
|
return nodeIds, assetIds, accountIds, nil
|
|
}
|
|
|
|
// Get authorized resource IDs from ACL (this handles role inheritance)
|
|
authResourceIds, err := acl.GetRoleResourceIds(ctx, currentUser.GetRid(), config.RESOURCE_AUTHORIZATION)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
// No authorized resources means no access
|
|
if len(authResourceIds) == 0 {
|
|
return []int{}, []int{}, []int{}, nil
|
|
}
|
|
|
|
// Query V2 rules by resource IDs (much more efficient than querying all rules)
|
|
rules, err := s.repo.GetByResourceIds(ctx, authResourceIds)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
// Filter enabled rules
|
|
enabledRules := lo.Filter(rules, func(rule *model.AuthorizationV2, _ int) bool {
|
|
return rule.Enabled
|
|
})
|
|
|
|
// Extract IDs from the user's authorized rules
|
|
nodeIds = s.extractNodeIdsEfficient(ctx, enabledRules)
|
|
assetIds = s.extractAssetIdsEfficient(ctx, enabledRules)
|
|
accountIds = s.extractAccountIdsEfficient(ctx, enabledRules)
|
|
|
|
return lo.Uniq(nodeIds), lo.Uniq(assetIds), lo.Uniq(accountIds), nil
|
|
}
|
|
|
|
// extractNodeIdsEfficient efficiently extracts node IDs from rules
|
|
func (s *AuthorizationV2Service) extractNodeIdsEfficient(ctx context.Context, rules []*model.AuthorizationV2) []int {
|
|
var nodeIds []int
|
|
nodeCache := make(map[int]*model.Node) // Cache to avoid repeated queries
|
|
|
|
for _, rule := range rules {
|
|
switch rule.NodeSelector.Type {
|
|
case model.SelectorTypeAll:
|
|
// Add all node IDs
|
|
if len(nodeCache) == 0 {
|
|
nodes, err := repository.GetAllFromCacheDb(ctx, model.DefaultNode)
|
|
if err != nil {
|
|
logger.L().Error("Failed to get nodes from cache", zap.Error(err))
|
|
continue
|
|
}
|
|
for _, node := range nodes {
|
|
nodeCache[node.Id] = node
|
|
}
|
|
}
|
|
for nodeId := range nodeCache {
|
|
nodeIds = append(nodeIds, nodeId)
|
|
}
|
|
|
|
case model.SelectorTypeIds:
|
|
// Add specific node IDs
|
|
for _, value := range rule.NodeSelector.Values {
|
|
if id, err := strconv.Atoi(value); err == nil {
|
|
nodeIds = append(nodeIds, id)
|
|
}
|
|
}
|
|
|
|
case model.SelectorTypeRegex:
|
|
// Match nodes by regex patterns (load cache if needed)
|
|
if len(nodeCache) == 0 {
|
|
nodes, err := repository.GetAllFromCacheDb(ctx, model.DefaultNode)
|
|
if err != nil {
|
|
logger.L().Error("Failed to get nodes from cache", zap.Error(err))
|
|
continue
|
|
}
|
|
for _, node := range nodes {
|
|
nodeCache[node.Id] = node
|
|
}
|
|
}
|
|
for _, node := range nodeCache {
|
|
if s.matchRegexPatterns(node.Name, rule.NodeSelector.Values) {
|
|
nodeIds = append(nodeIds, node.Id)
|
|
}
|
|
}
|
|
|
|
case model.SelectorTypeTags:
|
|
// Tags selector not supported for nodes (no tags field)
|
|
logger.L().Warn("Tags selector not supported for nodes")
|
|
}
|
|
}
|
|
|
|
return nodeIds
|
|
}
|
|
|
|
// extractAssetIdsEfficient efficiently extracts asset IDs from rules
|
|
func (s *AuthorizationV2Service) extractAssetIdsEfficient(ctx context.Context, rules []*model.AuthorizationV2) []int {
|
|
var assetIds []int
|
|
assetCache := make(map[int]*model.Asset) // Cache to avoid repeated queries
|
|
|
|
for _, rule := range rules {
|
|
switch rule.AssetSelector.Type {
|
|
case model.SelectorTypeAll:
|
|
// Add all asset IDs
|
|
if len(assetCache) == 0 {
|
|
assets, err := repository.GetAllFromCacheDb(ctx, model.DefaultAsset)
|
|
if err != nil {
|
|
logger.L().Error("Failed to get assets from cache", zap.Error(err))
|
|
continue
|
|
}
|
|
for _, asset := range assets {
|
|
assetCache[asset.Id] = asset
|
|
}
|
|
}
|
|
for assetId := range assetCache {
|
|
assetIds = append(assetIds, assetId)
|
|
}
|
|
|
|
case model.SelectorTypeIds:
|
|
// Add specific asset IDs
|
|
for _, value := range rule.AssetSelector.Values {
|
|
if id, err := strconv.Atoi(value); err == nil {
|
|
assetIds = append(assetIds, id)
|
|
}
|
|
}
|
|
|
|
case model.SelectorTypeRegex:
|
|
// Match assets by regex patterns (load cache if needed)
|
|
if len(assetCache) == 0 {
|
|
assets, err := repository.GetAllFromCacheDb(ctx, model.DefaultAsset)
|
|
if err != nil {
|
|
logger.L().Error("Failed to get assets from cache", zap.Error(err))
|
|
continue
|
|
}
|
|
for _, asset := range assets {
|
|
assetCache[asset.Id] = asset
|
|
}
|
|
}
|
|
for _, asset := range assetCache {
|
|
if s.matchRegexPatterns(asset.Name, rule.AssetSelector.Values) || s.matchRegexPatterns(asset.Ip, rule.AssetSelector.Values) {
|
|
assetIds = append(assetIds, asset.Id)
|
|
}
|
|
}
|
|
|
|
case model.SelectorTypeTags:
|
|
// Tags selector not supported for assets (no tags field)
|
|
logger.L().Warn("Tags selector not supported for assets")
|
|
}
|
|
}
|
|
|
|
return assetIds
|
|
}
|
|
|
|
// extractAccountIdsEfficient efficiently extracts account IDs from rules
|
|
func (s *AuthorizationV2Service) extractAccountIdsEfficient(ctx context.Context, rules []*model.AuthorizationV2) []int {
|
|
var accountIds []int
|
|
accountCache := make(map[int]*model.Account) // Cache to avoid repeated queries
|
|
|
|
for _, rule := range rules {
|
|
switch rule.AccountSelector.Type {
|
|
case model.SelectorTypeAll:
|
|
// Add all account IDs
|
|
if len(accountCache) == 0 {
|
|
accounts, err := repository.GetAllFromCacheDb(ctx, model.DefaultAccount)
|
|
if err != nil {
|
|
logger.L().Error("Failed to get accounts from cache", zap.Error(err))
|
|
continue
|
|
}
|
|
for _, account := range accounts {
|
|
accountCache[account.Id] = account
|
|
}
|
|
}
|
|
for accountId := range accountCache {
|
|
accountIds = append(accountIds, accountId)
|
|
}
|
|
|
|
case model.SelectorTypeIds:
|
|
// Add specific account IDs
|
|
for _, value := range rule.AccountSelector.Values {
|
|
if id, err := strconv.Atoi(value); err == nil {
|
|
accountIds = append(accountIds, id)
|
|
}
|
|
}
|
|
|
|
case model.SelectorTypeRegex:
|
|
// Match accounts by regex patterns (load cache if needed)
|
|
if len(accountCache) == 0 {
|
|
accounts, err := repository.GetAllFromCacheDb(ctx, model.DefaultAccount)
|
|
if err != nil {
|
|
logger.L().Error("Failed to get accounts from cache", zap.Error(err))
|
|
continue
|
|
}
|
|
for _, account := range accounts {
|
|
accountCache[account.Id] = account
|
|
}
|
|
}
|
|
for _, account := range accountCache {
|
|
if s.matchRegexPatterns(account.Name, rule.AccountSelector.Values) || s.matchRegexPatterns(account.Account, rule.AccountSelector.Values) {
|
|
accountIds = append(accountIds, account.Id)
|
|
}
|
|
}
|
|
|
|
case model.SelectorTypeTags:
|
|
// Tags selector not supported for accounts (no tags field)
|
|
logger.L().Warn("Tags selector not supported for accounts")
|
|
}
|
|
}
|
|
|
|
return accountIds
|
|
}
|
|
|
|
// matchRegexPatterns checks if a string matches any of the regex patterns
|
|
func (s *AuthorizationV2Service) matchRegexPatterns(str string, patterns []string) bool {
|
|
for _, pattern := range patterns {
|
|
if matched, err := regexp.MatchString(pattern, str); err == nil && matched {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CloneRule clones an existing authorization rule with ACL handling
|
|
func (s *AuthorizationV2Service) CloneRule(ctx context.Context, sourceId int, newName string) (*model.AuthorizationV2, error) {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
if currentUser == nil {
|
|
return nil, errors.New("user not found in context")
|
|
}
|
|
|
|
// Get the source rule
|
|
sourceRule, err := s.repo.GetById(ctx, sourceId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get source rule: %w", err)
|
|
}
|
|
if sourceRule == nil {
|
|
return nil, errors.New("source rule not found")
|
|
}
|
|
|
|
// Create a copy of the rule
|
|
clonedRule := &model.AuthorizationV2{
|
|
Name: newName,
|
|
Description: fmt.Sprintf("Clone of: %s", sourceRule.Description),
|
|
Enabled: false, // Start disabled by default
|
|
ValidFrom: sourceRule.ValidFrom,
|
|
ValidTo: sourceRule.ValidTo,
|
|
|
|
// Copy selectors
|
|
NodeSelector: sourceRule.NodeSelector,
|
|
AssetSelector: sourceRule.AssetSelector,
|
|
AccountSelector: sourceRule.AccountSelector,
|
|
|
|
// Copy permissions and access control
|
|
Permissions: sourceRule.Permissions,
|
|
AccessControl: sourceRule.AccessControl,
|
|
|
|
// Copy role IDs
|
|
Rids: sourceRule.Rids,
|
|
|
|
// Set metadata for new rule
|
|
CreatorId: currentUser.GetUid(),
|
|
UpdaterId: currentUser.GetUid(),
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
// Validate the cloned rule
|
|
if err := s.ValidateRule(ctx, clonedRule); err != nil {
|
|
return nil, fmt.Errorf("cloned rule validation failed: %w", err)
|
|
}
|
|
|
|
// Use transaction to ensure consistency
|
|
var result *model.AuthorizationV2
|
|
err = dbpkg.DB.Transaction(func(tx *gorm.DB) error {
|
|
// Create ACL resource for the cloned rule
|
|
resourceId, err := acl.CreateAcl(ctx, currentUser, config.RESOURCE_AUTHORIZATION, clonedRule.Name)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create ACL resource: %w", err)
|
|
}
|
|
clonedRule.ResourceId = resourceId
|
|
|
|
// Create the cloned rule in database
|
|
if err := tx.Create(clonedRule).Error; err != nil {
|
|
return fmt.Errorf("failed to create cloned rule: %w", err)
|
|
}
|
|
|
|
// Grant permissions to roles if specified
|
|
if len(clonedRule.Rids) > 0 {
|
|
if err := acl.BatchGrantRoleResource(ctx, currentUser.GetUid(), clonedRule.Rids, resourceId, []string{acl.READ}); err != nil {
|
|
return fmt.Errorf("failed to grant role permissions: %w", err)
|
|
}
|
|
}
|
|
|
|
result = clonedRule
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
// Clean up ACL resource if transaction failed
|
|
if clonedRule.ResourceId > 0 {
|
|
acl.DeleteResource(ctx, currentUser.GetUid(), clonedRule.ResourceId)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
logger.L().Info("Authorization rule cloned successfully",
|
|
zap.Int("source_id", sourceId),
|
|
zap.Int("cloned_id", result.Id),
|
|
zap.String("cloned_name", newName),
|
|
zap.Int("user_id", currentUser.GetUid()))
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// CreateRule creates a new authorization rule with ACL handling
|
|
func (s *AuthorizationV2Service) CreateRule(ctx context.Context, rule *model.AuthorizationV2) error {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
if currentUser == nil {
|
|
return errors.New("user not found in context")
|
|
}
|
|
|
|
// Validate the rule
|
|
if err := s.ValidateRule(ctx, rule); err != nil {
|
|
return fmt.Errorf("rule validation failed: %w", err)
|
|
}
|
|
|
|
// Set metadata
|
|
rule.CreatorId = currentUser.GetUid()
|
|
rule.UpdaterId = currentUser.GetUid()
|
|
rule.CreatedAt = time.Now()
|
|
rule.UpdatedAt = time.Now()
|
|
|
|
// Use transaction to ensure consistency
|
|
return dbpkg.DB.Transaction(func(tx *gorm.DB) error {
|
|
// Create ACL resource
|
|
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 rule in database
|
|
if err := tx.Create(rule).Error; err != nil {
|
|
return fmt.Errorf("failed to create rule: %w", err)
|
|
}
|
|
|
|
// Grant permissions to roles if specified
|
|
if len(rule.Rids) > 0 {
|
|
if err := acl.BatchGrantRoleResource(ctx, currentUser.GetUid(), rule.Rids, resourceId, []string{acl.READ}); err != nil {
|
|
return fmt.Errorf("failed to grant role permissions: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// UpdateRule updates an existing authorization rule with ACL handling
|
|
func (s *AuthorizationV2Service) UpdateRule(ctx context.Context, rule *model.AuthorizationV2) error {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
if currentUser == nil {
|
|
return errors.New("user not found in context")
|
|
}
|
|
|
|
// Get existing rule for comparison
|
|
existingRule, err := s.repo.GetById(ctx, rule.Id)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get existing rule: %w", err)
|
|
}
|
|
if existingRule == nil {
|
|
return errors.New("rule not found")
|
|
}
|
|
|
|
// Validate the updated rule
|
|
if err := s.ValidateRule(ctx, rule); err != nil {
|
|
return fmt.Errorf("rule validation failed: %w", err)
|
|
}
|
|
|
|
// Preserve some fields
|
|
rule.ResourceId = existingRule.ResourceId
|
|
rule.CreatorId = existingRule.CreatorId
|
|
rule.CreatedAt = existingRule.CreatedAt
|
|
rule.UpdaterId = currentUser.GetUid()
|
|
rule.UpdatedAt = time.Now()
|
|
|
|
// Use transaction to ensure consistency
|
|
return dbpkg.DB.Transaction(func(tx *gorm.DB) error {
|
|
// Update role permissions if Rids changed
|
|
if !reflect.DeepEqual(rule.Rids, existingRule.Rids) {
|
|
// Revoke permissions from removed roles
|
|
removedRids := lo.Without(existingRule.Rids, rule.Rids...)
|
|
if len(removedRids) > 0 {
|
|
if err := acl.BatchRevokeRoleResource(ctx, currentUser.GetUid(), removedRids, rule.ResourceId, []string{acl.READ}); err != nil {
|
|
return fmt.Errorf("failed to revoke role permissions: %w", err)
|
|
}
|
|
}
|
|
|
|
// Grant permissions to new roles
|
|
newRids := lo.Without(rule.Rids, existingRule.Rids...)
|
|
if len(newRids) > 0 {
|
|
if err := acl.BatchGrantRoleResource(ctx, currentUser.GetUid(), newRids, rule.ResourceId, []string{acl.READ}); err != nil {
|
|
return fmt.Errorf("failed to grant role permissions: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update the rule in database
|
|
if err := tx.Save(rule).Error; err != nil {
|
|
return fmt.Errorf("failed to update rule: %w", err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// DeleteRule deletes an authorization rule with ACL cleanup
|
|
func (s *AuthorizationV2Service) DeleteRule(ctx context.Context, id int) error {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
if currentUser == nil {
|
|
return errors.New("user not found in context")
|
|
}
|
|
|
|
// Get the rule to delete
|
|
rule, err := s.repo.GetById(ctx, id)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get rule: %w", err)
|
|
}
|
|
if rule == nil {
|
|
return errors.New("rule not found")
|
|
}
|
|
|
|
// Use transaction to ensure consistency
|
|
return dbpkg.DB.Transaction(func(tx *gorm.DB) error {
|
|
// Delete ACL resource
|
|
if err := acl.DeleteResource(ctx, currentUser.GetUid(), rule.ResourceId); err != nil {
|
|
return fmt.Errorf("failed to delete ACL resource: %w", err)
|
|
}
|
|
|
|
// Delete the rule from database
|
|
if err := tx.Delete(rule).Error; err != nil {
|
|
return fmt.Errorf("failed to delete rule: %w", err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// GetRuleById retrieves a rule by ID
|
|
func (s *AuthorizationV2Service) GetRuleById(ctx context.Context, id int) (*model.AuthorizationV2, error) {
|
|
return s.repo.GetById(ctx, id)
|
|
}
|
|
|
|
// generateCloneName generates a unique name for a cloned rule
|
|
func (s *AuthorizationV2Service) generateCloneName(ctx context.Context, baseName string) (string, error) {
|
|
// Try "Copy of {baseName}" first
|
|
candidateName := fmt.Sprintf("Copy of %s", baseName)
|
|
|
|
// Check if this name exists
|
|
if exists, err := s.checkNameExists(ctx, candidateName); err != nil {
|
|
return "", err
|
|
} else if !exists {
|
|
return candidateName, nil
|
|
}
|
|
|
|
// Try "Copy of {baseName} (2)", "Copy of {baseName} (3)", etc.
|
|
for i := 2; i <= 999; i++ {
|
|
candidateName = fmt.Sprintf("Copy of %s (%d)", baseName, i)
|
|
if exists, err := s.checkNameExists(ctx, candidateName); err != nil {
|
|
return "", err
|
|
} else if !exists {
|
|
return candidateName, nil
|
|
}
|
|
}
|
|
|
|
return "", errors.New("unable to generate unique clone name")
|
|
}
|
|
|
|
// checkNameExists checks if a rule name already exists
|
|
func (s *AuthorizationV2Service) checkNameExists(ctx context.Context, name string) (bool, error) {
|
|
var count int64
|
|
err := dbpkg.DB.Model(&model.AuthorizationV2{}).Where("name = ?", name).Count(&count).Error
|
|
return count > 0, err
|
|
}
|