diff --git a/cmd/climc/shell/policies.go b/cmd/climc/shell/policies.go index af4b5d86f7..4f595012b3 100644 --- a/cmd/climc/shell/policies.go +++ b/cmd/climc/shell/policies.go @@ -1,17 +1,41 @@ package shell import ( + "fmt" "io/ioutil" + "os" + "os/exec" + "strings" + "time" "yunion.io/x/jsonutils" + "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/auth" "yunion.io/x/onecloud/pkg/mcclient/modules" ) func init() { type PolicyListOptions struct { + Limit int64 `help:"Limit, default 0, i.e. no limit"` + Offset int64 `help:"Offset, default 0, i.e. no offset"` + Search string `help:"Search by name"` + Type string `help:"filter by type"` } R(&PolicyListOptions{}, "policy-list", "List all policies", func(s *mcclient.ClientSession, args *PolicyListOptions) error { - result, err := modules.Policies.List(s, nil) + params := jsonutils.NewDict() + if len(args.Search) > 0 { + params.Add(jsonutils.NewString(args.Search), "type__icontains") + } + if args.Limit > 0 { + params.Add(jsonutils.NewInt(args.Limit), "limit") + } + if args.Offset > 0 { + params.Add(jsonutils.NewInt(args.Offset), "offset") + } + if len(args.Type) > 0 { + params.Add(jsonutils.NewString(args.Type), "type") + } + result, err := modules.Policies.List(s, params) if err != nil { return err } @@ -20,16 +44,18 @@ func init() { }) type PolicyCreateOptions struct { + TYPE string `help:"type of the policy"` FILE string `help:"path to policy file"` } R(&PolicyCreateOptions{}, "policy-create", "Create a new policy", func(s *mcclient.ClientSession, args *PolicyCreateOptions) error { - blob, err := ioutil.ReadFile(args.FILE) + policyBytes, err := ioutil.ReadFile(args.FILE) if err != nil { return err } + params := jsonutils.NewDict() - params.Add(jsonutils.NewString("application/json"), "type") - params.Add(jsonutils.NewString(string(blob)), "blob") + params.Add(jsonutils.NewString(args.TYPE), "type") + params.Add(jsonutils.NewString(string(policyBytes)), "policy") result, err := modules.Policies.Create(s, params) if err != nil { @@ -43,18 +69,26 @@ func init() { type PolicyPatchOptions struct { ID string `help:"ID of policy"` - FILE string `help:"path to policy file"` + File string `help:"path to policy file"` + Type string `help:"policy type"` } R(&PolicyPatchOptions{}, "policy-patch", "Patch policy", func(s *mcclient.ClientSession, args *PolicyPatchOptions) error { - blob, err := ioutil.ReadFile(args.FILE) + policyId, err := modules.Policies.GetId(s, args.ID, nil) if err != nil { return err } params := jsonutils.NewDict() - params.Add(jsonutils.NewString("application/json"), "type") - params.Add(jsonutils.NewString(string(blob)), "blob") - - result, err := modules.Policies.Patch(s, args.ID, params) + if len(args.Type) > 0 { + params.Add(jsonutils.NewString(args.Type), "type") + } + if len(args.File) > 0 { + policyBytes, err := ioutil.ReadFile(args.File) + if err != nil { + return err + } + params.Add(jsonutils.NewString(string(policyBytes)), "policy") + } + result, err := modules.Policies.Patch(s, policyId, params) if err != nil { return err } @@ -68,7 +102,117 @@ func init() { ID string `help:"ID of policy"` } R(&PolicyDeleteOptions{}, "policy-delete", "Delete policy", func(s *mcclient.ClientSession, args *PolicyDeleteOptions) error { - result, err := modules.Policies.Delete(s, args.ID, nil) + policyId, err := modules.Policies.GetId(s, args.ID, nil) + if err != nil { + return err + } + result, err := modules.Policies.Delete(s, policyId, nil) + if err != nil { + return err + } + printObject(result) + return nil + }) + + type PolicyShowOptions struct { + ID string `help:"ID of policy"` + } + R(&PolicyShowOptions{}, "policy-show", "Show policy", func(s *mcclient.ClientSession, args *PolicyShowOptions) error { + result, err := modules.Policies.Get(s, args.ID, nil) + if err != nil { + return err + } + yaml, err := result.GetString("policy") + if err != nil { + return err + } + fmt.Println(yaml) + return nil + }) + + type PolicyEditOptions struct { + ID string `help:"ID of policy"` + } + R(&PolicyEditOptions{}, "policy-edit", "Edit and update policy", func(s *mcclient.ClientSession, args *PolicyEditOptions) error { + result, err := modules.Policies.Get(s, args.ID, nil) + if err != nil { + return err + } + policyId, err := result.GetString("id") + if err != nil { + return err + } + yaml, err := result.GetString("policy") + if err != nil { + return err + } + + tmpfile, err := ioutil.TempFile("", "policy-blob") + if err != nil { + return err + } + defer os.Remove(tmpfile.Name()) // clean up + + if _, err := tmpfile.Write([]byte(yaml)); err != nil { + return err + } + if err := tmpfile.Close(); err != nil { + return err + } + + cmd := exec.Command("vim", tmpfile.Name()) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + err = cmd.Run() + if err != nil { + return err + } + + params := jsonutils.NewDict() + policyBytes, err := ioutil.ReadFile(tmpfile.Name()) + if err != nil { + return err + } + params.Add(jsonutils.NewString(string(policyBytes)), "policy") + + result, err = modules.Policies.Patch(s, policyId, params) + if err != nil { + return err + } + + printObject(result) + return nil + }) + + type PolicyExplainOptions struct { + Request []string `help:"explain request, in format of key:is_admin:service:resource:action:extra"` + } + R(&PolicyExplainOptions{}, "policy-explain", "Explain policy result", func(s *mcclient.ClientSession, args *PolicyExplainOptions) error { + auth.InitFromClientSession(s) + db.EnableGlobalRbac(15*time.Second, 15*time.Second) + + req := jsonutils.NewDict() + for i := 0; i < len(args.Request); i += 1 { + parts := strings.Split(args.Request[i], ":") + if len(parts) < 3 { + return fmt.Errorf("invalid request, should be in the form of key:is_admin:service[:resource:action:extra]") + } + key := parts[0] + data := make([]jsonutils.JSONObject, 1) + if parts[1] == "true" { + data[0] = jsonutils.JSONTrue + } else if parts[1] == "false" { + data[0] = jsonutils.JSONFalse + } else { + return fmt.Errorf("invalid request, is_admin should be true|false") + } + for i := 2; i < len(parts); i += 1 { + data = append(data, jsonutils.NewString(parts[i])) + } + req.Add(jsonutils.NewArray(data...), key) + } + fmt.Println(req.String()) + result, err := db.PolicyManager.ExplainRpc(s.GetToken(), req) if err != nil { return err } diff --git a/cmd/climc/shell/shell.go b/cmd/climc/shell/shell.go index 6eea679223..330ee0ba0a 100644 --- a/cmd/climc/shell/shell.go +++ b/cmd/climc/shell/shell.go @@ -1,7 +1,7 @@ package shell import ( - "fmt" + "errors" ) type CMD struct { @@ -13,10 +13,12 @@ type CMD struct { var CommandTable []CMD = make([]CMD, 0) +var ErrEmtptyUpdate = errors.New("No valid update data") + func R(options interface{}, command string, desc string, callback interface{}) { CommandTable = append(CommandTable, CMD{options, command, desc, callback}) } func InvalidUpdateError() error { - return fmt.Errorf("No valid update data") + return ErrEmtptyUpdate } diff --git a/pkg/cloudcommon/auth.go b/pkg/cloudcommon/auth.go index 6906dcb5db..22220a7e1c 100644 --- a/pkg/cloudcommon/auth.go +++ b/pkg/cloudcommon/auth.go @@ -4,6 +4,8 @@ import ( "fmt" "os" + "time" + "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/mcclient/auth" ) @@ -39,4 +41,13 @@ func InitAuth(options *Options, authComplete auth.AuthCompletedCallback) { auth.Init(a, false, true, options.SslCertfile, options.SslKeyfile) // , authComplete) authComplete() + + if options.GlobalVirtualResourceNamespace { + db.EnableGlobalVirtualResourceNamespace() + } + + if options.EnableRbac { + db.EnableGlobalRbac(time.Duration(options.RbacPolicySyncPeriodSeconds)*time.Second, + time.Duration(options.RbacPolicySyncFailedRetrySeconds)*time.Second) + } } diff --git a/pkg/cloudcommon/db/db_dispatcher.go b/pkg/cloudcommon/db/db_dispatcher.go index 427f6dfb48..334f246ece 100644 --- a/pkg/cloudcommon/db/db_dispatcher.go +++ b/pkg/cloudcommon/db/db_dispatcher.go @@ -401,7 +401,7 @@ func listItems(manager IModelManager, ctx context.Context, userCred mcclient.Tok return nil, err } totalCnt := int64(q.Count()) - log.Debugf("total count %d", totalCnt) + // log.Debugf("total count %d", totalCnt) if totalCnt == 0 { emptyList := modules.ListResult{Data: []jsonutils.JSONObject{}} return &emptyList, nil @@ -488,9 +488,19 @@ func calculateListResult(data []jsonutils.JSONObject, total, limit, offset int64 func (dispatcher *DBModelDispatcher) List(ctx context.Context, query jsonutils.JSONObject, ctxId string) (*modules.ListResult, error) { userCred := fetchUserCredential(ctx) - if !dispatcher.modelManager.AllowListItems(ctx, userCred, query) { + + var isAllow bool + if globalsRbacEnabled { + isAdmin := jsonutils.QueryBoolean(query, "admin", false) + isAllow = PolicyManager.Allow(isAdmin, userCred, GetGlobalServiceType(), + dispatcher.modelManager.KeywordPlural(), PolicyActionList) + } else { + isAllow = dispatcher.modelManager.AllowListItems(ctx, userCred, query) + } + if !isAllow { return nil, httperrors.NewForbiddenError("Not allow to list") } + items, err := listItems(dispatcher.modelManager, ctx, userCred, query, ctxId) if err != nil { log.Errorf("Fail to list items: %s", err) @@ -591,7 +601,13 @@ func (dispatcher *DBModelDispatcher) Get(ctx context.Context, idStr string, quer return nil, err } // log.Debugf("Get found %s", model) - if !model.AllowGetDetails(ctx, userCred, query) { + var isAllow bool + if globalsRbacEnabled { + isAllow = isRbacAllowed(dispatcher.modelManager, model, userCred, PolicyActionGet) + } else { + isAllow = model.AllowGetDetails(ctx, userCred, query) + } + if !isAllow { return nil, httperrors.NewForbiddenError("Not allow to get details") } return getItemDetails(dispatcher.modelManager, model, ctx, userCred, query) @@ -607,35 +623,44 @@ func (dispatcher *DBModelDispatcher) GetSpecific(ctx context.Context, idStr stri return nil, err } - specCamel := utils.Kebab2Camel(spec, "-") - funcName := fmt.Sprintf("AllowGetDetails%s", specCamel) - modelValue := reflect.ValueOf(model) - funcValue := modelValue.MethodByName(funcName) - if !funcValue.IsValid() || funcValue.IsNil() { - return nil, httperrors.NewSpecNotFoundError(fmt.Sprintf("%s %s %s not found", dispatcher.Keyword(), idStr, spec)) - } - params := []reflect.Value{ reflect.ValueOf(ctx), reflect.ValueOf(userCred), reflect.ValueOf(query), } - outs := funcValue.Call(params) - if len(outs) != 1 { - return nil, httperrors.NewInternalServerError("Invald %s return value", funcName) + specCamel := utils.Kebab2Camel(spec, "-") + modelValue := reflect.ValueOf(model) + + var isAllow bool + if globalsRbacEnabled { + isAllow = isRbacAllowed(dispatcher.modelManager, model, userCred, PolicyActionGet, spec) + } else { + funcName := fmt.Sprintf("AllowGetDetails%s", specCamel) + + funcValue := modelValue.MethodByName(funcName) + if !funcValue.IsValid() || funcValue.IsNil() { + return nil, httperrors.NewSpecNotFoundError(fmt.Sprintf("%s %s %s not found", dispatcher.Keyword(), idStr, spec)) + } + + outs := funcValue.Call(params) + if len(outs) != 1 { + return nil, httperrors.NewInternalServerError("Invald %s return value", funcName) + } + isAllow = outs[0].Bool() } - if !outs[0].Bool() { + + if !isAllow { return nil, httperrors.NewForbiddenError(fmt.Sprintf("%s not allow to get spec %s", dispatcher.Keyword(), spec)) } - funcName = fmt.Sprintf("GetDetails%s", specCamel) - funcValue = modelValue.MethodByName(funcName) + funcName := fmt.Sprintf("GetDetails%s", specCamel) + funcValue := modelValue.MethodByName(funcName) if !funcValue.IsValid() || funcValue.IsNil() { return nil, httperrors.NewSpecNotFoundError(fmt.Sprintf("%s %s %s not found", dispatcher.Keyword(), idStr, spec)) } - outs = funcValue.Call(params) + outs := funcValue.Call(params) if len(outs) != 2 { return nil, httperrors.NewInternalServerError("Invald %s return value", funcName) } @@ -664,7 +689,13 @@ func fetchOwnerProjectId(ctx context.Context, userCred mcclient.TokenCredential, if len(projId) == 0 { return userCred.GetProjectId(), nil } - if !userCred.IsSystemAdmin() { + var isAllow bool + if globalsRbacEnabled { + isAllow = PolicyManager.Allow(true, userCred, GetGlobalServiceType(), PolicyDelegation, "") + } else { + isAllow = userCred.IsSystemAdmin() + } + if !isAllow { return "", httperrors.NewForbiddenError("Delegation not allowed") } t, _ := TenantCacheManager.FetchTenantByIdOrName(ctx, projId) @@ -779,7 +810,13 @@ func (dispatcher *DBModelDispatcher) Create(ctx context.Context, query jsonutils lockman.LockClass(ctx, dispatcher.modelManager, ownerProjId) defer lockman.ReleaseClass(ctx, dispatcher.modelManager, ownerProjId) - if !dispatcher.modelManager.AllowCreateItem(ctx, userCred, query, data) { + var isAllow bool + if globalsRbacEnabled { + isAllow = isRbacAllowed(dispatcher.modelManager, nil, userCred, PolicyActionCreate) + } else { + isAllow = dispatcher.modelManager.AllowCreateItem(ctx, userCred, query, data) + } + if !isAllow { return nil, httperrors.NewForbiddenError("Not allow to create item") } @@ -837,7 +874,13 @@ func (dispatcher *DBModelDispatcher) BatchCreate(ctx context.Context, query json lockman.LockClass(ctx, dispatcher.modelManager, ownerProjId) defer lockman.ReleaseClass(ctx, dispatcher.modelManager, ownerProjId) - if !dispatcher.modelManager.AllowCreateItem(ctx, userCred, query, data) { + var isAllow bool + if globalsRbacEnabled { + isAllow = isRbacAllowed(dispatcher.modelManager, nil, userCred, PolicyActionCreate) + } else { + isAllow = dispatcher.modelManager.AllowCreateItem(ctx, userCred, query, data) + } + if !isAllow { return nil, httperrors.NewForbiddenError("Not allow to create item") } @@ -889,16 +932,30 @@ func (dispatcher *DBModelDispatcher) PerformClassAction(ctx context.Context, act lockman.LockClass(ctx, dispatcher.modelManager, ownerProjId) defer lockman.ReleaseClass(ctx, dispatcher.modelManager, ownerProjId) - managerValue := reflect.ValueOf(dispatcher.modelManager) if action == "check-create-data" { manager := dispatcher.modelManager - if body, err := data.(*jsonutils.JSONDict).Get(manager.Keyword()); err != nil { + + body, err := data.(*jsonutils.JSONDict).Get(manager.Keyword()) + if err != nil { return nil, httperrors.NewGeneralError(err) - } else { - return manager.ValidateCreateData(ctx, userCred, ownerProjId, query, body.(*jsonutils.JSONDict)) } + data := body.(*jsonutils.JSONDict) + + var isAllow bool + if globalsRbacEnabled { + isAllow = isRbacAllowed(manager, nil, userCred, PolicyActionPerform, action) + } else { + isAllow = manager.AllowPerformCheckCreateData(ctx, userCred, query, data) + } + if !isAllow { + return nil, httperrors.NewForbiddenError("not allow to perform %s", action) + } + + return manager.ValidateCreateData(ctx, userCred, ownerProjId, query, data) } - return objectPerformAction(dispatcher, managerValue, ctx, userCred, action, query, data) + + managerValue := reflect.ValueOf(dispatcher.modelManager) + return objectPerformAction(dispatcher, nil, managerValue, ctx, userCred, action, query, data) } func (dispatcher *DBModelDispatcher) PerformAction(ctx context.Context, idStr string, action string, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { @@ -915,7 +972,7 @@ func (dispatcher *DBModelDispatcher) PerformAction(ctx context.Context, idStr st defer lockman.ReleaseObject(ctx, model) modelValue := reflect.ValueOf(model) - result, err := objectPerformAction(dispatcher, modelValue, ctx, userCred, action, query, data) + result, err := objectPerformAction(dispatcher, model, modelValue, ctx, userCred, action, query, data) if err == nil && result == nil { return getItemDetails(dispatcher.modelManager, model, ctx, userCred, query) } else { @@ -923,23 +980,23 @@ func (dispatcher *DBModelDispatcher) PerformAction(ctx context.Context, idStr st } } -func objectPerformAction(dispatcher *DBModelDispatcher, modelValue reflect.Value, ctx context.Context, userCred mcclient.TokenCredential, action string, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { - isGeneral := false - +func objectPerformAction(dispatcher *DBModelDispatcher, model IModel, modelValue reflect.Value, ctx context.Context, userCred mcclient.TokenCredential, action string, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { const generalFuncName = "PerformAction" - const generalAllowFuncName = "AllowPerformAction" + // const generalAllowFuncName = "AllowPerformAction" + isGeneral := false funcName := fmt.Sprintf("Perform%s", utils.Kebab2Camel(action, "-")) - allowFuncName := "Allow" + funcName - funcValue := modelValue.MethodByName(allowFuncName) + + funcValue := modelValue.MethodByName(funcName) if !funcValue.IsValid() || funcValue.IsNil() { - funcValue = modelValue.MethodByName(generalAllowFuncName) + funcValue = modelValue.MethodByName(generalFuncName) if !funcValue.IsValid() || funcValue.IsNil() { - msg := fmt.Sprintf("%s allow perform action %s not found", dispatcher.Keyword(), action) + msg := fmt.Sprintf("%s perform action %s not found", dispatcher.Keyword(), action) log.Errorf(msg) return nil, httperrors.NewActionNotFoundError(msg) } else { isGeneral = true + funcName = generalFuncName } } @@ -962,25 +1019,30 @@ func objectPerformAction(dispatcher *DBModelDispatcher, modelValue reflect.Value } } - outs := funcValue.Call(params) - if len(outs) != 1 { - return nil, httperrors.NewInternalServerError("Invald %s return value", allowFuncName) + var isAllow bool + if globalsRbacEnabled { + isAllow = isRbacAllowed(dispatcher.modelManager, model, userCred, PolicyActionPerform, action) + } else { + allowFuncName := "Allow" + funcName + allowFuncValue := modelValue.MethodByName(allowFuncName) + if !allowFuncValue.IsValid() || allowFuncValue.IsNil() { + msg := fmt.Sprintf("%s allow perform action %s not found", dispatcher.Keyword(), action) + log.Errorf(msg) + return nil, httperrors.NewActionNotFoundError(msg) + } + + outs := funcValue.Call(params) + if len(outs) != 1 { + return nil, httperrors.NewInternalServerError("Invald %s return value", allowFuncName) + } + + isAllow = outs[0].Bool() } - if !outs[0].Bool() { + if !isAllow { return nil, httperrors.NewForbiddenError(fmt.Sprintf("%s not allow to perform action %s", dispatcher.Keyword(), action)) } - if isGeneral { - funcValue = modelValue.MethodByName(generalFuncName) - } else { - funcName = fmt.Sprintf("Perform%s", utils.Kebab2Camel(action, "-")) - funcValue = modelValue.MethodByName(funcName) - } - if !funcValue.IsValid() || funcValue.IsNil() { - return nil, httperrors.NewActionNotFoundError(fmt.Sprintf("%s perform action %s not found", dispatcher.Keyword(), action)) - } - - outs = funcValue.Call(params) + outs := funcValue.Call(params) if len(outs) != 2 { return nil, httperrors.NewInternalServerError("Invald %s return value", funcName) } @@ -1069,7 +1131,13 @@ func (dispatcher *DBModelDispatcher) Update(ctx context.Context, idStr string, q return nil, httperrors.NewGeneralError(err) } - if !model.AllowUpdateItem(ctx, userCred) { + var isAllow bool + if globalsRbacEnabled { + isAllow = isRbacAllowed(dispatcher.modelManager, model, userCred, PolicyActionUpdate) + } else { + isAllow = model.AllowUpdateItem(ctx, userCred) + } + if !isAllow { return nil, httperrors.NewForbiddenError(fmt.Sprintf("Not allow to update item")) } @@ -1098,7 +1166,14 @@ func DeleteModel(ctx context.Context, userCred mcclient.TokenCredential, item IM func deleteItem(manager IModelManager, model IModel, ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { log.Debugf("deleteItem %s", jsonutils.Marshal(model)) - if !model.AllowDeleteItem(ctx, userCred, query, data) { + + var isAllow bool + if globalsRbacEnabled { + isAllow = isRbacAllowed(manager, model, userCred, PolicyActionDelete) + } else { + isAllow = model.AllowDeleteItem(ctx, userCred, query, data) + } + if !isAllow { log.Errorf("not allow to delete") return nil, httperrors.NewForbiddenError(fmt.Sprintf("%s(%s) not allow to delete", manager.KeywordPlural(), model.GetId())) } diff --git a/pkg/cloudcommon/db/db_joint_dispatcher.go b/pkg/cloudcommon/db/db_joint_dispatcher.go index 236d6104f6..9d629f6b35 100644 --- a/pkg/cloudcommon/db/db_joint_dispatcher.go +++ b/pkg/cloudcommon/db/db_joint_dispatcher.go @@ -44,7 +44,7 @@ func (dispatcher *DBJointModelDispatcher) SlaveKeywordPlural() string { } func (dispatcher *DBJointModelDispatcher) ListMasterDescendent(ctx context.Context, idStr string, query jsonutils.JSONObject) (*modules.ListResult, error) { - log.Debugf("ListMasterDescendent %s %s", dispatcher.JointModelManager().GetMasterManager().Keyword(), idStr) + //log.Debugf("ListMasterDescendent %s %s", dispatcher.JointModelManager().GetMasterManager().Keyword(), idStr) userCred := fetchUserCredential(ctx) var queryDict *jsonutils.JSONDict @@ -71,8 +71,7 @@ func (dispatcher *DBJointModelDispatcher) ListMasterDescendent(ctx context.Conte } func (dispatcher *DBJointModelDispatcher) ListSlaveDescendent(ctx context.Context, idStr string, query jsonutils.JSONObject) (*modules.ListResult, error) { - log.Debugf("ListSlaveDescendent %s %s", dispatcher.JointModelManager().GetMasterManager().Keyword(), idStr) - + //log.Debugf("ListSlaveDescendent %s %s", dispatcher.JointModelManager().GetMasterManager().Keyword(), idStr) userCred := fetchUserCredential(ctx) var queryDict *jsonutils.JSONDict @@ -99,7 +98,15 @@ func (dispatcher *DBJointModelDispatcher) ListSlaveDescendent(ctx context.Contex } func (dispatcher *DBJointModelDispatcher) _listJoint(ctx context.Context, userCred mcclient.TokenCredential, ctxModel IModel, queryDict jsonutils.JSONObject) (*modules.ListResult, error) { - if !dispatcher.JointModelManager().AllowListDescendent(ctx, userCred, ctxModel, queryDict) { + var isAllow bool + if IsGlobalRbacEnabled() { + isAdmin := jsonutils.QueryBoolean(queryDict, "admin", false) + isAllow = PolicyManager.Allow(isAdmin, userCred, GetGlobalServiceType(), + dispatcher.JointModelManager().KeywordPlural(), PolicyActionList) + } else { + isAllow = dispatcher.JointModelManager().AllowListDescendent(ctx, userCred, ctxModel, queryDict) + } + if !isAllow { return nil, httperrors.NewForbiddenError("Not allow to list") } @@ -136,7 +143,13 @@ func (dispatcher *DBJointModelDispatcher) Get(ctx context.Context, id1 string, i } else if err != nil { return nil, httperrors.NewGeneralError(err) } - if !item.AllowGetJointDetails(ctx, userCred, query, item) { + var isAllow bool + if IsGlobalRbacEnabled() { + isAllow = isJointRbacAllowed(dispatcher.JointModelManager(), item, userCred, PolicyActionGet) + } else { + isAllow = item.AllowGetJointDetails(ctx, userCred, query, item) + } + if !isAllow { return nil, httperrors.NewForbiddenError("Not allow to get details") } return getItemDetails(dispatcher.JointModelManager(), item, ctx, userCred, query) @@ -201,7 +214,13 @@ func (dispatcher *DBJointModelDispatcher) Update(ctx context.Context, id1 string return nil, httperrors.NewGeneralError(err) } - if !item.AllowUpdateJointItem(ctx, userCred, item) { + var isAllow bool + if IsGlobalRbacEnabled() { + isAllow = isJointRbacAllowed(dispatcher.JointModelManager(), item, userCred, PolicyActionUpdate) + } else { + isAllow = item.AllowUpdateJointItem(ctx, userCred, item) + } + if !isAllow { return nil, httperrors.NewForbiddenError(fmt.Sprintf("Not allow to update item")) } diff --git a/pkg/cloudcommon/db/global.go b/pkg/cloudcommon/db/global.go index d9b2dd6a9c..86e3c59efc 100644 --- a/pkg/cloudcommon/db/global.go +++ b/pkg/cloudcommon/db/global.go @@ -1,9 +1,44 @@ package db +import "time" + /// Global virtual resource namespace -var globalVirtualResourceNamespace = false +var ( + globalVirtualResourceNamespace = false + + globalRegion = "" + + globalServiceType = "" + + globalsRbacEnabled = false +) func EnableGlobalVirtualResourceNamespace() { globalVirtualResourceNamespace = true } + +func SetGlobalRegion(region string) { + globalRegion = region +} + +func GetGlobalRegion() string { + return globalRegion +} + +func SetGlobalServiceType(srvType string) { + globalServiceType = srvType +} + +func GetGlobalServiceType() string { + return globalServiceType +} + +func EnableGlobalRbac(refreshInterval time.Duration, retryInterval time.Duration) { + globalsRbacEnabled = true + PolicyManager.start(refreshInterval, retryInterval) +} + +func IsGlobalRbacEnabled() bool { + return globalsRbacEnabled +} diff --git a/pkg/cloudcommon/db/interface.go b/pkg/cloudcommon/db/interface.go index 2262cebe28..d046ac6e26 100644 --- a/pkg/cloudcommon/db/interface.go +++ b/pkg/cloudcommon/db/interface.go @@ -54,6 +54,7 @@ type IModelManager interface { // allow perform action AllowPerformAction(ctx context.Context, userCred mcclient.TokenCredential, action string, query jsonutils.JSONObject, data jsonutils.JSONObject) bool + AllowPerformCheckCreateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool PerformAction(ctx context.Context, userCred mcclient.TokenCredential, action string, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) DoCreate(ctx context.Context, userCred mcclient.TokenCredential, kwargs jsonutils.JSONObject, data jsonutils.JSONObject, realManager IModelManager) (IModel, error) diff --git a/pkg/cloudcommon/db/modelbase.go b/pkg/cloudcommon/db/modelbase.go index 14c3090680..20c1e4544e 100644 --- a/pkg/cloudcommon/db/modelbase.go +++ b/pkg/cloudcommon/db/modelbase.go @@ -150,6 +150,10 @@ func (manager *SModelBaseManager) PerformAction(ctx context.Context, userCred mc return nil, nil } +func (manager *SModelBaseManager) AllowPerformCheckCreateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool { + return userCred.IsSystemAdmin() +} + func (manager *SModelBaseManager) InitializeData() error { return nil } diff --git a/pkg/cloudcommon/db/policy.go b/pkg/cloudcommon/db/policy.go new file mode 100644 index 0000000000..84bd91c678 --- /dev/null +++ b/pkg/cloudcommon/db/policy.go @@ -0,0 +1,207 @@ +package db + +import ( + "yunion.io/x/jsonutils" + "yunion.io/x/log" + + "time" + "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/rbacutils" +) + +const ( + PolicyDelegation = "delegate" + + PolicyActionList = "list" + PolicyActionGet = "get" + PolicyActionUpdate = "update" + PolicyActionPatch = "patch" + PolicyActionCreate = "create" + PolicyActionDelete = "delete" + PolicyActionPerform = "perform" +) + +var ( + PolicyManager *SPolicyManager + + PolicyFailedRetryInterval = 15 * time.Second + PolicyRefreshInterval = 15 * time.Minute +) + +func init() { + PolicyManager = &SPolicyManager{} +} + +type SPolicyManager struct { + policies map[string]rbacutils.SRbacPolicy + adminPolicies map[string]rbacutils.SRbacPolicy + lastSync time.Time +} + +func parseJsonPolicy(obj jsonutils.JSONObject) (string, rbacutils.SRbacPolicy, error) { + policy := rbacutils.SRbacPolicy{} + typeStr, err := obj.GetString("type") + if err != nil { + log.Errorf("get type error %s", err) + return "", policy, err + } + + blobStr, err := obj.GetString("blob") + if err != nil { + log.Errorf("get blob error %s", err) + return "", policy, err + } + blob, err := jsonutils.ParseString(blobStr) + if err != nil { + log.Errorf("parse blob json error %s", err) + return "", policy, err + } + err = policy.Decode(blob) + if err != nil { + log.Errorf("policy decode error %s", err) + return "", policy, err + } + + return typeStr, policy, nil +} + +func fetchPolicies() (map[string]rbacutils.SRbacPolicy, map[string]rbacutils.SRbacPolicy, error) { + s := auth.GetAdminSession(GetGlobalRegion(), "v1") + + policies := make(map[string]rbacutils.SRbacPolicy) + adminPolicies := make(map[string]rbacutils.SRbacPolicy) + + offset := 0 + for { + params := jsonutils.NewDict() + params.Add(jsonutils.NewInt(2048), "limit") + params.Add(jsonutils.NewInt(int64(offset)), "offset") + result, err := modules.Policies.ResourceManager.List(s, params) + + if err != nil { + log.Errorf("fetch policy failed") + return nil, nil, err + } + + 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 policy.IsAdmin { + adminPolicies[typeStr] = policy + } else { + policies[typeStr] = policy + } + } + + offset += len(result.Data) + if offset >= result.Total { + break + } + } + + return policies, adminPolicies, nil +} + +func (manager *SPolicyManager) start(refreshInterval time.Duration, retryInterval time.Duration) { + log.Infof("PolicyManager start to fetch policies ...") + PolicyRefreshInterval = refreshInterval + PolicyFailedRetryInterval = retryInterval + manager.sync() +} + +func (manager *SPolicyManager) sync() { + log.Debugf("start synchronize RBAC policies ...") + policies, adminPolicies, err := fetchPolicies() + if err != nil { + log.Errorf("sync policy fail %s", err) + time.AfterFunc(PolicyFailedRetryInterval, manager.sync) + return + } + manager.policies = policies + manager.adminPolicies = adminPolicies + manager.lastSync = time.Now() + time.AfterFunc(PolicyRefreshInterval, manager.sync) +} + +func (manager *SPolicyManager) Allow(isAdmin bool, userCred mcclient.TokenCredential, service string, resource string, action string, extra ...string) bool { + var policies map[string]rbacutils.SRbacPolicy + if isAdmin { + policies = manager.adminPolicies + } else { + policies = manager.policies + } + if policies == nil { + log.Warningf("no policies fetched") + return false + } + userCredJson := userCred.ToJson() + log.Debugf("%s", userCredJson) + for _, p := range policies { + if p.Allow(userCredJson, service, resource, action, extra...) { + return true + } + } + return false +} + +func (manager *SPolicyManager) explainPolicy(userCred mcclient.TokenCredential, policyReq jsonutils.JSONObject) (bool, error) { + policySeq, err := policyReq.GetArray() + if err != nil { + return false, httperrors.NewInputParameterError("invalid format") + } + isAdmin, _ := policySeq[0].Bool() + if !IsGlobalRbacEnabled() { + if !isAdmin || (isAdmin && userCred.IsSystemAdmin()) { + return true, nil + } else { + return false, httperrors.NewForbiddenError("operation not allowed") + } + } + 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 { + extra[i-4], _ = policySeq[i].GetString() + } + } + + return manager.Allow(isAdmin, userCred, service, resource, action, extra...), nil +} + +func (manager *SPolicyManager) ExplainRpc(userCred mcclient.TokenCredential, params jsonutils.JSONObject) (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 { + allow, err := manager.explainPolicy(userCred, policyReq) + if err != nil { + return nil, err + } + if allow { + ret.Add(jsonutils.JSONTrue, key) + } else { + ret.Add(jsonutils.JSONFalse, key) + } + } + return ret, nil +} diff --git a/pkg/cloudcommon/db/quotas/handler.go b/pkg/cloudcommon/db/quotas/handler.go index 6399dbe295..80bfeedd21 100644 --- a/pkg/cloudcommon/db/quotas/handler.go +++ b/pkg/cloudcommon/db/quotas/handler.go @@ -75,11 +75,33 @@ func getQuotaHanlder(ctx context.Context, w http.ResponseWriter, r *http.Request projectId := params[""] if len(projectId) == 0 { projectId = userCred.GetProjectId() + if db.IsGlobalRbacEnabled() { + if !db.PolicyManager.Allow(false, userCred, db.GetGlobalServiceType(), + "quotas", db.PolicyActionGet) { + httperrors.ForbiddenError(w, "not allow to get quota") + return + } + } } else { - if !userCred.IsSystemAdmin() { - httperrors.ForbiddenError(w, "not allow to query quota") + isAllow := false + if db.IsGlobalRbacEnabled() { + isAllow = db.PolicyManager.Allow(true, userCred, db.GetGlobalServiceType(), + db.PolicyDelegation, db.PolicyActionGet) + } else { + isAllow = userCred.IsSystemAdmin() + } + if !isAllow { + httperrors.ForbiddenError(w, "not allow to delegate query quota") return } + if db.IsGlobalRbacEnabled() { + if !db.PolicyManager.Allow(true, userCred, db.GetGlobalServiceType(), + "quotas", db.PolicyActionGet) { + httperrors.ForbiddenError(w, "not allow to query quota") + return + } + } + tenant, err := db.TenantCacheManager.FetchTenantByIdOrName(ctx, projectId) if err != nil { if err == sql.ErrNoRows { @@ -107,7 +129,15 @@ func getQuotaHanlder(ctx context.Context, w http.ResponseWriter, r *http.Request func setQuotaHanlder(ctx context.Context, w http.ResponseWriter, r *http.Request) { userCred := auth.FetchUserCredential(ctx) - if !userCred.IsSystemAdmin() { + + var isAllow bool + if db.IsGlobalRbacEnabled() { + isAllow = db.PolicyManager.Allow(true, userCred, db.GetGlobalServiceType(), + "quotas", db.PolicyActionUpdate) + } else { + isAllow = userCred.IsSystemAdmin() + } + if !isAllow { httperrors.ForbiddenError(w, "not allow to set quota") return } @@ -162,10 +192,26 @@ func setQuotaHanlder(ctx context.Context, w http.ResponseWriter, r *http.Request func checkQuotaHanlder(ctx context.Context, w http.ResponseWriter, r *http.Request) { userCred := auth.FetchUserCredential(ctx) - if !userCred.IsSystemAdmin() { - httperrors.ForbiddenError(w, "not allow to set quota") + + isAllow := false + if db.IsGlobalRbacEnabled() { + isAllow = db.PolicyManager.Allow(true, userCred, db.GetGlobalServiceType(), + db.PolicyDelegation, db.PolicyActionGet) + } else { + isAllow = userCred.IsSystemAdmin() + } + if !isAllow { + httperrors.ForbiddenError(w, "not allow to delegate check quota") return } + if db.IsGlobalRbacEnabled() { + if !db.PolicyManager.Allow(true, userCred, db.GetGlobalServiceType(), + "quotas", db.PolicyActionGet) { + httperrors.ForbiddenError(w, "not allow to query quota") + return + } + } + params := appctx.AppContextParams(ctx) projectId := params[""] if len(projectId) == 0 { diff --git a/pkg/cloudcommon/db/rbac.go b/pkg/cloudcommon/db/rbac.go new file mode 100644 index 0000000000..a6994279c2 --- /dev/null +++ b/pkg/cloudcommon/db/rbac.go @@ -0,0 +1,55 @@ +package db + +import ( + "yunion.io/x/onecloud/pkg/mcclient" +) + +func isRbacAllowed(manager IModelManager, model IModel, userCred mcclient.TokenCredential, action string, extra ...string) bool { + var isAllow bool + var isAdmin bool + if model == nil { + _, ok := manager.(IVirtualModelManager) + if ok { + isAdmin = false + } + } else { + virtModel, ok := model.(IVirtualModel) + if ok { + if virtModel.IsOwner(userCred) { + isAdmin = false + } else { + isAdmin = true + } + } else { + isAdmin = true + } + } + if !isAdmin { + isAllow = PolicyManager.Allow(false, userCred, GetGlobalServiceType(), + manager.KeywordPlural(), action, extra...) + } + if !isAllow { + isAllow = PolicyManager.Allow(true, userCred, GetGlobalServiceType(), + manager.KeywordPlural(), action, extra...) + } + return isAllow +} + +func isJointRbacAllowed(manager IJointModelManager, item IJointModel, userCred mcclient.TokenCredential, action string, extra ...string) bool { + isAllow := false + isAdmin := true + master := item.Master() + virtualMaster, ok := master.(IVirtualModel) + if ok && virtualMaster.IsOwner(userCred) { + isAdmin = false + } + if !isAdmin { + isAllow = PolicyManager.Allow(false, userCred, GetGlobalServiceType(), + manager.KeywordPlural(), action, extra...) + } + if !isAllow { + isAllow = PolicyManager.Allow(true, userCred, GetGlobalServiceType(), + manager.KeywordPlural(), action, extra...) + } + return isAllow +} diff --git a/pkg/cloudcommon/db/tenantcache.go b/pkg/cloudcommon/db/tenantcache.go index bb756a7b63..3bb9bfb0c2 100644 --- a/pkg/cloudcommon/db/tenantcache.go +++ b/pkg/cloudcommon/db/tenantcache.go @@ -77,7 +77,7 @@ func (manager *STenantCacheManager) FetchTenantByName(ctx context.Context, idStr } func (manager *STenantCacheManager) fetchTenantFromKeystone(ctx context.Context, idStr string) (*STenant, error) { - s := auth.GetAdminSession("", "v1") + s := auth.GetAdminSession(GetGlobalRegion(), "v1") tenant, err := modules.Projects.Get(s, idStr, nil) if err != nil { log.Errorf("fetch project fail %s", err) diff --git a/pkg/cloudcommon/options.go b/pkg/cloudcommon/options.go index 39b0141f6c..a28253eb80 100644 --- a/pkg/cloudcommon/options.go +++ b/pkg/cloudcommon/options.go @@ -40,6 +40,10 @@ type Options struct { SslCertfile string `help:"ssl certification file"` SslKeyfile string `help:"ssl certification key file"` + EnableRbac bool `help:"Switch on Role-based Access Control" default:"false"` + RbacPolicySyncPeriodSeconds int `help:"policy sync interval in seconds, default 15 minutes" default:"900"` + RbacPolicySyncFailedRetrySeconds int `help:"seconds to wait after a failed sync, default 30 seconds" default:"30"` + structarg.BaseOptions } diff --git a/pkg/compute/models/quotas.go b/pkg/compute/models/quotas.go index 152095735e..3e6c0f4a38 100644 --- a/pkg/compute/models/quotas.go +++ b/pkg/compute/models/quotas.go @@ -91,7 +91,7 @@ func (self *SQuota) FetchUsage(projectId string) error { self.Bw = net.InternalBandwidth self.Ebw = net.ExternalBandwidth self.Keypair = 0 // keypair - s := auth.GetAdminSession("", "") + s := auth.GetAdminSession(options.Options.Region, "") self.Image, _ = modules.Images.GetPrivateImageCount(s, projectId, true) self.Group = 0 self.Secgroup = totalSecurityGroupCount(projectId) diff --git a/pkg/compute/service/service.go b/pkg/compute/service/service.go index c33e89f0ce..e5de80bc75 100644 --- a/pkg/compute/service/service.go +++ b/pkg/compute/service/service.go @@ -24,6 +24,8 @@ import ( ) func StartService() { + db.SetGlobalServiceType("compute") + cloudcommon.ParseOptions(&options.Options, &options.Options.Options, os.Args, "region.conf") if options.Options.DebugSqlchemy { @@ -39,10 +41,6 @@ func StartService() { log.Infof("Auth complete!!") }) - if options.Options.GlobalVirtualResourceNamespace { - db.EnableGlobalVirtualResourceNamespace() - } - cloudcommon.InitDB(&options.Options.DBOptions) defer cloudcommon.CloseDB() diff --git a/pkg/compute/usages/handler.go b/pkg/compute/usages/handler.go index 5bc4f607b4..71899df1ea 100644 --- a/pkg/compute/usages/handler.go +++ b/pkg/compute/usages/handler.go @@ -243,12 +243,33 @@ func getCommonGeneralUsage(cred mcclient.TokenCredential, rangeObj db.IStandalon func ReportGeneralUsage(userCred mcclient.TokenCredential, rangeObj db.IStandaloneModel, hostTypes []string) (count Usage, err error) { count = make(map[string]interface{}) - if userCred.IsSystemAdmin() { + + isAdmin := false + + if db.IsGlobalRbacEnabled() { + if db.PolicyManager.Allow(true, userCred, db.GetGlobalServiceType(), + "usages", db.PolicyActionGet) { + isAdmin = true + } + } else { + isAdmin = userCred.IsSystemAdmin() + } + + if isAdmin { count, err = getAdminGeneralUsage(userCred, rangeObj, hostTypes) if err != nil { return } } + + if db.IsGlobalRbacEnabled() { + if !db.PolicyManager.Allow(false, userCred, db.GetGlobalServiceType(), + "usages", db.PolicyActionGet) { + err = httperrors.NewForbiddenError("not allow to get usages") + return + } + } + commonUsage, err := getCommonGeneralUsage(userCred, rangeObj, hostTypes) if err != nil { return diff --git a/pkg/mcclient/auth/auth.go b/pkg/mcclient/auth/auth.go index 58a50b4a07..2fada83d1d 100644 --- a/pkg/mcclient/auth/auth.go +++ b/pkg/mcclient/auth/auth.go @@ -269,3 +269,15 @@ func GetAdminSession(region string, apiVersion string) *mcclient.ClientSession { func GetSession(token mcclient.TokenCredential, region string, apiVersion string) *mcclient.ClientSession { return manager.client.NewSession(region, "", "internal", token, apiVersion) } + +// use for climc test only +func InitFromClientSession(session *mcclient.ClientSession) { + cli := session.GetClient() + token := session.GetToken() + info := &AuthInfo{} + manager = &authManager{ + client: cli, + info: info, + adminCredential: token, + } +} diff --git a/pkg/mcclient/modules/base.go b/pkg/mcclient/modules/base.go index 231fae5e13..3759b231e9 100644 --- a/pkg/mcclient/modules/base.go +++ b/pkg/mcclient/modules/base.go @@ -8,7 +8,6 @@ import ( "strings" "yunion.io/x/jsonutils" - "yunion.io/x/log" "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/util/httputils" ) @@ -53,7 +52,7 @@ func (this *BaseManager) versionedURL(path string) string { } else { ret = fmt.Sprintf("/%s", path[offset:]) } - log.Debugf("versionedURL %s %s => %s", this.version, path, ret) + // log.Debugf("versionedURL %s %s => %s", this.version, path, ret) return ret } diff --git a/pkg/mcclient/modules/mod_policies.go b/pkg/mcclient/modules/mod_policies.go index eda1cf0d4c..1cf6f6b133 100644 --- a/pkg/mcclient/modules/mod_policies.go +++ b/pkg/mcclient/modules/mod_policies.go @@ -1,11 +1,143 @@ package modules -var Policies ResourceManager +import ( + "fmt" + + "yunion.io/x/jsonutils" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/util/httputils" +) + +type SPolicyManager struct { + ResourceManager +} + +var Policies SPolicyManager + +func translateS2C(s jsonutils.JSONObject) (jsonutils.JSONObject, error) { + ss := s.(*jsonutils.JSONDict) + ret := ss.CopyIncludes("id", "type") + blobStr, err := ss.GetString("blob") + if err != nil { + return nil, err + } + blobJson, err := jsonutils.ParseString(blobStr) + if err != nil { + return nil, err + } + ret.Add(jsonutils.NewString(blobJson.YAMLString()), "policy") + return ret, nil +} + +func translateC2S(s jsonutils.JSONObject) (jsonutils.JSONObject, error) { + ret := jsonutils.NewDict() + if s.Contains("policy") { + blobYaml, err := s.GetString("policy") + if err != nil { + return nil, err + } + blobJson, err := jsonutils.ParseYAML(blobYaml) + if err != nil { + return nil, err + } + ret.Add(jsonutils.NewString(blobJson.String()), "blob") + } + if s.Contains("type") { + typeStr, err := s.GetString("type") + if err != nil { + return nil, err + } + ret.Add(jsonutils.NewString(typeStr), "type") + } + return ret, nil +} + +func (pm *SPolicyManager) Get(s *mcclient.ClientSession, id string, query jsonutils.JSONObject) (jsonutils.JSONObject, error) { + result, err := pm.GetById(s, id, nil) + if err == nil { + return result, nil + } + jsonErr := err.(*httputils.JSONClientError) + if jsonErr.Code != 404 { + return nil, err + } + return pm.GetByName(s, id, query) +} + +func (pm *SPolicyManager) GetId(s *mcclient.ClientSession, id string, params jsonutils.JSONObject) (string, error) { + obj, err := pm.Get(s, id, params) + if err != nil { + return "", err + } + return obj.GetString("id") +} + +func (pm *SPolicyManager) GetById(s *mcclient.ClientSession, id string, query jsonutils.JSONObject) (jsonutils.JSONObject, error) { + result, err := pm.ResourceManager.GetById(s, id, query) + if err != nil { + return nil, err + } + return translateS2C(result) +} + +func (pm *SPolicyManager) GetByName(s *mcclient.ClientSession, name string, query jsonutils.JSONObject) (jsonutils.JSONObject, error) { + params := jsonutils.NewDict() + params.Add(jsonutils.NewString(name), "type") + listResult, err := Policies.List(s, params) + if err != nil { + return nil, err + } + if listResult.Total == 1 { + return listResult.Data[0], nil + } else if listResult.Total == 0 { + return nil, &httputils.JSONClientError{Code: 404, Class: "NotFound", + Details: fmt.Sprintf("%d not found", name)} + } else { + return nil, &httputils.JSONClientError{Code: 409, Class: "Conflict", + Details: fmt.Sprintf("multiple %d found", name)} + } +} + +func (pm *SPolicyManager) Create(s *mcclient.ClientSession, params jsonutils.JSONObject) (jsonutils.JSONObject, error) { + data, err := translateC2S(params) + if err != nil { + return nil, err + } + result, err := pm.ResourceManager.Create(s, data) + if err != nil { + return nil, err + } + return translateS2C(result) +} + +func (pm *SPolicyManager) Patch(s *mcclient.ClientSession, id string, params jsonutils.JSONObject) (jsonutils.JSONObject, error) { + data, err := translateC2S(params) + if err != nil { + return nil, err + } + result, err := pm.ResourceManager.Patch(s, id, data) + if err != nil { + return nil, err + } + return translateS2C(result) +} + +func (pm *SPolicyManager) List(s *mcclient.ClientSession, params jsonutils.JSONObject) (*ListResult, error) { + results, err := pm.ResourceManager.List(s, params) + if err != nil { + return nil, err + } + for i := 0; i < len(results.Data); i += 1 { + val, _ := translateS2C(results.Data[i]) + results.Data[i] = val + } + return results, nil +} func init() { - Policies = NewIdentityV3Manager("policy", "policies", - []string{}, - []string{}) + Policies = SPolicyManager{NewIdentityV3Manager("policy", "policies", + []string{"id", "type", "policy"}, + []string{})} register(&Policies) } diff --git a/pkg/mcclient/modules/resource.go b/pkg/mcclient/modules/resource.go index ca10923f0c..c1c0537cb4 100644 --- a/pkg/mcclient/modules/resource.go +++ b/pkg/mcclient/modules/resource.go @@ -136,7 +136,7 @@ func (this *ResourceManager) GetIdInContext(session *mcclient.ClientSession, id } func (this *ResourceManager) GetIdInContexts(session *mcclient.ClientSession, id string, params jsonutils.JSONObject, ctxs []ManagerContext) (string, error) { - obj, e := this.Get(session, id, params) + obj, e := this.GetInContexts(session, id, params, ctxs) if e != nil { return "", e } diff --git a/pkg/mcclient/session.go b/pkg/mcclient/session.go index 624badf80c..95bf5ff367 100644 --- a/pkg/mcclient/session.go +++ b/pkg/mcclient/session.go @@ -241,3 +241,7 @@ func (this *ClientSession) ToJson() jsonutils.JSONObject { } return params } + +func (cs *ClientSession) GetToken() TokenCredential { + return cs.token +} diff --git a/pkg/mcclient/token.go b/pkg/mcclient/token.go index c7eed77a2b..6aef27acd7 100644 --- a/pkg/mcclient/token.go +++ b/pkg/mcclient/token.go @@ -54,4 +54,6 @@ type TokenCredential interface { GetInternalServices(region string) []string GetExternalServices(region string) []ExternalService GetEndpoints(region string, endpointType string) []Endpoint + + ToJson() jsonutils.JSONObject } diff --git a/pkg/mcclient/token2.go b/pkg/mcclient/token2.go index 109b0e4bab..f2a8eb8a43 100644 --- a/pkg/mcclient/token2.go +++ b/pkg/mcclient/token2.go @@ -260,3 +260,7 @@ func (self *TokenCredentialV2) String() string { func (self *TokenCredentialV2) IsZero() bool { return len(self.GetUserId()) == 0 && len(self.GetProjectId()) == 0 } + +func (self *TokenCredentialV2) ToJson() jsonutils.JSONObject { + return SimplifyToken(self).ToJson() +} diff --git a/pkg/mcclient/token3.go b/pkg/mcclient/token3.go index 5502571bf6..bc172bf78d 100644 --- a/pkg/mcclient/token3.go +++ b/pkg/mcclient/token3.go @@ -344,3 +344,7 @@ func (self *TokenCredentialV3) String() string { func (self *TokenCredentialV3) IsZero() bool { return len(self.GetUserId()) == 0 && len(self.GetProjectId()) == 0 } + +func (self *TokenCredentialV3) ToJson() jsonutils.JSONObject { + return SimplifyToken(self).ToJson() +} diff --git a/pkg/mcclient/tokensimple.go b/pkg/mcclient/tokensimple.go index 308de2138a..f10c2b73fa 100644 --- a/pkg/mcclient/tokensimple.go +++ b/pkg/mcclient/tokensimple.go @@ -138,13 +138,17 @@ func (self *SSimpleToken) GetCatalogData(serviceTypes []string, region string) j } func (self *SSimpleToken) String() string { - return jsonutils.Marshal(self).String() + return self.ToJson().String() } func (self *SSimpleToken) IsZero() bool { return len(self.UserId) == 0 && len(self.ProjectId) == 0 } +func (self *SSimpleToken) ToJson() jsonutils.JSONObject { + return jsonutils.Marshal(self) +} + var TokenCredentialType reflect.Type func init() { diff --git a/pkg/util/conditionparser/parser.go b/pkg/util/conditionparser/parser.go index 6002933563..3354e97924 100644 --- a/pkg/util/conditionparser/parser.go +++ b/pkg/util/conditionparser/parser.go @@ -33,6 +33,9 @@ func IsValid(exprStr string) bool { } func Eval(exprStr string, input interface{}) (bool, error) { + if len(exprStr) == 0 { + return true, nil + } expr, err := parser.ParseExpr(exprStr) if err != nil { log.Errorf("parse expr %s error %s", exprStr, err) @@ -252,6 +255,11 @@ func evalCallInternal(funcV interface{}, args []interface{}) (interface{}, error return nil, ErrFuncArgument } return jsonX.Size(), nil + case "keys": + if len(args) > 0 { + return nil, ErrFuncArgument + } + return jsonutils.NewStringArray(jsonX.SortedKeys()), nil default: return nil, ErrMethodNotFound } @@ -263,6 +271,31 @@ func evalCallInternal(funcV interface{}, args []interface{}) (interface{}, error return nil, ErrFuncArgument } return len(array), nil + case "contains": + if len(args) < 1 { + return nil, ErrFuncArgument + } + for j := 0; j < len(args); j += 1 { + find := false + for i := 0; i < len(array); i += 1 { + findInf, err := evalBinaryInternal(array[i], args[j], token.EQL) + if err != nil { + return nil, err + } + switch findInf.(type) { + case bool: + if findInf.(bool) { + find = true + } + default: + return nil, ErrInvalidOp + } + } + if !find { + return false, nil + } + } + return true, nil default: return nil, ErrMethodNotFound } @@ -358,7 +391,7 @@ func evalSelectorInternal(X interface{}, identStr string) (interface{}, error) { return ret, err } case []interface{}, *jsonutils.JSONArray: - if identStr == "len" { + if identStr == "len" || identStr == "contains" { return &funcCaller{caller: X, method: identStr}, nil } arrX := getArray(X) diff --git a/pkg/util/conditionparser/parser_test.go b/pkg/util/conditionparser/parser_test.go index 7abe48ef7d..156a0c426d 100644 --- a/pkg/util/conditionparser/parser_test.go +++ b/pkg/util/conditionparser/parser_test.go @@ -29,6 +29,9 @@ func TestAst(t *testing.T) { {`server.disks[0].contains("medium_type")`, true}, {`server.disk[0].contains("medium_type")`, true}, {`server.disks[0].contains("backend")`, false}, + {`server.keys().contains("os_type", "disks")`, true}, + {`server.keys() == "os_type"`, true}, + {`server.keys() == "os_type1"`, false}, } for _, c := range cases { diff --git a/pkg/util/rbacutils/rabc.go b/pkg/util/rbacutils/rabc.go new file mode 100644 index 0000000000..a586a42dcf --- /dev/null +++ b/pkg/util/rbacutils/rabc.go @@ -0,0 +1,340 @@ +package rbacutils + +import ( + "fmt" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/onecloud/pkg/util/conditionparser" +) + +type TRbacResult string + +const ( + WILD_MATCH = "*" + Allow = TRbacResult("allow") + Deny = TRbacResult("deny") +) + +type SRbacPolicy struct { + Condition string + IsAdmin bool + Rules []SRbacRule +} + +type SRbacRule struct { + Service string + Resource string + Action string + Extra []string + Result TRbacResult +} + +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) 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 { + maxMatchCnt := 0 + minWeight := 1000000 + var matchRule *SRbacRule + for i := 0; i < len(policy.Rules); i += 1 { + match, matchCnt, weight := policy.Rules[i].match(service, resource, action, extra...) + if match && (maxMatchCnt < matchCnt || (maxMatchCnt == matchCnt && minWeight > weight)) { + maxMatchCnt = matchCnt + minWeight = weight + matchRule = &policy.Rules[i] + } + } + return matchRule +} + +func compactRules(rules []SRbacRule) []SRbacRule { + 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 output +} + +func (policy *SRbacPolicy) Decode(policyJson jsonutils.JSONObject) error { + policy.Condition, _ = policyJson.GetString("condition") + policy.IsAdmin = jsonutils.QueryBoolean(policyJson, "is_admin", false) + + ruleJson, err := policyJson.Get("policy") + if err != nil { + return err + } + + rules, err := decode(ruleJson, SRbacRule{}, levelService) + if err != nil { + return err + } + + policy.Rules = compactRules(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() + if ruleStr == string(Allow) { + decodeRule.Result = Allow + } else if ruleStr == string(Deny) { + decodeRule.Result = Deny + } else { + 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, fmt.Errorf("get rule map fail %s", err) + } + 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, err + } + rules = append(rules, decoded...) + } + return rules, nil + default: + return nil, fmt.Errorf("unsupport rule data %s", 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 fmt.Errorf("conflict?") + case *jsonutils.JSONDict: + nextJsonDict := nextJson.(*jsonutils.JSONDict) + addRule2Json(nextJsonDict, []string{WILD_MATCH}, result) + return nil + default: + return 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 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, err + } + } + + 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(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 (policy *SRbacPolicy) Allow(userCred jsonutils.JSONObject, service, resource, action string, extra ...string) bool { + if len(policy.Condition) > 0 { + match, err := conditionparser.Eval(policy.Condition, userCred) + if err != nil { + log.Errorf("eval condition %s fail %s", policy.Condition, err) + return false + } + if !match { + return false + } + } + rule := policy.GetMatchRule(service, resource, action, extra...) + if rule == nil { + return false + } + if rule.Result == Deny { + return false + } + return true +} diff --git a/pkg/util/rbacutils/rabc_test.go b/pkg/util/rbacutils/rabc_test.go new file mode 100644 index 0000000000..0edcb3ff7b --- /dev/null +++ b/pkg/util/rbacutils/rabc_test.go @@ -0,0 +1,282 @@ +package rbacutils + +import ( + "testing" + + "yunion.io/x/jsonutils" +) + +func TestSRabcRule_Match(t *testing.T) { + all := SRbacRule{Service: "*", Resource: "*", Action: "*"} + compute := SRbacRule{Service: "compute", Resource: "*", Action: "*"} + getOnly := SRbacRule{Service: "*", Resource: "*", Action: "get"} + listOnly := SRbacRule{Service: "*", Resource: "*", Action: "list"} + serverList := SRbacRule{Service: "compute", Resource: "server", Action: "list"} + serverPerform := SRbacRule{Service: "compute", Resource: "server", Action: "perform", Extra: []string{"*"}} + + rule_server_list := []string{"compute", "server", "list"} + rule_server_perform_start := []string{"compute", "server", "perform", "start"} + rule_server_create := []string{"compute", "server", "create"} + + cases := []struct { + inRule SRbacRule + inMatch []string + want bool + count int + }{ + {all, rule_server_list, true, 0}, + {all, rule_server_perform_start, true, 0}, + {all, rule_server_create, true, 0}, + {compute, rule_server_list, true, 1}, + {compute, rule_server_perform_start, true, 1}, + {compute, rule_server_create, true, 1}, + {getOnly, rule_server_list, false, 0}, + {getOnly, rule_server_perform_start, false, 0}, + {getOnly, rule_server_create, false, 0}, + {listOnly, rule_server_list, true, 1}, + {listOnly, rule_server_perform_start, false, 0}, + {listOnly, rule_server_create, false, 0}, + {serverList, rule_server_list, true, 3}, + {serverList, rule_server_perform_start, false, 0}, + {serverList, rule_server_create, false, 0}, + {serverPerform, rule_server_list, false, 0}, + {serverPerform, rule_server_perform_start, true, 3}, + {serverPerform, rule_server_create, false, 0}, + } + + for _, c := range cases { + got, cnt, _ := c.inRule.match(c.inMatch[0], c.inMatch[1], c.inMatch[2], c.inMatch[3:]...) + if got != c.want { + t.Errorf("%#v %#v want %#v got %#v", c.inRule, c.inMatch, c.want, got) + } + if cnt != c.count { + t.Errorf("%#v %#v want %#v got %#v", c.inRule, c.inMatch, c.count, cnt) + } + } +} + +func TestContains(t *testing.T) { + cases := []struct { + left SRbacRule + right SRbacRule + contains bool + }{ + { + SRbacRule{Service: "*", Resource: "*", Action: "*", Result: Allow}, + SRbacRule{Service: "*", Resource: "*", Action: "*", Result: Allow}, + true, + }, + { + SRbacRule{Service: "*", Resource: "*", Action: "*", Result: Allow}, + SRbacRule{Service: "compute", Resource: "*", Action: "*", Result: Allow}, + true, + }, + { + SRbacRule{Service: "*", Resource: "*", Action: "*", Result: Allow}, + SRbacRule{Service: "compute", Resource: "server", Action: "*", Result: Allow}, + true, + }, + { + SRbacRule{Service: "*", Resource: "*", Action: "*", Result: Allow}, + SRbacRule{Service: "compute", Resource: "server", Action: "list", Result: Allow}, + true, + }, + { + SRbacRule{Service: "*", Resource: "*", Action: "*", Result: Allow}, + SRbacRule{Service: "compute", Resource: "server", Action: "get", Extra: []string{"vnc"}, Result: Allow}, + true, + }, + { + SRbacRule{Service: "compute", Resource: "*", Action: "*", Result: Allow}, + SRbacRule{Service: "*", Resource: "*", Action: "*", Result: Allow}, + false, + }, + { + SRbacRule{Service: "compute", Resource: "server", Action: "*", Result: Allow}, + SRbacRule{Service: "*", Resource: "*", Action: "*", Result: Allow}, + false, + }, + { + SRbacRule{Service: "compute", Resource: "server", Action: "list", Result: Allow}, + SRbacRule{Service: "*", Resource: "*", Action: "*", Result: Allow}, + false, + }, + { + SRbacRule{Service: "compute", Resource: "server", Action: "get", Extra: []string{"vnc"}, Result: Allow}, + SRbacRule{Service: "*", Resource: "*", Action: "*", Result: Allow}, + false, + }, + } + + for _, c := range cases { + got := c.left.contains(&c.right) + if got != c.contains { + t.Errorf("%s contains %s want %#v got %#v", c.left, c.right, c.contains, got) + } + } +} + +func TestSRabcPolicy_Encode(t *testing.T) { + policyStr := `{ + "condition": "usercred.project != \"system\" && usercred.roles==\"projectowner\"", + "is_admin": false, + "policy": { + "compute": { + "keypair": "allow", + "server": "deny", + "*": { + "*": "allow", + "create": "deny" + } + }, + "meter": { + "*": "allow" + } + } + }` + policyJson, err := jsonutils.ParseString(policyStr) + if err != nil { + t.Errorf("fail to parse json string %s", err) + return + } + + policy := SRbacPolicy{} + + err = policy.Decode(policyJson) + if err != nil { + t.Errorf("decode error %s", err) + return + } + + policyJson1, err := policy.Encode() + if err != nil { + t.Errorf("encode error %s", err) + return + } + + policy2 := SRbacPolicy{} + + err = policy2.Decode(policyJson1) + if err != nil { + t.Errorf("decode error 2 %s", err) + return + } + + policyJson2, err := policy2.Encode() + if err != nil { + t.Errorf("encode error 2 %s", err) + return + } + + policyStr1 := policyJson1.PrettyString() + policyStr2 := policyJson2.PrettyString() + + if policyStr1 != policyStr2 { + t.Errorf("%s != %s", policyStr1, policyStr2) + return + } + + t.Logf("%s", policyStr1) +} + +func TestSRbacPolicy_Allow(t *testing.T) { + userCredStr := `{"domain":"Default","domain_id":"default","expires":"2018-10-28T05:33:54.000000Z","roles":"admin,teamleader","tenant":"system","tenant_id":"5d65667d112e47249ae66dbd7bc07030","token":"gAAAAABb0_jC2Qz2PpB00-pieLi4exKXq4O3QrvoqerpqoSxbp9pOLLdNWaAg0cPcd8eAjkiPhSo7VWQAVodoxnad95LdNbUf_1It8R_wXVDtO20caB7oLcas1oQt8b1cG0a7qagauP0iWVSW_dq_e92rD5Hd3SHn3Lw6ycrp_eHLskz_8EbPiI","user":"sysadmin","user_id":"dddf386b6ff24572b2e6a771d768495e"}` + + userCred, err := jsonutils.ParseString(userCredStr) + if err != nil { + t.Errorf("parse json fail %s", err) + return + } + + cases := []struct { + policy string + ops []string + want bool + }{ + { + `{ + "condition": "tenant==\"system\" && roles.contains(\"admin\")", + "is_admin": true, + "policy": { + "*": "allow" + } +}`, + []string{"compute", "servers", "list"}, + true, + }, + { + `{"is_admin":"false","policy":{"*":{"*":{"*":"allow","delete":"deny"}}}}`, + []string{"compute", "servers", "delete"}, + false, + }, + } + + for _, c := range cases { + policyJson, err := jsonutils.ParseString(c.policy) + if err != nil { + t.Errorf("fail to parse json string %s", err) + return + } + policy := SRbacPolicy{} + + err = policy.Decode(policyJson) + if err != nil { + t.Errorf("decode error %s", err) + return + } + + isAllow := policy.Allow(userCred, c.ops[0], c.ops[1], c.ops[2]) + if isAllow != c.want { + t.Errorf("%s %#v expect %#v, but get %#v", policyJson.String(), c.ops, c.want, isAllow) + return + } + } +} + +func TestSRabcPolicy_Explain(t *testing.T) { + policyStr := `{ + "condition": "usercred.project != \"system\" && usercred.roles==\"projectowner\"", + "is_admin": false, + "policy": { + "compute": { + "keypair": "allow", + "server": "deny", + "*": { + "*": "allow", + "create": "deny" + } + }, + "meter": { + "*": "allow" + }, + "k8s": "allow" + } + }` + policyJson, err := jsonutils.ParseString(policyStr) + if err != nil { + t.Errorf("fail to parse json string %s", err) + return + } + + policy := SRbacPolicy{} + + err = policy.Decode(policyJson) + if err != nil { + t.Errorf("decode error %s", err) + return + } + + request := [][]string{ + {"compute", "keypair", "list"}, + {"compute", "server", "list"}, + {"compute", "server", "get", "vnc"}, + {"compute", "keypair", "create"}, + {"meter", "price", "list"}, + {"image", "image", "list"}, + {"k8s", "pod", "list"}, + } + + output := policy.Explain(request) + + t.Logf("%#v", output) +} diff --git a/scripts/rbac/member.yaml b/scripts/rbac/member.yaml new file mode 100644 index 0000000000..7cc23ef8a5 --- /dev/null +++ b/scripts/rbac/member.yaml @@ -0,0 +1,8 @@ +# rbac for normal user, not allow for delete +is_admin: false +policy: + *: + *: + *: allow + create: deny + delete: deny diff --git a/scripts/rbac/projectowner.yaml b/scripts/rbac/projectowner.yaml new file mode 100644 index 0000000000..b1d82b6dd2 --- /dev/null +++ b/scripts/rbac/projectowner.yaml @@ -0,0 +1,7 @@ +# rbac for project owner, not allow for delete +condition: roles.contains("project_owner") +is_admin: false +policy: + *: + *: + *: allow diff --git a/scripts/rbac/sysadmin.yaml b/scripts/rbac/sysadmin.yaml new file mode 100644 index 0000000000..61ddf503ec --- /dev/null +++ b/scripts/rbac/sysadmin.yaml @@ -0,0 +1,5 @@ +# rbac for sysadmin +condition: tenant=="system" && roles.contains("admin") +is_admin: true +policy: + *: allow