mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-06-23 11:06:01 +08:00
557 lines
16 KiB
Go
557 lines
16 KiB
Go
// Copyright 2019 Yunion
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package policy
|
|
|
|
import (
|
|
"context"
|
|
"runtime/debug"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"yunion.io/x/jsonutils"
|
|
"yunion.io/x/log"
|
|
"yunion.io/x/pkg/errors"
|
|
|
|
"yunion.io/x/onecloud/pkg/appsrv"
|
|
"yunion.io/x/onecloud/pkg/cloudcommon/consts"
|
|
"yunion.io/x/onecloud/pkg/httperrors"
|
|
"yunion.io/x/onecloud/pkg/mcclient"
|
|
"yunion.io/x/onecloud/pkg/mcclient/auth"
|
|
"yunion.io/x/onecloud/pkg/mcclient/modules"
|
|
"yunion.io/x/onecloud/pkg/util/hashcache"
|
|
"yunion.io/x/onecloud/pkg/util/rbacutils"
|
|
)
|
|
|
|
const (
|
|
PolicyDelegation = "delegate"
|
|
|
|
PolicyActionList = rbacutils.ActionList
|
|
PolicyActionGet = rbacutils.ActionGet
|
|
PolicyActionUpdate = rbacutils.ActionUpdate
|
|
PolicyActionPatch = rbacutils.ActionPatch
|
|
PolicyActionCreate = rbacutils.ActionCreate
|
|
PolicyActionDelete = rbacutils.ActionDelete
|
|
PolicyActionPerform = rbacutils.ActionPerform
|
|
)
|
|
|
|
type PolicyFetchFunc func() (map[rbacutils.TRbacScope]map[string]*rbacutils.SRbacPolicy, error)
|
|
|
|
var (
|
|
PolicyManager *SPolicyManager
|
|
DefaultPolicyFetcher PolicyFetchFunc
|
|
|
|
syncWorkerManager *appsrv.SWorkerManager
|
|
)
|
|
|
|
func init() {
|
|
PolicyManager = &SPolicyManager{
|
|
lock: &sync.Mutex{},
|
|
}
|
|
DefaultPolicyFetcher = remotePolicyFetcher
|
|
|
|
// no need to queue many sync tasks
|
|
syncWorkerManager = appsrv.NewWorkerManagerIgnoreOverflow("sync_policy_worker", 1, 2, true, true)
|
|
}
|
|
|
|
type SPolicyManager struct {
|
|
policies map[rbacutils.TRbacScope]map[string]*rbacutils.SRbacPolicy
|
|
defaultPolicies map[rbacutils.TRbacScope][]*rbacutils.SRbacPolicy
|
|
lastSync time.Time
|
|
|
|
failedRetryInterval time.Duration
|
|
refreshInterval time.Duration
|
|
|
|
cache *hashcache.Cache // policy cache
|
|
|
|
lock *sync.Mutex
|
|
}
|
|
|
|
func parseJsonPolicy(obj jsonutils.JSONObject) (string, *rbacutils.SRbacPolicy, error) {
|
|
typeStr, err := obj.GetString("type")
|
|
if err != nil {
|
|
return "", nil, errors.Wrap(err, "missing type")
|
|
}
|
|
domainId, err := obj.GetString("domain_id")
|
|
if err != nil {
|
|
return "", nil, errors.Wrap(err, "missing domain_id")
|
|
}
|
|
|
|
isPublic := jsonutils.QueryBoolean(obj, "is_public", false)
|
|
|
|
blob, err := obj.Get("policy")
|
|
if err != nil {
|
|
log.Errorf("get blob error %s", err)
|
|
return "", nil, errors.Wrap(err, "json.Get")
|
|
}
|
|
|
|
policy := rbacutils.SRbacPolicy{}
|
|
err = policy.Decode(blob)
|
|
if err != nil {
|
|
log.Errorf("policy decode error %s", err)
|
|
return "", nil, errors.Wrap(err, "policy.Decode")
|
|
}
|
|
|
|
policy.DomainId = domainId
|
|
policy.IsPublic = isPublic
|
|
|
|
return typeStr, &policy, nil
|
|
}
|
|
|
|
func remotePolicyFetcher() (map[rbacutils.TRbacScope]map[string]*rbacutils.SRbacPolicy, error) {
|
|
s := auth.GetAdminSession(context.Background(), consts.GetRegion(), "v1")
|
|
|
|
policies := make(map[rbacutils.TRbacScope]map[string]*rbacutils.SRbacPolicy)
|
|
|
|
offset := 0
|
|
for {
|
|
params := jsonutils.NewDict()
|
|
params.Add(jsonutils.NewInt(2048), "limit")
|
|
params.Add(jsonutils.NewInt(int64(offset)), "offset")
|
|
params.Add(jsonutils.JSONTrue, "admin")
|
|
params.Add(jsonutils.JSONTrue, "enabled")
|
|
result, err := modules.Policies.List(s, params)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "modules.Policies.List")
|
|
}
|
|
|
|
for i := 0; i < len(result.Data); i += 1 {
|
|
typeStr, policy, err := parseJsonPolicy(result.Data[i])
|
|
if err != nil {
|
|
log.Errorf("error parse policty %s", err)
|
|
continue
|
|
}
|
|
|
|
if _, ok := policies[policy.Scope]; !ok {
|
|
policies[policy.Scope] = make(map[string]*rbacutils.SRbacPolicy)
|
|
}
|
|
policies[policy.Scope][typeStr] = policy
|
|
}
|
|
|
|
offset += len(result.Data)
|
|
if offset >= result.Total {
|
|
break
|
|
}
|
|
}
|
|
return policies, nil
|
|
}
|
|
|
|
func (manager *SPolicyManager) start(refreshInterval time.Duration, retryInterval time.Duration) {
|
|
log.Infof("PolicyManager start to fetch policies ...")
|
|
manager.refreshInterval = refreshInterval
|
|
manager.failedRetryInterval = retryInterval
|
|
if len(predefinedDefaultPolicies) > 0 {
|
|
policiesMap := make(map[rbacutils.TRbacScope][]*rbacutils.SRbacPolicy)
|
|
for i := range predefinedDefaultPolicies {
|
|
policy := predefinedDefaultPolicies[i]
|
|
if _, ok := policiesMap[policy.Scope]; !ok {
|
|
policiesMap[policy.Scope] = make([]*rbacutils.SRbacPolicy, 0)
|
|
}
|
|
policies := policiesMap[policy.Scope]
|
|
policies = append(policies, &policy)
|
|
policiesMap[policy.Scope] = policies
|
|
}
|
|
manager.defaultPolicies = policiesMap
|
|
log.Debugf("%#v", manager.defaultPolicies)
|
|
}
|
|
|
|
manager.cache = hashcache.NewCache(2048, manager.refreshInterval/2)
|
|
|
|
manager.SyncOnce()
|
|
}
|
|
|
|
func (manager *SPolicyManager) SyncOnce() {
|
|
syncWorkerManager.Run(manager.sync, nil, nil)
|
|
}
|
|
|
|
func (manager *SPolicyManager) doSync() error {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Errorf("policyManager doSync error %s", r)
|
|
debug.PrintStack()
|
|
}
|
|
}()
|
|
|
|
policies, err := DefaultPolicyFetcher()
|
|
if err != nil {
|
|
log.Errorf("sync rbac policy failed: %s", err)
|
|
return errors.Wrap(err, "DefaultPolicyFetcher")
|
|
}
|
|
|
|
manager.lock.Lock()
|
|
defer manager.lock.Unlock()
|
|
|
|
manager.policies = policies
|
|
|
|
manager.lastSync = time.Now()
|
|
manager.cache.Invalidate()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (manager *SPolicyManager) sync() {
|
|
err := manager.doSync()
|
|
var interval time.Duration
|
|
if err != nil {
|
|
interval = manager.failedRetryInterval
|
|
} else {
|
|
interval = manager.refreshInterval
|
|
}
|
|
time.AfterFunc(interval, manager.SyncOnce)
|
|
}
|
|
|
|
func queryKey(scope rbacutils.TRbacScope, userCred mcclient.TokenCredential, service string, resource string, action string, extra ...string) string {
|
|
queryKeys := []string{string(scope)}
|
|
queryKeys = append(queryKeys, userCred.GetProjectId(), userCred.GetDomainId(), userCred.GetUserId())
|
|
roles := userCred.GetRoles()
|
|
if len(roles) > 0 {
|
|
sort.Strings(roles)
|
|
}
|
|
queryKeys = append(queryKeys, strings.Join(roles, ":"))
|
|
if rbacutils.WILD_MATCH == service || len(service) == 0 {
|
|
service = rbacutils.WILD_MATCH
|
|
}
|
|
queryKeys = append(queryKeys, service)
|
|
if rbacutils.WILD_MATCH == resource || len(resource) == 0 {
|
|
resource = rbacutils.WILD_MATCH
|
|
}
|
|
queryKeys = append(queryKeys, resource)
|
|
if rbacutils.WILD_MATCH == action || len(action) == 0 {
|
|
action = rbacutils.WILD_MATCH
|
|
}
|
|
queryKeys = append(queryKeys, action)
|
|
if len(extra) > 0 {
|
|
queryKeys = append(queryKeys, extra...)
|
|
}
|
|
return strings.Join(queryKeys, "-")
|
|
}
|
|
|
|
func (manager *SPolicyManager) AllowScope(userCred mcclient.TokenCredential, service string, resource string, action string, extra ...string) rbacutils.TRbacScope {
|
|
for _, scope := range []rbacutils.TRbacScope{
|
|
rbacutils.ScopeSystem,
|
|
rbacutils.ScopeDomain,
|
|
rbacutils.ScopeProject,
|
|
rbacutils.ScopeUser,
|
|
} {
|
|
result := manager.allow(scope, userCred, service, resource, action, extra...)
|
|
if result == rbacutils.Allow {
|
|
return scope
|
|
}
|
|
}
|
|
return rbacutils.ScopeNone
|
|
}
|
|
|
|
func (manager *SPolicyManager) Allow(targetScope rbacutils.TRbacScope, userCred mcclient.TokenCredential, service string, resource string, action string, extra ...string) rbacutils.TRbacResult {
|
|
var retryScopes []rbacutils.TRbacScope
|
|
switch targetScope {
|
|
case rbacutils.ScopeSystem:
|
|
retryScopes = []rbacutils.TRbacScope{
|
|
rbacutils.ScopeSystem,
|
|
}
|
|
case rbacutils.ScopeDomain:
|
|
retryScopes = []rbacutils.TRbacScope{
|
|
rbacutils.ScopeSystem,
|
|
rbacutils.ScopeDomain,
|
|
}
|
|
case rbacutils.ScopeProject:
|
|
retryScopes = []rbacutils.TRbacScope{
|
|
rbacutils.ScopeSystem,
|
|
rbacutils.ScopeDomain,
|
|
rbacutils.ScopeProject,
|
|
}
|
|
case rbacutils.ScopeUser:
|
|
retryScopes = []rbacutils.TRbacScope{
|
|
rbacutils.ScopeSystem,
|
|
rbacutils.ScopeUser,
|
|
}
|
|
}
|
|
for _, scope := range retryScopes {
|
|
result := manager.allow(scope, userCred, service, resource, action, extra...)
|
|
if result == rbacutils.Allow {
|
|
return rbacutils.Allow
|
|
}
|
|
}
|
|
return rbacutils.Deny
|
|
}
|
|
|
|
func (manager *SPolicyManager) allow(scope rbacutils.TRbacScope, userCred mcclient.TokenCredential, service string, resource string, action string, extra ...string) rbacutils.TRbacResult {
|
|
if manager.cache != nil && userCred != nil {
|
|
key := queryKey(scope, userCred, service, resource, action, extra...)
|
|
val := manager.cache.Get(key)
|
|
if val != nil {
|
|
if consts.IsRbacDebug() {
|
|
log.Debugf("query %s:%s:%s:%s from cache %s", service, resource, action, extra, val)
|
|
}
|
|
return val.(rbacutils.TRbacResult)
|
|
}
|
|
result := manager.allowWithoutCache(scope, userCred, service, resource, action, extra...)
|
|
manager.cache.Set(key, result)
|
|
return result
|
|
} else {
|
|
return manager.allowWithoutCache(scope, userCred, service, resource, action, extra...)
|
|
}
|
|
}
|
|
|
|
func (manager *SPolicyManager) findPolicyByName(scope rbacutils.TRbacScope, name string) *rbacutils.SRbacPolicy {
|
|
if policies, ok := manager.policies[scope]; ok {
|
|
if p, ok := policies[name]; ok {
|
|
return p
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getMatchedPolicyNames(policies map[string]*rbacutils.SRbacPolicy, userCred rbacutils.IRbacIdentity) []string {
|
|
matchNames := make([]string, 0)
|
|
maxMatchWeight := 0
|
|
for k := range policies {
|
|
isMatched, matchWeight := policies[k].Match(userCred)
|
|
if !isMatched || matchWeight < maxMatchWeight {
|
|
continue
|
|
}
|
|
if maxMatchWeight < matchWeight {
|
|
maxMatchWeight = matchWeight
|
|
matchNames = matchNames[:0]
|
|
}
|
|
matchNames = append(matchNames, k)
|
|
}
|
|
return matchNames
|
|
}
|
|
|
|
func getMatchedPolicyRules(policies map[string]*rbacutils.SRbacPolicy, userCred rbacutils.IRbacIdentity, service string, resource string, action string, extra ...string) ([]rbacutils.SRbacRule, bool) {
|
|
matchRules := make([]rbacutils.SRbacRule, 0)
|
|
findMatchPolicy := false
|
|
maxMatchWeight := 0
|
|
for k := range policies {
|
|
isMatched, matchWeight := policies[k].Match(userCred)
|
|
if !isMatched || matchWeight < maxMatchWeight {
|
|
continue
|
|
}
|
|
if maxMatchWeight < matchWeight {
|
|
maxMatchWeight = matchWeight
|
|
matchRules = matchRules[:0]
|
|
}
|
|
findMatchPolicy = true
|
|
rule := policies[k].GetMatchRule(service, resource, action, extra...)
|
|
if rule != nil {
|
|
matchRules = append(matchRules, *rule)
|
|
}
|
|
}
|
|
return matchRules, findMatchPolicy
|
|
}
|
|
|
|
func (manager *SPolicyManager) allowWithoutCache(scope rbacutils.TRbacScope, userCred mcclient.TokenCredential, service string, resource string, action string, extra ...string) rbacutils.TRbacResult {
|
|
matchRules := make([]rbacutils.SRbacRule, 0)
|
|
findMatchPolicy := false
|
|
policies, ok := manager.policies[scope]
|
|
if !ok {
|
|
log.Warningf("no policies fetched for scope %s", scope)
|
|
} else {
|
|
matchRules, findMatchPolicy = getMatchedPolicyRules(policies, userCred, service, resource, action, extra...)
|
|
}
|
|
|
|
scopedDeny := false
|
|
switch scope {
|
|
case rbacutils.ScopeUser:
|
|
if !isUserResource(service, resource) {
|
|
scopedDeny = true
|
|
}
|
|
case rbacutils.ScopeProject:
|
|
if !isProjectResource(service, resource) {
|
|
scopedDeny = true
|
|
}
|
|
case rbacutils.ScopeDomain:
|
|
if isSystemResource(service, resource) {
|
|
scopedDeny = true
|
|
}
|
|
case rbacutils.ScopeSystem:
|
|
// no deny at all for system scope
|
|
}
|
|
if scopedDeny {
|
|
rule := rbacutils.SRbacRule{
|
|
Service: service,
|
|
Resource: resource,
|
|
Result: rbacutils.Deny,
|
|
}
|
|
matchRules = append(matchRules, rule)
|
|
}
|
|
|
|
// try default policies
|
|
defaultPolicies, ok := manager.defaultPolicies[scope]
|
|
if ok {
|
|
for i := range defaultPolicies {
|
|
isMatched, _ := defaultPolicies[i].Match(userCred)
|
|
if !isMatched {
|
|
continue
|
|
}
|
|
rule := defaultPolicies[i].GetMatchRule(service, resource, action, extra...)
|
|
if rule != nil {
|
|
matchRules = append(matchRules, *rule)
|
|
}
|
|
}
|
|
}
|
|
|
|
var result rbacutils.TRbacResult
|
|
if len(matchRules) > 0 {
|
|
rule := rbacutils.GetMatchRule(matchRules, service, resource, action, extra...)
|
|
result = rule.Result
|
|
} else if findMatchPolicy {
|
|
// if find matched policy, but no rule matching, allow anyway
|
|
result = rbacutils.Allow
|
|
} else {
|
|
result = rbacutils.Deny
|
|
}
|
|
|
|
if consts.IsRbacDebug() {
|
|
log.Debugf("[RBAC: %s] %s %s %s %#v permission %s userCred: %s MatchRules: %d(%s)", scope, service, resource, action, extra, result, userCred, len(matchRules), jsonutils.Marshal(matchRules))
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (manager *SPolicyManager) explainPolicy(userCred mcclient.TokenCredential, policyReq jsonutils.JSONObject, name string) ([]string, rbacutils.TRbacResult, error) {
|
|
_, request, result, err := manager.explainPolicyInternal(userCred, policyReq, name)
|
|
return request, result, err
|
|
}
|
|
|
|
func (manager *SPolicyManager) explainPolicyInternal(userCred mcclient.TokenCredential, policyReq jsonutils.JSONObject, name string) (rbacutils.TRbacScope, []string, rbacutils.TRbacResult, error) {
|
|
policySeq, err := policyReq.GetArray()
|
|
if err != nil {
|
|
return rbacutils.ScopeSystem, nil, rbacutils.Deny, httperrors.NewInputParameterError("invalid format")
|
|
}
|
|
service := rbacutils.WILD_MATCH
|
|
resource := rbacutils.WILD_MATCH
|
|
action := rbacutils.WILD_MATCH
|
|
extra := make([]string, 0)
|
|
if len(policySeq) > 1 {
|
|
service, _ = policySeq[1].GetString()
|
|
}
|
|
if len(policySeq) > 2 {
|
|
resource, _ = policySeq[2].GetString()
|
|
}
|
|
if len(policySeq) > 3 {
|
|
action, _ = policySeq[3].GetString()
|
|
}
|
|
if len(policySeq) > 4 {
|
|
for i := 4; i < len(policySeq); i += 1 {
|
|
ev, _ := policySeq[i].GetString()
|
|
extra = append(extra, ev)
|
|
}
|
|
}
|
|
|
|
reqStrs := []string{service, resource, action}
|
|
if len(extra) > 0 {
|
|
reqStrs = append(reqStrs, extra...)
|
|
}
|
|
|
|
scopeStr, _ := policySeq[0].GetString()
|
|
scope := rbacutils.String2Scope(scopeStr)
|
|
if !consts.IsRbacEnabled() {
|
|
if scope == rbacutils.ScopeProject || (scope == rbacutils.ScopeSystem && userCred.HasSystemAdminPrivilege()) {
|
|
return scope, reqStrs, rbacutils.Allow, nil
|
|
} else {
|
|
return scope, reqStrs, rbacutils.Deny, httperrors.NewForbiddenError("operation not allowed")
|
|
}
|
|
}
|
|
|
|
if len(name) == 0 {
|
|
return scope, reqStrs, manager.Allow(scope, userCred, service, resource, action, extra...), nil
|
|
}
|
|
|
|
policy := manager.findPolicyByName(scope, name)
|
|
if policy == nil {
|
|
return scope, reqStrs, rbacutils.Deny, httperrors.NewNotFoundError("policy %s not found", name)
|
|
}
|
|
|
|
rule := policy.GetMatchRule(service, resource, action, extra...)
|
|
result := rbacutils.Deny
|
|
if rule != nil {
|
|
result = rule.Result
|
|
}
|
|
return scope, reqStrs, result, nil
|
|
}
|
|
|
|
func (manager *SPolicyManager) ExplainRpc(userCred mcclient.TokenCredential, params jsonutils.JSONObject, name string) (jsonutils.JSONObject, error) {
|
|
paramDict, err := params.GetMap()
|
|
if err != nil {
|
|
return nil, httperrors.NewInputParameterError("invalid input format")
|
|
}
|
|
ret := jsonutils.NewDict()
|
|
for key, policyReq := range paramDict {
|
|
reqStrs, result, err := manager.explainPolicy(userCred, policyReq, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reqStrs = append(reqStrs, string(result))
|
|
ret.Add(jsonutils.NewStringArray(reqStrs), key)
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func (manager *SPolicyManager) IsScopeCapable(userCred mcclient.TokenCredential, scope rbacutils.TRbacScope) bool {
|
|
if !consts.IsRbacEnabled() {
|
|
if userCred.HasSystemAdminPrivilege() {
|
|
return true
|
|
}
|
|
if scope == rbacutils.ScopeProject {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
if policies, ok := manager.policies[scope]; ok {
|
|
pnames := getMatchedPolicyNames(policies, userCred)
|
|
if len(pnames) > 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (manager *SPolicyManager) MatchedPolicies(scope rbacutils.TRbacScope, userCred rbacutils.IRbacIdentity) []string {
|
|
ret := make([]string, 0)
|
|
policies, ok := manager.policies[scope]
|
|
if !ok {
|
|
return ret
|
|
}
|
|
return getMatchedPolicyNames(policies, userCred)
|
|
}
|
|
|
|
func (manager *SPolicyManager) AllPolicies() map[string][]string {
|
|
ret := make(map[string][]string)
|
|
for scope, p := range manager.policies {
|
|
k := string(scope)
|
|
ret[k] = make([]string, len(p))
|
|
i := 0
|
|
for pn := range p {
|
|
ret[k][i] = pn
|
|
i += 1
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (manager *SPolicyManager) RoleMatchPolicies(roleName string) []string {
|
|
ret := make([]string, 0)
|
|
for _, policies := range manager.policies {
|
|
for name, policy := range policies {
|
|
if policy.MatchRole(roleName) {
|
|
ret = append(ret, name)
|
|
}
|
|
}
|
|
}
|
|
return ret
|
|
}
|