Files
cloudpods/pkg/util/rbacutils/rbac.go
2020-06-09 22:17:35 +08:00

734 lines
18 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 rbacutils
import (
"regexp"
"strings"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/netutils"
)
type TRbacResult string
type TRbacScope string
const (
WILD_MATCH = "*"
Allow = TRbacResult("allow")
Deny = TRbacResult("deny")
// no more many allow levels, only allow/deny
AdminAllow = TRbacResult("admin") // deprecated
OwnerAllow = TRbacResult("owner") // deprecated
UserAllow = TRbacResult("user") // deprecated
GuestAllow = TRbacResult("guest") // deprecated
ScopeSystem = TRbacScope("system")
ScopeDomain = TRbacScope("domain")
ScopeProject = TRbacScope("project")
// ScopeObject = "object"
ScopeUser = TRbacScope("user")
ScopeNone = TRbacScope("none")
)
var (
strictness = map[TRbacResult]int{
Deny: 0,
AdminAllow: 1,
OwnerAllow: 2,
UserAllow: 3,
GuestAllow: 4,
Allow: 5,
}
scopeScore = map[TRbacScope]int{
ScopeNone: 0,
ScopeUser: 1,
ScopeProject: 2,
ScopeDomain: 3,
ScopeSystem: 4,
}
)
func (r TRbacResult) Strictness() int {
return strictness[r]
}
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]
}
func (s1 TRbacScope) HigherThan(s2 TRbacScope) bool {
return scopeScore[s1] > scopeScore[s2]
}
type SRbacPolicy struct {
// condition, when the policy takes effects
// Deprecated
Condition string
DomainId string
IsPublic bool
PublicScope TRbacScope
SharedDomainIds []string
Projects []string
Roles []string
Ips []netutils.IPV4Prefix
Auth bool // whether needs authentication
// scope, the scope of the policy, system/domain/project
Scope TRbacScope
// Deprecated
// is_admin=true means scope=system, is_admin=false means scope=project
IsAdmin bool
// rules, the exact rules
Rules []SRbacRule
}
type SRbacRule struct {
Service string
Resource string
Action string
Extra []string
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
}
func (rule *SRbacRule) contains(rule2 *SRbacRule) bool {
if !isWildMatch(rule.Service) && rule.Service != rule2.Service {
return false
}
if !isWildMatch(rule.Resource) && rule.Resource != rule2.Resource {
return false
}
if !isWildMatch(rule.Action) && rule.Action != rule2.Action {
return false
}
if len(rule.Extra) > 0 {
for i := 0; i < len(rule.Extra); i += 1 {
if !isWildMatch(rule.Extra[i]) && (rule2.Extra == nil || len(rule2.Extra) < i || rule.Extra[i] != rule2.Extra[i]) {
return false
}
}
}
if string(rule.Result) != string(rule2.Result) {
return false
}
return true
}
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
if !isWildMatch(rule.Service) {
if rule.Service != service {
return false, 0, 0
}
matched += 1
weight += 1
}
if !isWildMatch(rule.Resource) {
if rule.Resource != resource {
return false, 0, 0
}
matched += 1
weight += 10
}
if !isWildMatch(rule.Action) {
if rule.Action != action {
return false, 0, 0
}
matched += 1
weight += 100
}
for i := 0; i < len(rule.Extra) && i < len(extra); i += 1 {
if !isWildMatch(rule.Extra[i]) {
if rule.Extra[i] != extra[i] {
return false, 0, 0
}
matched += 1
weight += 1000 * (i + 1)
}
}
return true, matched, weight
}
func (policy *SRbacPolicy) getMatchRule(req []string) *SRbacRule {
service := WILD_MATCH
if len(req) > levelService {
service = req[levelService]
}
resource := WILD_MATCH
if len(req) > levelResource {
resource = req[levelResource]
}
action := WILD_MATCH
if len(req) > levelAction {
action = req[levelAction]
}
var extra []string
if len(req) > levelExtra {
extra = req[levelExtra:]
} else {
extra = make([]string, 0)
}
return policy.GetMatchRule(service, resource, action, extra...)
}
func (policy *SRbacPolicy) GetMatchRule(service string, resource string, action string, extra ...string) *SRbacRule {
return GetMatchRule(policy.Rules, service, resource, action, extra...)
}
var (
ShowMatchRuleDebug = false
)
func GetMatchRule(rules []SRbacRule, service string, resource string, action string, extra ...string) *SRbacRule {
maxMatchCnt := 0
minWeight := 1000000
var matchRule *SRbacRule
for i := 0; i < len(rules); i += 1 {
match, matchCnt, weight := rules[i].match(service, resource, action, extra...)
if match && ShowMatchRuleDebug {
log.Debugf("rule %s match cnt %d weight %d", rules[i], matchCnt, weight)
}
if match && (maxMatchCnt < matchCnt ||
(maxMatchCnt == matchCnt && minWeight > weight) ||
(maxMatchCnt == matchCnt && minWeight == weight && matchRule.stricterThan(&rules[i]))) {
maxMatchCnt = matchCnt
minWeight = weight
matchRule = &rules[i]
}
}
return matchRule
}
func CompactRules(rules []SRbacRule) []SRbacRule {
if len(rules) == 0 {
return nil
}
/*output := make([]SRbacRule, 1)
output[0] = rules[0]
for i := 1; i < len(rules); i += 1 {
isContains := false
for j := 0; j < len(output); j += 1 {
if output[j].contains(&rules[i]) {
isContains = true
break
}
if rules[i].contains(&output[j]) {
output[j] = rules[i]
isContains = true
break
}
}
if !isContains {
output = append(output, rules[i])
}
}*/
return reduceRules(rules)
}
var (
tenantEqualsPattern = regexp.MustCompile(`tenant\s*==\s*['"]?(\w+)['"]?`)
roleContainsPattern = regexp.MustCompile(`roles.contains\(['"]?(\w+)['"]?\)`)
)
func searchMatchStrings(pattern *regexp.Regexp, condstr string) []string {
ret := make([]string, 0)
matches := pattern.FindAllStringSubmatch(condstr, -1)
for _, match := range matches {
ret = append(ret, match[1])
}
return ret
}
func searchMatchTenants(condstr string) []string {
return searchMatchStrings(tenantEqualsPattern, condstr)
}
func searchMatchRoles(condstr string) []string {
return searchMatchStrings(roleContainsPattern, condstr)
}
func (policy *SRbacPolicy) Decode(policyJson jsonutils.JSONObject) error {
policy.Condition, _ = policyJson.GetString("condition")
if policyJson.Contains("projects") {
projectJson, _ := policyJson.GetArray("projects")
policy.Projects = jsonutils.JSONArray2StringArray(projectJson)
}
if policyJson.Contains("roles") {
roleJson, _ := policyJson.GetArray("roles")
policy.Roles = jsonutils.JSONArray2StringArray(roleJson)
}
if len(policy.Projects) == 0 && len(policy.Roles) == 0 && len(policy.Condition) > 0 {
// XXX hack
// for smooth transtion from condition to projects&roles
policy.Projects = searchMatchTenants(policy.Condition)
policy.Roles = searchMatchRoles(policy.Condition)
}
// empty condition, no longer use this field
policy.Condition = ""
scopeStr, _ := policyJson.GetString("scope")
if len(scopeStr) > 0 {
policy.Scope = TRbacScope(scopeStr)
} else {
policy.IsAdmin = jsonutils.QueryBoolean(policyJson, "is_admin", false)
if len(policy.Scope) == 0 {
if policy.IsAdmin {
policy.Scope = ScopeSystem
} else {
policy.Scope = ScopeProject
}
}
}
if policyJson.Contains("ips") {
ipsJson, _ := policyJson.GetArray("ips")
ipStrs := jsonutils.JSONArray2StringArray(ipsJson)
policy.Ips = make([]netutils.IPV4Prefix, 0)
for _, ipStr := range ipStrs {
if len(ipStr) == 0 || ipStr == "0.0.0.0" {
continue
}
prefix, err := netutils.NewIPV4Prefix(ipStr)
if err != nil {
continue
}
policy.Ips = append(policy.Ips, prefix)
}
}
policy.Auth = jsonutils.QueryBoolean(policyJson, "auth", true)
if len(policy.Ips) > 0 || len(policy.Roles) > 0 || len(policy.Projects) > 0 {
policy.Auth = true
}
ruleJson, err := policyJson.Get("policy")
if err != nil {
return err
}
/*rules, err := decode(ruleJson, SRbacRule{}, levelService)*/
rules, err := json2Rules(ruleJson)
if err != nil {
return errors.Wrap(err, "json2Rules")
}
if len(rules) == 0 {
return ErrEmptyPolicy
}
policy.Rules = rules
return nil
}
const (
levelService = 0
levelResource = 1
levelAction = 2
levelExtra = 3
)
func decode(rules jsonutils.JSONObject, decodeRule SRbacRule, level int) ([]SRbacRule, error) {
switch rules.(type) {
case *jsonutils.JSONString:
ruleJsonStr := rules.(*jsonutils.JSONString)
ruleStr, _ := ruleJsonStr.GetString()
switch ruleStr {
case string(Allow), string(AdminAllow), string(OwnerAllow), string(UserAllow), string(GuestAllow):
decodeRule.Result = Allow
default:
decodeRule.Result = Deny
// default:
// return nil, fmt.Errorf("unsupported rule string %s", ruleStr)
}
return []SRbacRule{decodeRule}, nil
case *jsonutils.JSONDict:
ruleJsonDict, err := rules.GetMap()
if err != nil {
return nil, errors.Wrap(err, "get rule map fail")
}
rules := make([]SRbacRule, 0)
for key, ruleJson := range ruleJsonDict {
rule := decodeRule
switch {
case level == levelService:
rule.Service = key
case level == levelResource:
rule.Resource = key
case level == levelAction:
rule.Action = key
case level >= levelExtra:
if rule.Extra == nil {
rule.Extra = make([]string, 1)
rule.Extra[0] = key
} else {
rule.Extra = append(rule.Extra, key)
}
}
decoded, err := decode(ruleJson, rule, level+1)
if err != nil {
return nil, errors.Wrap(err, "decode")
}
rules = append(rules, decoded...)
}
return rules, nil
default:
return nil, errors.Wrap(ErrUnsuportRuleData, rules.String())
}
}
func (rule *SRbacRule) toStringArray() []string {
strArr := make([]string, 0)
strArr = append(strArr, rule.Service)
strArr = append(strArr, rule.Resource)
strArr = append(strArr, rule.Action)
if rule.Extra != nil {
strArr = append(strArr, rule.Extra...)
}
i := len(strArr) - 1
for i > 0 && (len(strArr[i]) == 0 || strArr[i] == WILD_MATCH) {
i -= 1
}
return strArr[0 : i+1]
}
func addRule2Json(nodeJson *jsonutils.JSONDict, keys []string, result TRbacResult) error {
if len(keys) == 1 {
if nodeJson.Contains(keys[0]) {
nextJson, _ := nodeJson.Get(keys[0])
switch nextJson.(type) {
case *jsonutils.JSONString: // conflict??
return ErrConflict // fmt.Errorf("conflict?")
case *jsonutils.JSONDict:
nextJsonDict := nextJson.(*jsonutils.JSONDict)
addRule2Json(nextJsonDict, []string{WILD_MATCH}, result)
return nil
default:
return ErrInvalidRules // fmt.Errorf("invalid rules")
}
} else {
nodeJson.Add(jsonutils.NewString(string(result)), keys[0])
return nil
}
}
// len(keys) > 1
exist, _ := nodeJson.Get(keys[0])
if exist != nil {
switch exist.(type) {
case *jsonutils.JSONString: // need restruct
newDict := jsonutils.NewDict()
newDict.Add(exist, "*")
nodeJson.Set(keys[0], newDict)
return addRule2Json(newDict, keys[1:], result)
case *jsonutils.JSONDict:
existDict := exist.(*jsonutils.JSONDict)
return addRule2Json(existDict, keys[1:], result)
default:
return ErrInvalidRules // fmt.Errorf("invalid rules")
}
} else {
next := jsonutils.NewDict()
nodeJson.Add(next, keys[0])
return addRule2Json(next, keys[1:], result)
}
}
func (policy *SRbacPolicy) Encode() (jsonutils.JSONObject, error) {
/*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")
// if policy.IsAdmin {
// ret.Add(jsonutils.JSONTrue, "is_admin")
// } else {
// ret.Add(jsonutils.JSONFalse, "is_admin")
// }
ret.Add(jsonutils.NewString(string(policy.Scope)), "scope")
if !policy.Auth && len(policy.Projects) == 0 && len(policy.Roles) == 0 && len(policy.Ips) == 0 {
ret.Add(jsonutils.JSONFalse, "auth")
} else {
ret.Add(jsonutils.JSONTrue, "auth")
}
if len(policy.Projects) > 0 {
ret.Add(jsonutils.NewStringArray(policy.Projects), "projects")
}
if len(policy.Roles) > 0 {
ret.Add(jsonutils.NewStringArray(policy.Roles), "roles")
}
if len(policy.Ips) > 0 {
ipStrs := make([]string, len(policy.Ips))
for i := range policy.Ips {
ipStrs[i] = policy.Ips[i].String()
}
ret.Add(jsonutils.NewStringArray(ipStrs), "ips")
}
ret.Add(rules, "policy")
return ret, nil
}
func (policy *SRbacPolicy) Explain(request [][]string) [][]string {
output := make([][]string, len(request))
for i := 0; i < len(request); i += 1 {
rule := policy.getMatchRule(request[i])
if rule == nil {
output[i] = append(request[i], string(Deny))
} else {
output[i] = append(request[i], string(rule.Result))
}
}
return output
}
func contains(s1 []string, s string) bool {
for i := range s1 {
if s1[i] == s {
return true
}
}
return false
}
func intersect(s1 []string, s2 []string) bool {
for i := range s1 {
for j := range s2 {
if s1[i] == s2[j] {
return true
}
}
}
return false
}
func containsIp(ips []netutils.IPV4Prefix, ipStr string) bool {
if len(ipStr) == 0 {
// user comes from unknown ip, assume matches
return true
}
ip, err := netutils.NewIPV4Addr(ipStr)
if err != nil {
log.Errorf("user comes from invalid ipv4 addr %s: %s", ipStr, err)
return false
}
for i := range ips {
if ips[i].Contains(ip) {
return true
}
}
return false
}
type IRbacIdentity interface {
GetProjectDomainId() string
GetProjectName() string
GetRoles() []string
GetLoginIp() string
GetTokenString() string
}
func (policy *SRbacPolicy) IsSystemWidePolicy() bool {
return (len(policy.DomainId) == 0 || (policy.IsPublic && policy.PublicScope == ScopeSystem)) && len(policy.Roles) == 0 && len(policy.Projects) == 0
}
func (policy *SRbacPolicy) MatchDomain(domainId string) bool {
if len(policy.DomainId) == 0 || len(domainId) == 0 {
return true
}
if policy.DomainId == domainId {
return true
}
if policy.IsPublic {
if policy.PublicScope == ScopeSystem {
return true
}
if contains(policy.SharedDomainIds, domainId) {
return true
}
}
return false
}
func (policy *SRbacPolicy) MatchProject(projectName string) bool {
if len(policy.Projects) == 0 || len(projectName) == 0 {
return true
}
if contains(policy.Projects, projectName) {
return true
}
return false
}
func (policy *SRbacPolicy) MatchRoles(roleNames []string) bool {
if len(policy.Roles) == 0 {
return true
}
if intersect(policy.Roles, roleNames) {
return true
}
return false
}
// 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 || len(userCred.GetTokenString()) == 0 {
return false, 0
}
weight := 0
if policy.MatchDomain(userCred.GetProjectDomainId()) {
if len(policy.DomainId) > 0 {
if policy.DomainId == userCred.GetProjectDomainId() {
weight += 30 // exact domain match
} else if len(policy.SharedDomainIds) > 0 {
weight += 20 // shared domain match
} else {
weight += 10 // else, system scope match
}
}
if policy.MatchRoles(userCred.GetRoles()) {
if len(policy.Roles) != 0 {
weight += 100
}
if policy.MatchProject(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
}
}
}
}
return false, 0
}
type sSimpleRbacIdentity struct {
domainId string
projectName string
roleNames []string
}
func (id sSimpleRbacIdentity) GetProjectDomainId() string {
return id.domainId
}
func (id sSimpleRbacIdentity) GetRoles() []string {
return id.roleNames
}
func (id sSimpleRbacIdentity) GetProjectName() string {
return id.projectName
}
func (id sSimpleRbacIdentity) GetLoginIp() string {
return ""
}
func (id sSimpleRbacIdentity) GetTokenString() string {
return "faketoken"
}
func NewRbacIdentity(domainId, projectName string, roleNames []string) IRbacIdentity {
return sSimpleRbacIdentity{
domainId: domainId,
projectName: projectName,
roleNames: roleNames,
}
}
func String2Scope(str string) TRbacScope {
return String2ScopeDefault(str, ScopeProject)
}
func String2ScopeDefault(str string, defScope TRbacScope) TRbacScope {
switch strings.ToLower(str) {
case string(ScopeSystem):
return ScopeSystem
case string(ScopeDomain):
return ScopeDomain
case string(ScopeProject):
return ScopeProject
case string(ScopeUser):
return ScopeUser
case "true":
return ScopeSystem
default:
return defScope
}
}