github.com/polarismesh/polaris@v1.17.8/cache/auth/strategy.go (about)

     1  /**
     2   * Tencent is pleased to support the open source community by making Polaris available.
     3   *
     4   * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
     5   *
     6   * Licensed under the BSD 3-Clause License (the "License");
     7   * you may not use this file except in compliance with the License.
     8   * You may obtain a copy of the License at
     9   *
    10   * https://opensource.org/licenses/BSD-3-Clause
    11   *
    12   * Unless required by applicable law or agreed to in writing, software distributed
    13   * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
    14   * CONDITIONS OF ANY KIND, either express or implied. See the License for the
    15   * specific language governing permissions and limitations under the License.
    16   */
    17  
    18  package auth
    19  
    20  import (
    21  	"fmt"
    22  	"math"
    23  	"time"
    24  
    25  	apisecurity "github.com/polarismesh/specification/source/go/api/v1/security"
    26  	"go.uber.org/zap"
    27  	"golang.org/x/sync/singleflight"
    28  
    29  	types "github.com/polarismesh/polaris/cache/api"
    30  	"github.com/polarismesh/polaris/common/model"
    31  	"github.com/polarismesh/polaris/common/utils"
    32  	"github.com/polarismesh/polaris/store"
    33  )
    34  
    35  const (
    36  	removePrincipalChSize = 8
    37  )
    38  
    39  // strategyCache
    40  type strategyCache struct {
    41  	*types.BaseCache
    42  
    43  	storage          store.Store
    44  	strategys        *utils.SyncMap[string, *model.StrategyDetailCache]
    45  	uid2Strategy     *utils.SyncMap[string, *utils.SyncSet[string]]
    46  	groupid2Strategy *utils.SyncMap[string, *utils.SyncSet[string]]
    47  
    48  	namespace2Strategy   *utils.SyncMap[string, *utils.SyncSet[string]]
    49  	service2Strategy     *utils.SyncMap[string, *utils.SyncSet[string]]
    50  	configGroup2Strategy *utils.SyncMap[string, *utils.SyncSet[string]]
    51  
    52  	lastMtime    int64
    53  	userCache    *userCache
    54  	singleFlight *singleflight.Group
    55  }
    56  
    57  // NewStrategyCache
    58  func NewStrategyCache(storage store.Store, cacheMgr types.CacheManager) types.StrategyCache {
    59  	return &strategyCache{
    60  		BaseCache: types.NewBaseCache(storage, cacheMgr),
    61  		storage:   storage,
    62  	}
    63  }
    64  
    65  func (sc *strategyCache) Initialize(c map[string]interface{}) error {
    66  	sc.userCache = sc.BaseCache.CacheMgr.GetCacher(types.CacheUser).(*userCache)
    67  	sc.strategys = utils.NewSyncMap[string, *model.StrategyDetailCache]()
    68  	sc.uid2Strategy = utils.NewSyncMap[string, *utils.SyncSet[string]]()
    69  	sc.groupid2Strategy = utils.NewSyncMap[string, *utils.SyncSet[string]]()
    70  	sc.namespace2Strategy = utils.NewSyncMap[string, *utils.SyncSet[string]]()
    71  	sc.service2Strategy = utils.NewSyncMap[string, *utils.SyncSet[string]]()
    72  	sc.configGroup2Strategy = utils.NewSyncMap[string, *utils.SyncSet[string]]()
    73  	sc.singleFlight = new(singleflight.Group)
    74  	sc.lastMtime = 0
    75  	return nil
    76  }
    77  
    78  func (sc *strategyCache) Update() error {
    79  	// 多个线程竞争,只有一个线程进行更新
    80  	_, err, _ := sc.singleFlight.Do(sc.Name(), func() (interface{}, error) {
    81  		return nil, sc.DoCacheUpdate(sc.Name(), sc.realUpdate)
    82  	})
    83  	return err
    84  }
    85  
    86  func (sc *strategyCache) ForceSync() error {
    87  	return sc.Update()
    88  }
    89  
    90  func (sc *strategyCache) realUpdate() (map[string]time.Time, int64, error) {
    91  	// 获取几秒前的全部数据
    92  	var (
    93  		start           = time.Now()
    94  		lastTime        = sc.LastFetchTime()
    95  		strategies, err = sc.storage.GetStrategyDetailsForCache(lastTime, sc.IsFirstUpdate())
    96  	)
    97  	if err != nil {
    98  		log.Errorf("[Cache][AuthStrategy] refresh auth strategy cache err: %s", err.Error())
    99  		return nil, -1, err
   100  	}
   101  
   102  	lastMtimes, add, update, del := sc.setStrategys(strategies)
   103  	timeDiff := time.Since(start)
   104  	if timeDiff > time.Second {
   105  		log.Info("[Cache][AuthStrategy] get more auth strategy",
   106  			zap.Int("add", add), zap.Int("update", update), zap.Int("delete", del),
   107  			zap.Time("last", lastTime), zap.Duration("used", time.Since(start)))
   108  	}
   109  	return lastMtimes, int64(len(strategies)), nil
   110  }
   111  
   112  // setStrategys 处理策略的数据更新情况
   113  // step 1. 先处理resource以及principal的数据更新情况(主要是为了能够获取到新老数据进行对比计算)
   114  // step 2. 处理真正的 strategy 的缓存更新
   115  func (sc *strategyCache) setStrategys(strategies []*model.StrategyDetail) (map[string]time.Time, int, int, int) {
   116  	var add, remove, update int
   117  
   118  	sc.handlerResourceStrategy(strategies)
   119  	sc.handlerPrincipalStrategy(strategies)
   120  
   121  	lastMtime := sc.LastMtime(sc.Name()).Unix()
   122  
   123  	for index := range strategies {
   124  		rule := strategies[index]
   125  		if !rule.Valid {
   126  			sc.strategys.Delete(rule.ID)
   127  			remove++
   128  		} else {
   129  			_, ok := sc.strategys.Load(rule.ID)
   130  			if !ok {
   131  				add++
   132  			} else {
   133  				update++
   134  			}
   135  			sc.strategys.Store(rule.ID, buildEnchanceStrategyDetail(rule))
   136  		}
   137  
   138  		lastMtime = int64(math.Max(float64(lastMtime), float64(rule.ModifyTime.Unix())))
   139  	}
   140  
   141  	return map[string]time.Time{sc.Name(): time.Unix(lastMtime, 0)}, add, update, remove
   142  }
   143  
   144  func buildEnchanceStrategyDetail(strategy *model.StrategyDetail) *model.StrategyDetailCache {
   145  	users := make(map[string]model.Principal, 0)
   146  	groups := make(map[string]model.Principal, 0)
   147  
   148  	for index := range strategy.Principals {
   149  		principal := strategy.Principals[index]
   150  		if principal.PrincipalRole == model.PrincipalUser {
   151  			users[principal.PrincipalID] = principal
   152  		} else {
   153  			groups[principal.PrincipalID] = principal
   154  		}
   155  	}
   156  
   157  	return &model.StrategyDetailCache{
   158  		StrategyDetail: strategy,
   159  		UserPrincipal:  users,
   160  		GroupPrincipal: groups,
   161  	}
   162  }
   163  
   164  func (sc *strategyCache) writeSet(linkContainers *utils.SyncMap[string, *utils.SyncSet[string]], key, val string, isDel bool) {
   165  	if isDel {
   166  		values, ok := linkContainers.Load(key)
   167  		if ok {
   168  			values.Remove(val)
   169  		}
   170  	} else {
   171  		if _, ok := linkContainers.Load(key); !ok {
   172  			linkContainers.Store(key, utils.NewSyncSet[string]())
   173  		}
   174  		values, _ := linkContainers.Load(key)
   175  		values.Add(val)
   176  	}
   177  }
   178  
   179  // handlerResourceStrategy 处理资源视角下策略的缓存
   180  // 根据新老策略的资源列表比对,计算出哪些资源不在和该策略存在关联关系,哪些资源新增了相关的策略
   181  func (sc *strategyCache) handlerResourceStrategy(strategies []*model.StrategyDetail) {
   182  	operateLink := func(resType int32, resId, strategyId string, remove bool) {
   183  		switch resType {
   184  		case int32(apisecurity.ResourceType_Namespaces):
   185  			sc.writeSet(sc.namespace2Strategy, resId, strategyId, remove)
   186  		case int32(apisecurity.ResourceType_Services):
   187  			sc.writeSet(sc.service2Strategy, resId, strategyId, remove)
   188  		case int32(apisecurity.ResourceType_ConfigGroups):
   189  			sc.writeSet(sc.configGroup2Strategy, resId, strategyId, remove)
   190  		}
   191  	}
   192  
   193  	for sIndex := range strategies {
   194  		rule := strategies[sIndex]
   195  		addRes := rule.Resources
   196  
   197  		if oldRule, exist := sc.strategys.Load(rule.ID); exist {
   198  			delRes := make([]model.StrategyResource, 0, 8)
   199  			// 计算前后对比, resource 的变化
   200  			newRes := make(map[string]struct{}, len(addRes))
   201  			for i := range addRes {
   202  				newRes[fmt.Sprintf("%d_%s", addRes[i].ResType, addRes[i].ResID)] = struct{}{}
   203  			}
   204  
   205  			// 筛选出从策略中被踢出的 resource 列表
   206  			for i := range oldRule.Resources {
   207  				item := oldRule.Resources[i]
   208  				if _, ok := newRes[fmt.Sprintf("%d_%s", item.ResType, item.ResID)]; !ok {
   209  					delRes = append(delRes, item)
   210  				}
   211  			}
   212  
   213  			// 针对被剔除的 resource 列表,清理掉所关联的鉴权策略信息
   214  			for rIndex := range delRes {
   215  				resource := delRes[rIndex]
   216  				operateLink(resource.ResType, resource.ResID, rule.ID, true)
   217  			}
   218  		}
   219  
   220  		for rIndex := range addRes {
   221  			resource := addRes[rIndex]
   222  			if rule.Valid {
   223  				operateLink(resource.ResType, resource.ResID, rule.ID, false)
   224  			} else {
   225  				operateLink(resource.ResType, resource.ResID, rule.ID, true)
   226  			}
   227  		}
   228  	}
   229  }
   230  
   231  // handlerPrincipalStrategy
   232  func (sc *strategyCache) handlerPrincipalStrategy(strategies []*model.StrategyDetail) {
   233  	for index := range strategies {
   234  		rule := strategies[index]
   235  		// 计算 uid -> auth rule
   236  		principals := rule.Principals
   237  
   238  		if oldRule, exist := sc.strategys.Load(rule.ID); exist {
   239  			delMembers := make([]model.Principal, 0, 8)
   240  			// 计算前后对比, principal 的变化
   241  			newRes := make(map[string]struct{}, len(principals))
   242  			for i := range principals {
   243  				newRes[fmt.Sprintf("%d_%s", principals[i].PrincipalRole, principals[i].PrincipalID)] = struct{}{}
   244  			}
   245  
   246  			// 筛选出从策略中被踢出的 principal 列表
   247  			for i := range oldRule.Principals {
   248  				item := oldRule.Principals[i]
   249  				if _, ok := newRes[fmt.Sprintf("%d_%s", item.PrincipalRole, item.PrincipalID)]; !ok {
   250  					delMembers = append(delMembers, item)
   251  				}
   252  			}
   253  
   254  			// 针对被剔除的 principal 列表,清理掉所关联的鉴权策略信息
   255  			for rIndex := range delMembers {
   256  				principal := delMembers[rIndex]
   257  				sc.removePrincipalLink(principal, rule)
   258  			}
   259  		}
   260  		if rule.Valid {
   261  			for pos := range principals {
   262  				principal := principals[pos]
   263  				sc.addPrincipalLink(principal, rule)
   264  			}
   265  		} else {
   266  			for pos := range principals {
   267  				principal := principals[pos]
   268  				sc.removePrincipalLink(principal, rule)
   269  			}
   270  		}
   271  	}
   272  }
   273  
   274  func (sc *strategyCache) removePrincipalLink(principal model.Principal, rule *model.StrategyDetail) {
   275  	linkContainers := sc.uid2Strategy
   276  	if principal.PrincipalRole != model.PrincipalUser {
   277  		linkContainers = sc.groupid2Strategy
   278  	}
   279  	sc.writeSet(linkContainers, principal.PrincipalID, rule.ID, true)
   280  }
   281  
   282  func (sc *strategyCache) addPrincipalLink(principal model.Principal, rule *model.StrategyDetail) {
   283  	linkContainers := sc.uid2Strategy
   284  	if principal.PrincipalRole != model.PrincipalUser {
   285  		linkContainers = sc.groupid2Strategy
   286  	}
   287  	sc.writeSet(linkContainers, principal.PrincipalID, rule.ID, false)
   288  }
   289  
   290  func (sc *strategyCache) Clear() error {
   291  	sc.BaseCache.Clear()
   292  	sc.strategys = utils.NewSyncMap[string, *model.StrategyDetailCache]()
   293  	sc.uid2Strategy = utils.NewSyncMap[string, *utils.SyncSet[string]]()
   294  	sc.groupid2Strategy = utils.NewSyncMap[string, *utils.SyncSet[string]]()
   295  	sc.namespace2Strategy = utils.NewSyncMap[string, *utils.SyncSet[string]]()
   296  	sc.service2Strategy = utils.NewSyncMap[string, *utils.SyncSet[string]]()
   297  	sc.configGroup2Strategy = utils.NewSyncMap[string, *utils.SyncSet[string]]()
   298  	sc.lastMtime = 0
   299  	return nil
   300  }
   301  
   302  func (sc *strategyCache) Name() string {
   303  	return types.StrategyRuleName
   304  }
   305  
   306  // 对于 check 逻辑,如果是计算 * 策略,则必须要求 * 资源下必须有策略
   307  // 如果是具体的资源ID,则该资源下不必有策略,如果没有策略就认为这个资源是可以被任何人编辑的
   308  func (sc *strategyCache) checkResourceEditable(strategIds *utils.SyncSet[string], principal model.Principal, mustCheck bool) bool {
   309  	// 是否可以编辑
   310  	editable := false
   311  	// 是否真的包含策略
   312  	isCheck := strategIds.Len() != 0
   313  
   314  	// 如果根本没有遍历过,则表示该资源下没有对应的策略列表,直接返回可编辑状态即可
   315  	if !isCheck && !mustCheck {
   316  		return true
   317  	}
   318  
   319  	strategIds.Range(func(strategyId string) {
   320  		isCheck = true
   321  		if rule, ok := sc.strategys.Load(strategyId); ok {
   322  			if principal.PrincipalRole == model.PrincipalUser {
   323  				_, exist := rule.UserPrincipal[principal.PrincipalID]
   324  				editable = editable || exist
   325  			} else {
   326  				_, exist := rule.GroupPrincipal[principal.PrincipalID]
   327  				editable = editable || exist
   328  			}
   329  		}
   330  	})
   331  
   332  	return editable
   333  }
   334  
   335  // IsResourceEditable 判断当前资源是否可以操作
   336  // 这里需要考虑两种情况,一种是 “ * ” 策略,另一种是明确指出了具体的资源ID的策略
   337  func (sc *strategyCache) IsResourceEditable(
   338  	principal model.Principal, resType apisecurity.ResourceType, resId string) bool {
   339  	var (
   340  		valAll, val *utils.SyncSet[string]
   341  		ok          bool
   342  	)
   343  	switch resType {
   344  	case apisecurity.ResourceType_Namespaces:
   345  		val, ok = sc.namespace2Strategy.Load(resId)
   346  		valAll, _ = sc.namespace2Strategy.Load("*")
   347  	case apisecurity.ResourceType_Services:
   348  		val, ok = sc.service2Strategy.Load(resId)
   349  		valAll, _ = sc.service2Strategy.Load("*")
   350  	case apisecurity.ResourceType_ConfigGroups:
   351  		val, ok = sc.configGroup2Strategy.Load(resId)
   352  		valAll, _ = sc.configGroup2Strategy.Load("*")
   353  	}
   354  
   355  	// 代表该资源没有关联到任何策略,任何人都可以编辑
   356  	if !ok {
   357  		return true
   358  	}
   359  
   360  	principals := make([]model.Principal, 0, 4)
   361  	principals = append(principals, principal)
   362  	if principal.PrincipalRole == model.PrincipalUser {
   363  		groupids := sc.userCache.GetUserLinkGroupIds(principal.PrincipalID)
   364  		for i := range groupids {
   365  			principals = append(principals, model.Principal{
   366  				PrincipalID:   groupids[i],
   367  				PrincipalRole: model.PrincipalGroup,
   368  			})
   369  		}
   370  	}
   371  
   372  	for i := range principals {
   373  		item := principals[i]
   374  		if valAll != nil && sc.checkResourceEditable(valAll, item, true) {
   375  			return true
   376  		}
   377  
   378  		if sc.checkResourceEditable(val, item, false) {
   379  			return true
   380  		}
   381  	}
   382  
   383  	return false
   384  }
   385  
   386  func (sc *strategyCache) GetStrategyDetailsByUID(uid string) []*model.StrategyDetail {
   387  	return sc.getStrategyDetails(uid, "")
   388  }
   389  
   390  func (sc *strategyCache) GetStrategyDetailsByGroupID(groupid string) []*model.StrategyDetail {
   391  	return sc.getStrategyDetails("", groupid)
   392  }
   393  
   394  func (sc *strategyCache) getStrategyDetails(uid string, gid string) []*model.StrategyDetail {
   395  	var (
   396  		strategyIds []string
   397  	)
   398  	if uid != "" {
   399  		sets, ok := sc.uid2Strategy.Load(uid)
   400  		if !ok {
   401  			return nil
   402  		}
   403  		strategyIds = sets.ToSlice()
   404  	} else if gid != "" {
   405  		sets, ok := sc.groupid2Strategy.Load(gid)
   406  		if !ok {
   407  			return nil
   408  		}
   409  		strategyIds = sets.ToSlice()
   410  	}
   411  
   412  	if len(strategyIds) > 0 {
   413  		result := make([]*model.StrategyDetail, 0, 16)
   414  		for i := range strategyIds {
   415  			strategy, ok := sc.strategys.Load(strategyIds[i])
   416  			if ok {
   417  				result = append(result, strategy.StrategyDetail)
   418  			}
   419  		}
   420  
   421  		return result
   422  	}
   423  
   424  	return nil
   425  }
   426  
   427  // IsResourceLinkStrategy 校验
   428  func (sc *strategyCache) IsResourceLinkStrategy(resType apisecurity.ResourceType, resId string) bool {
   429  	switch resType {
   430  	case apisecurity.ResourceType_Namespaces:
   431  		val, ok := sc.namespace2Strategy.Load(resId)
   432  		return ok && hasLinkRule(val)
   433  	case apisecurity.ResourceType_Services:
   434  		val, ok := sc.service2Strategy.Load(resId)
   435  		return ok && hasLinkRule(val)
   436  	case apisecurity.ResourceType_ConfigGroups:
   437  		val, ok := sc.configGroup2Strategy.Load(resId)
   438  		return ok && hasLinkRule(val)
   439  	default:
   440  		return true
   441  	}
   442  }
   443  
   444  func hasLinkRule(sets *utils.SyncSet[string]) bool {
   445  	return sets.Len() != 0
   446  }