Merge pull request #2477 from swordqiu/hotfix/qj-policy-match-weight-support

fix: use policy match weight to override less specific policy
This commit is contained in:
yunion-ci-robot
2019-08-24 18:38:11 +08:00
committed by GitHub
10 changed files with 693 additions and 51 deletions
+14
View File
@@ -1,3 +1,17 @@
// 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 main
import (
+53 -30
View File
@@ -38,13 +38,13 @@ import (
const (
PolicyDelegation = "delegate"
PolicyActionList = "list"
PolicyActionGet = "get"
PolicyActionUpdate = "update"
PolicyActionPatch = "patch"
PolicyActionCreate = "create"
PolicyActionDelete = "delete"
PolicyActionPerform = "perform"
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)
@@ -307,25 +307,53 @@ func (manager *SPolicyManager) findPolicyByName(scope rbacutils.TRbacScope, name
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 {
for i := range policies {
if !policies[i].Match(userCred) {
continue
}
findMatchPolicy = true
rule := policies[i].GetMatchRule(service, resource, action, extra...)
if rule != nil {
matchRules = append(matchRules, *rule)
}
}
matchRules, findMatchPolicy = getMatchedPolicyRules(policies, userCred, service, resource, action, extra...)
}
scopedDeny := false
@@ -358,7 +386,8 @@ func (manager *SPolicyManager) allowWithoutCache(scope rbacutils.TRbacScope, use
defaultPolicies, ok := manager.defaultPolicies[scope]
if ok {
for i := range defaultPolicies {
if !defaultPolicies[i].Match(userCred) {
isMatched, _ := defaultPolicies[i].Match(userCred)
if !isMatched {
continue
}
rule := defaultPolicies[i].GetMatchRule(service, resource, action, extra...)
@@ -476,10 +505,9 @@ func (manager *SPolicyManager) IsScopeCapable(userCred mcclient.TokenCredential,
}
if policies, ok := manager.policies[scope]; ok {
for _, p := range policies {
if p.Match(userCred) {
return true
}
pnames := getMatchedPolicyNames(policies, userCred)
if len(pnames) > 0 {
return true
}
}
return false
@@ -491,12 +519,7 @@ func (manager *SPolicyManager) MatchedPolicies(scope rbacutils.TRbacScope, userC
if !ok {
return ret
}
for k, p := range policies {
if p.Match(userCred) {
ret = append(ret, k)
}
}
return ret
return getMatchedPolicyNames(policies, userCred)
}
func (manager *SPolicyManager) AllPolicies() map[string][]string {
+14
View File
@@ -1,3 +1,17 @@
// 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 modules
type ConfigsManager struct {
+14
View File
@@ -1,3 +1,17 @@
// 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 models
import (
+43
View File
@@ -0,0 +1,43 @@
// 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 rbacutils
import (
"yunion.io/x/onecloud/pkg/util/stringutils2"
)
const (
ActionList = "list"
ActionGet = "get"
ActionUpdate = "update"
ActionCreate = "create"
ActionDelete = "delete"
ActionPerform = "perform"
ActionPatch = "patch"
)
var (
AllActions = []string{
ActionList,
ActionGet,
ActionUpdate,
ActionCreate,
ActionDelete,
ActionPerform,
}
AllSortedActions = stringutils2.NewSortedStrings(AllActions)
)
+62 -18
View File
@@ -76,6 +76,10 @@ func (r1 TRbacResult) StricterThan(r2 TRbacResult) bool {
return r1.Strictness() < r2.Strictness()
}
func (r1 TRbacResult) LooserThan(r2 TRbacResult) bool {
return r1.Strictness() > r2.Strictness()
}
func (s1 TRbacScope) HigherEqual(s2 TRbacScope) bool {
return scopeScore[s1] >= scopeScore[s2]
}
@@ -113,6 +117,15 @@ type SRbacRule struct {
Result TRbacResult
}
func (r SRbacRule) clone() SRbacRule {
nr := r
nr.Extra = make([]string, len(r.Extra))
if len(r.Extra) > 0 {
copy(nr.Extra, r.Extra)
}
return nr
}
func isWildMatch(str string) bool {
return len(str) == 0 || str == WILD_MATCH
}
@@ -144,6 +157,10 @@ func (rule *SRbacRule) stricterThan(r2 *SRbacRule) bool {
return rule.Result.StricterThan(r2.Result)
}
func (rule *SRbacRule) looserThan(r2 *SRbacRule) bool {
return rule.Result.LooserThan(r2.Result)
}
func (rule *SRbacRule) match(service string, resource string, action string, extra ...string) (bool, int, int) {
matched := 0
weight := 0
@@ -215,7 +232,7 @@ func GetMatchRule(rules []SRbacRule, service string, resource string, action str
match, matchCnt, weight := rules[i].match(service, resource, action, extra...)
if match && (maxMatchCnt < matchCnt ||
(maxMatchCnt == matchCnt && minWeight > weight) ||
(maxMatchCnt == matchCnt && minWeight == weight && matchRule.stricterThan(&rules[i]))) {
(maxMatchCnt == matchCnt && minWeight == weight && matchRule.looserThan(&rules[i]))) {
maxMatchCnt = matchCnt
minWeight = weight
matchRule = &rules[i]
@@ -228,7 +245,7 @@ func CompactRules(rules []SRbacRule) []SRbacRule {
if len(rules) == 0 {
return nil
}
output := make([]SRbacRule, 1)
/*output := make([]SRbacRule, 1)
output[0] = rules[0]
for i := 1; i < len(rules); i += 1 {
isContains := false
@@ -246,8 +263,8 @@ func CompactRules(rules []SRbacRule) []SRbacRule {
if !isContains {
output = append(output, rules[i])
}
}
return output
}*/
return reduceRules(rules)
}
var (
@@ -332,9 +349,10 @@ func (policy *SRbacPolicy) Decode(policyJson jsonutils.JSONObject) error {
return err
}
rules, err := decode(ruleJson, SRbacRule{}, levelService)
/*rules, err := decode(ruleJson, SRbacRule{}, levelService)*/
rules, err := json2Rules(ruleJson)
if err != nil {
return err
return errors.Wrap(err, "json2Rules")
}
if len(rules) == 0 {
@@ -459,14 +477,16 @@ func addRule2Json(nodeJson *jsonutils.JSONDict, keys []string, result TRbacResul
}
func (policy *SRbacPolicy) Encode() (jsonutils.JSONObject, error) {
rules := jsonutils.NewDict()
/*rules := jsonutils.NewDict()
for i := 0; i < len(policy.Rules); i += 1 {
keys := policy.Rules[i].toStringArray()
err := addRule2Json(rules, keys, policy.Rules[i].Result)
if err != nil {
return nil, errors.Wrap(err, "addRule2Json")
}
}
}*/
rules := rules2Json(policy.Rules)
ret := jsonutils.NewDict()
// ret.Add(jsonutils.NewString(policy.Condition), "condition")
@@ -564,20 +584,44 @@ func (policy *SRbacPolicy) IsSystemWidePolicy() bool {
return len(policy.Roles) == 0 && len(policy.Projects) == 0
}
func (policy *SRbacPolicy) Match(userCred IRbacIdentity) bool {
if !policy.Auth && len(policy.Projects) == 0 && len(policy.Roles) == 0 && len(policy.Ips) == 0 {
return true
// check whether policy maches a userCred
// return value
// bool isMatched
// int match weight, the higher the value, the more exact the match
// the more exact match wins
func (policy *SRbacPolicy) Match(userCred IRbacIdentity) (bool, int) {
if !policy.Auth && len(policy.Roles) == 0 && len(policy.Projects) == 0 && len(policy.Ips) == 0 {
return true, 1
}
if userCred == nil {
return false
return false, 0
}
if !policy.IsPublic && len(policy.DomainId) > 0 && policy.DomainId != userCred.GetProjectDomainId() {
return false
weight := 0
if policy.IsPublic || len(policy.DomainId) == 0 || policy.DomainId == userCred.GetProjectDomainId() {
if len(policy.DomainId) > 0 {
weight += 10
}
if !policy.IsPublic {
weight += 10
}
if len(policy.Roles) == 0 || intersect(policy.Roles, userCred.GetRoles()) {
if len(policy.Roles) != 0 {
weight += 100
}
if len(policy.Projects) == 0 || contains(policy.Projects, userCred.GetProjectName()) {
if len(policy.Projects) > 0 {
weight += 1000
}
if len(policy.Ips) == 0 || containsIp(policy.Ips, userCred.GetLoginIp()) {
if len(policy.Ips) > 0 {
weight += 10000
}
return true, weight
}
}
}
}
if (len(policy.Projects) == 0 || contains(policy.Projects, userCred.GetProjectName())) && (len(policy.Roles) == 0 || intersect(policy.Roles, userCred.GetRoles())) && (len(policy.Ips) == 0 || containsIp(policy.Ips, userCred.GetLoginIp())) {
return true
}
return false
return false, 0
}
func (policy *SRbacPolicy) MatchRole(roleName string) bool {
+3 -3
View File
@@ -475,10 +475,10 @@ func TestSRbacPolicyMatch(t *testing.T) {
true,
},
}
for _, c := range cases {
got := c.policy.Match(c.userCred)
for i, c := range cases {
got, _ := c.policy.Match(c.userCred)
if got != c.want {
t.Errorf("%#v %#v got %v want %v", c.policy, c.userCred, got, c.want)
t.Errorf("[%d]: %#v %#v got %v want %v", i, c.policy, c.userCred, got, c.want)
}
}
}
+309
View File
@@ -0,0 +1,309 @@
// 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 rbacutils
import (
"sort"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/util/stringutils2"
)
type sRbacNode struct {
defNode *sRbacNode
downStream map[string]*sRbacNode
result *TRbacResult
level int
}
func newRbacNode(level int) *sRbacNode {
return &sRbacNode{
downStream: make(map[string]*sRbacNode),
level: level,
}
}
func (n *sRbacNode) AddRule(rule SRbacRule) {
n.addRule(rule, levelService)
}
func (n *sRbacNode) addRule(rule SRbacRule, level int) {
if level <= levelAction || len(rule.Extra) > level-levelExtra {
if n.result != nil {
// node is a leaf
if n.defNode != nil {
log.Fatalf("illegal state, result != nil and defNode != nil")
}
n.defNode = newRbacNode(n.level + 1)
n.defNode.result = n.result
n.result = nil
}
var key string
if level == levelService {
key = rule.Service
} else if level == levelResource {
key = rule.Resource
} else if level == levelAction {
key = rule.Action
} else {
key = rule.Extra[level-levelExtra]
}
if len(key) == 0 || key == WILD_MATCH {
next := n.defNode
if n.defNode == nil {
next = newRbacNode(level + 1)
n.defNode = next
}
next.addRule(rule, level+1)
} else {
next, ok := n.downStream[key]
if !ok {
next = newRbacNode(level + 1)
n.downStream[key] = next
}
next.addRule(rule, level+1)
}
} else {
if n.result != nil {
log.Warningf("node has been occupide!!!")
}
n.result = &rule.Result
}
}
func (n *sRbacNode) isLeaf() bool {
return n.result != nil && n.defNode == nil && len(n.downStream) == 0
}
func (n *sRbacNode) reduceDownstream() {
allowKey := make([]string, 0)
denyKey := make([]string, 0)
skipKey := make([]string, 0)
for k, v := range n.downStream {
if v.result == nil {
skipKey = append(skipKey, k)
continue
}
if *v.result == Allow {
allowKey = append(allowKey, k)
} else {
denyKey = append(denyKey, k)
}
}
if len(allowKey)+len(denyKey) > 0 {
var result TRbacResult
var keys []string
if n.defNode != nil {
if n.defNode.isLeaf() {
if *n.defNode.result == Allow {
keys = allowKey
result = Allow
} else {
keys = denyKey
result = Deny
}
}
} else {
if len(allowKey) >= len(denyKey) {
keys = allowKey
result = Allow
} else {
keys = denyKey
result = Deny
}
if len(allowKey) == 0 || len(denyKey) == 0 {
// reduce whole downStream
needBranch := false
if n.level == levelAction {
sort.Strings(keys)
if !stringutils2.Contains(stringutils2.SSortedStrings(keys), AllSortedActions) {
// not a complete set of action, cancel reduce
needBranch = true
}
} else if n.level >= levelExtra {
if len(keys) <= 1 {
needBranch = true
}
}
if needBranch {
keys = nil
if result == Allow {
result = Deny
} else {
result = Allow
}
// add a default branch
n.defNode = newRbacNode(n.level + 1)
n.defNode.result = &result
}
}
}
if len(keys) > 0 {
for _, k := range keys {
delete(n.downStream, k)
}
if n.defNode == nil {
n.defNode = newRbacNode(n.level + 1)
}
n.defNode.result = &result
}
}
if len(n.downStream) == 0 && n.defNode != nil && n.defNode.isLeaf() {
n.result = n.defNode.result
n.defNode = nil
}
}
func (n *sRbacNode) reduce() {
if n.defNode != nil {
n.defNode.reduce()
}
for k := range n.downStream {
n.downStream[k].reduce()
}
n.reduceDownstream()
}
func (n *sRbacNode) GetRules() []SRbacRule {
return n.getRules(SRbacRule{Service: WILD_MATCH}, levelService)
}
func (n *sRbacNode) getRules(seed SRbacRule, level int) []SRbacRule {
result := make([]SRbacRule, 0)
if n.result != nil {
rule := seed.clone()
rule.Result = *n.result
return []SRbacRule{rule}
} else {
if n.defNode != nil {
rule := seed.clone()
if level == levelService {
rule.Service = WILD_MATCH
} else if level == levelResource {
rule.Resource = WILD_MATCH
} else if level == levelAction {
rule.Action = WILD_MATCH
} else if level >= levelExtra {
rule.Extra = append(rule.Extra, WILD_MATCH)
}
newRules := n.defNode.getRules(rule, level+1)
result = append(result, newRules...)
}
for k := range n.downStream {
rule := seed.clone()
if level == levelService {
rule.Service = k
} else if level == levelResource {
rule.Resource = k
} else if level == levelAction {
rule.Action = k
} else if level >= levelExtra {
rule.Extra = append(rule.Extra, k)
}
newRules := n.downStream[k].getRules(rule, level+1)
result = append(result, newRules...)
}
}
return result
}
func reduceRules(rules []SRbacRule) []SRbacRule {
root := newRbacNode(levelService)
for _, r := range rules {
root.AddRule(r)
}
root.reduce()
return root.GetRules()
}
func rules2Json(rules []SRbacRule) jsonutils.JSONObject {
root := newRbacNode(levelService)
for _, r := range rules {
root.AddRule(r)
}
root.reduce()
return root.json()
}
func json2Rules(json jsonutils.JSONObject) ([]SRbacRule, error) {
root := newRbacNode(levelService)
err := root.parseJson(json)
if err != nil {
return nil, errors.Wrap(err, "root.parseJson")
}
root.reduce()
return root.GetRules(), nil
}
func (n *sRbacNode) json() jsonutils.JSONObject {
var result jsonutils.JSONObject
if n.result != nil {
return jsonutils.NewString(string(*n.result))
} else {
result = jsonutils.NewDict()
if n.defNode != nil {
result.(*jsonutils.JSONDict).Add(n.defNode.json(), "*")
}
for k := range n.downStream {
result.(*jsonutils.JSONDict).Add(n.downStream[k].json(), k)
}
}
return result
}
func (n *sRbacNode) parseJson(input jsonutils.JSONObject) error {
switch val := input.(type) {
case *jsonutils.JSONString:
ruleStr, err := val.GetString()
if err != nil {
return errors.Wrap(err, "val.GetString")
}
var result TRbacResult
switch ruleStr {
case string(Allow), string(AdminAllow), string(OwnerAllow), string(UserAllow), string(GuestAllow):
result = Allow
default:
result = Deny
}
n.result = &result
case *jsonutils.JSONDict:
ruleJsonDict, err := val.GetMap()
if err != nil {
return errors.Wrap(err, "val.GetMap")
}
for key, ruleJson := range ruleJsonDict {
if key == WILD_MATCH {
n.defNode = newRbacNode(n.level + 1)
err := n.defNode.parseJson(ruleJson)
if err != nil {
return errors.Wrap(err, "n.defNode.parseJson")
}
} else {
n.downStream[key] = newRbacNode(n.level + 1)
err := n.downStream[key].parseJson(ruleJson)
if err != nil {
return errors.Wrap(err, "n.downStream[key].parseJson")
}
}
}
default:
return errors.Wrap(ErrUnsuportRuleData, input.String())
}
return nil
}
+163
View File
@@ -0,0 +1,163 @@
// 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 rbacutils
import (
"testing"
)
func TestReduce(t *testing.T) {
cases := map[string]struct {
in []SRbacRule
want []SRbacRule
}{
"merge1": {
in: []SRbacRule{
{
Service: "compute",
Resource: "servers",
Action: "list",
Result: Allow,
},
{
Service: "compute",
Resource: "servers",
Action: "get",
Result: Allow,
},
{
Service: "compute",
Resource: "servers",
Action: "delete",
Result: Allow,
},
},
want: []SRbacRule{
{
Service: "compute",
Resource: "servers",
Result: Allow,
},
},
},
"merge2": {
in: []SRbacRule{
{
Service: "compute",
Resource: "servers",
Action: "get",
Result: Allow,
},
{
Service: "compute",
Resource: "servers",
Action: "create",
Result: Allow,
},
{
Service: "compute",
Resource: "networks",
Action: "get",
Result: Allow,
},
{
Service: "compute",
Resource: "networks",
Action: "create",
Result: Deny,
},
},
want: []SRbacRule{
{
Service: "compute",
Resource: "servers",
Action: "",
Result: Allow,
},
{
Service: "compute",
Resource: "networks",
Action: "get",
Result: Allow,
},
{
Service: "compute",
Resource: "networks",
Action: "create",
Result: Deny,
},
},
},
"merge3": {
in: []SRbacRule{
{
Service: "compute",
Resource: "servers",
Action: "get",
Result: Allow,
},
{
Service: "compute",
Resource: "servers",
Action: "get",
Extra: []string{"vnc"},
Result: Deny,
},
{
Service: "compute",
Resource: "servers",
Action: "create",
Result: Allow,
},
{
Service: "compute",
Resource: "servers",
Action: "update",
Result: Allow,
},
{
Service: "compute",
Resource: "servers",
Action: "delete",
Result: Deny,
},
},
want: []SRbacRule{
{
Service: "compute",
Resource: "servers",
Result: Allow,
},
{
Service: "compute",
Resource: "servers",
Action: "get",
Extra: []string{"vnc"},
Result: Deny,
},
{
Service: "compute",
Resource: "servers",
Action: "delete",
Result: Deny,
},
},
},
}
for name, c := range cases {
got := reduceRules(c.in)
t.Logf("[%s]: want: %s got: %s", name, c.want, got)
}
}
+18
View File
@@ -84,6 +84,24 @@ func (ss SSortedStrings) ContainsAll(needles ...string) bool {
return true
}
func Contains(a, b SSortedStrings) bool {
_, _, bNoA := Split(a, b)
if len(bNoA) == 0 {
return true
} else {
return false
}
}
func Equals(a, b SSortedStrings) bool {
aNoB, _, bNoA := Split(a, b)
if len(aNoB) == 0 && len(bNoA) == 0 {
return true
} else {
return false
}
}
func Split(a, b SSortedStrings) (aNoB SSortedStrings, aAndB SSortedStrings, bNoA SSortedStrings) {
a_b := make([]string, 0)
b_a := make([]string, 0)