From d0fef591f1040cdebcb88cdd2f7b5913b5beab68 Mon Sep 17 00:00:00 2001 From: Qiu Jian Date: Sat, 27 Oct 2018 14:34:09 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E6=94=B9=E8=BF=9B=EF=BC=9Amodel=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E6=94=AF=E6=8C=81rbac=E8=AE=A4=E8=AF=81=EF=BC=8C?= =?UTF-8?q?=E9=9C=80=E8=A6=81=E6=89=93=E5=BC=80enable=5Frbac=E9=80=89?= =?UTF-8?q?=E9=A1=B9=EF=BC=8C=E9=BB=98=E8=AE=A4=E5=85=B3=E9=97=AD=E3=80=82?= =?UTF-8?q?=E9=9D=9Emodel=E8=B5=84=E6=BA=90=E8=BF=98=E4=B8=8D=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/climc/shell/policies.go | 55 +++- cmd/climc/shell/shell.go | 6 +- pkg/cloudcommon/auth.go | 9 + pkg/cloudcommon/db/db_dispatcher.go | 210 +++++++++++---- pkg/cloudcommon/db/global.go | 31 ++- pkg/cloudcommon/db/interface.go | 1 + pkg/cloudcommon/db/modelbase.go | 4 + pkg/cloudcommon/db/policy.go | 146 ++++++++++ pkg/cloudcommon/db/tenantcache.go | 2 +- pkg/cloudcommon/options.go | 2 + pkg/compute/models/quotas.go | 2 +- pkg/compute/service/service.go | 6 +- pkg/mcclient/token.go | 2 + pkg/mcclient/token2.go | 4 + pkg/mcclient/token3.go | 4 + pkg/mcclient/tokensimple.go | 6 +- pkg/util/conditionparser/parser.go | 32 ++- pkg/util/conditionparser/parser_test.go | 3 + pkg/util/rbacutils/rabc.go | 344 ++++++++++++++++++++++++ pkg/util/rbacutils/rabc_test.go | 282 +++++++++++++++++++ scripts/rbac/member.yaml | 8 + scripts/rbac/projectowner.yaml | 7 + scripts/rbac/sysadmin.yaml | 5 + 23 files changed, 1097 insertions(+), 74 deletions(-) create mode 100644 pkg/cloudcommon/db/policy.go create mode 100644 pkg/util/rbacutils/rabc.go create mode 100644 pkg/util/rbacutils/rabc_test.go create mode 100644 scripts/rbac/member.yaml create mode 100644 scripts/rbac/projectowner.yaml create mode 100644 scripts/rbac/sysadmin.yaml diff --git a/cmd/climc/shell/policies.go b/cmd/climc/shell/policies.go index af4b5d86f7..365c32e0a6 100644 --- a/cmd/climc/shell/policies.go +++ b/cmd/climc/shell/policies.go @@ -9,9 +9,26 @@ import ( 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,6 +37,7 @@ 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 { @@ -27,9 +45,13 @@ func init() { if err != nil { return err } + jsonBlob, err := jsonutils.ParseYAML(string(blob)) + 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(jsonBlob.String()), "blob") result, err := modules.Policies.Create(s, params) if err != nil { @@ -43,17 +65,28 @@ 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) - if err != nil { - return err - } params := jsonutils.NewDict() - params.Add(jsonutils.NewString("application/json"), "type") - params.Add(jsonutils.NewString(string(blob)), "blob") - + if len(args.Type) > 0 { + params.Add(jsonutils.NewString(args.Type), "type") + } + if len(args.File) > 0 { + blob, err := ioutil.ReadFile(args.File) + if err != nil { + return err + } + jsonBlob, err := jsonutils.ParseYAML(string(blob)) + if err != nil { + return err + } + params.Add(jsonutils.NewString(jsonBlob.String()), "blob") + } + if params.Size() == 0 { + return InvalidUpdateError() + } result, err := modules.Policies.Patch(s, args.ID, params) 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..bd937da888 100644 --- a/pkg/cloudcommon/auth.go +++ b/pkg/cloudcommon/auth.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/mcclient/auth" ) @@ -39,4 +40,12 @@ 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() + } } diff --git a/pkg/cloudcommon/db/db_dispatcher.go b/pkg/cloudcommon/db/db_dispatcher.go index e19295e1ba..7cbcc168d5 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 @@ -457,9 +457,19 @@ func listItems(manager IModelManager, ctx context.Context, userCred mcclient.Tok 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) @@ -541,6 +551,37 @@ func (dispatcher *DBModelDispatcher) tryGetModelProperty(ctx context.Context, pr } } +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 (dispatcher *DBModelDispatcher) Get(ctx context.Context, idStr string, query jsonutils.JSONObject) (jsonutils.JSONObject, error) { // log.Debugf("Get %s", idStr) userCred := fetchUserCredential(ctx) @@ -560,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) @@ -576,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) } @@ -633,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) @@ -748,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") } @@ -806,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") } @@ -858,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) { @@ -884,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 { @@ -892,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 } } @@ -931,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) } @@ -1038,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")) } @@ -1067,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/global.go b/pkg/cloudcommon/db/global.go index d9b2dd6a9c..3d44199872 100644 --- a/pkg/cloudcommon/db/global.go +++ b/pkg/cloudcommon/db/global.go @@ -2,8 +2,37 @@ package db /// 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() { + globalsRbacEnabled = true + PolicyManager.start() +} diff --git a/pkg/cloudcommon/db/interface.go b/pkg/cloudcommon/db/interface.go index 9866b6dcdc..390d7e11d2 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 0cfdec9dbc..07ba9554dc 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..93544fc89c --- /dev/null +++ b/pkg/cloudcommon/db/policy.go @@ -0,0 +1,146 @@ +package db + +import ( + "yunion.io/x/jsonutils" + "yunion.io/x/log" + + "time" + "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 ( + PolicyFailedRetryInterval = 15 * time.Second + PolicyRefreshInterval = 15 * time.Minute + + PolicyDelegation = "delegate" + + PolicyActionList = "list" + PolicyActionGet = "get" + PolicyActionUpdate = "update" + PolicyActionPatch = "patch" + PolicyActionCreate = "create" + PolicyActionDelete = "delete" + PolicyActionPerform = "perform" +) + +var PolicyManager *SPolicyManager + +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.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() { + log.Infof("PolicyManager start to fetch policies ...") + manager.sync() +} + +func (manager *SPolicyManager) sync() { + 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 +} 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..80a6174b66 100644 --- a/pkg/cloudcommon/options.go +++ b/pkg/cloudcommon/options.go @@ -40,6 +40,8 @@ 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"` + 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/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..b4831f5ad2 100644 --- a/pkg/util/conditionparser/parser.go +++ b/pkg/util/conditionparser/parser.go @@ -252,6 +252,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 +268,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 +388,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..8ff872f0d2 --- /dev/null +++ b/pkg/util/rbacutils/rabc.go @@ -0,0 +1,344 @@ +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 rule.Extra != nil && 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 + } + if len(extra) > 0 { + if rule.Extra != nil { + 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 From 0c0621636420793b464a63489d4dab3c959a7444 Mon Sep 17 00:00:00 2001 From: Qiu Jian Date: Sat, 27 Oct 2018 16:40:41 +0800 Subject: [PATCH 2/7] JointModel support --- cmd/climc/shell/policies.go | 164 +++++++++++++++++++--- pkg/cloudcommon/db/db_dispatcher.go | 31 ---- pkg/cloudcommon/db/db_joint_dispatcher.go | 31 +++- pkg/cloudcommon/db/global.go | 4 + pkg/cloudcommon/db/quotas/handler.go | 14 +- pkg/cloudcommon/db/rbac.go | 55 ++++++++ 6 files changed, 240 insertions(+), 59 deletions(-) create mode 100644 pkg/cloudcommon/db/rbac.go diff --git a/cmd/climc/shell/policies.go b/cmd/climc/shell/policies.go index 365c32e0a6..d2b28e9606 100644 --- a/cmd/climc/shell/policies.go +++ b/cmd/climc/shell/policies.go @@ -1,12 +1,88 @@ package shell import ( + "fmt" "io/ioutil" + "os" + "os/exec" "yunion.io/x/jsonutils" "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/mcclient/modules" + "yunion.io/x/onecloud/pkg/util/httputils" ) +func getPolicy(s *mcclient.ClientSession, id string) (jsonutils.JSONObject, error) { + result, err := modules.Policies.GetById(s, id, nil) + if err == nil { + return result, nil + } + jsonErr := err.(*httputils.JSONClientError) + if jsonErr.Code != 404 { + return nil, err + } + params := jsonutils.NewDict() + params.Add(jsonutils.NewString(id), "type") + listResult, err := modules.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", id)} + } else { + return nil, &httputils.JSONClientError{Code: 409, Class: "Conflict", + Details: fmt.Sprintf("multiple %d found", id)} + } +} + +func getPolicyId(s *mcclient.ClientSession, name string) (string, error) { + policyJson, err := getPolicy(s, name) + if err != nil { + return "", err + } + return policyJson.GetString("id") +} + +func getPolicyYaml(s *mcclient.ClientSession, id string) (string, error) { + result, err := modules.Policies.GetById(s, id, nil) + if err != nil { + return "", err + } + blobStr, err := result.GetString("blob") + if err != nil { + return "", err + } + blobJson, err := jsonutils.ParseString(blobStr) + if err != nil { + return "", err + } + return blobJson.YAMLString(), nil +} + +func patchPolicyYaml(s *mcclient.ClientSession, policyId string, typeStr string, fileName string) (jsonutils.JSONObject, error) { + params := jsonutils.NewDict() + if len(typeStr) > 0 { + params.Add(jsonutils.NewString(typeStr), "type") + } + if len(fileName) > 0 { + blob, err := ioutil.ReadFile(fileName) + if err != nil { + return nil, err + } + jsonBlob, err := jsonutils.ParseYAML(string(blob)) + if err != nil { + return nil, err + } + params.Add(jsonutils.NewString(jsonBlob.String()), "blob") + } + if params.Size() == 0 { + return nil, InvalidUpdateError() + } + return modules.Policies.Patch(s, policyId, params) +} + func init() { type PolicyListOptions struct { Limit int64 `help:"Limit, default 0, i.e. no limit"` @@ -69,25 +145,11 @@ func init() { Type string `help:"policy type"` } R(&PolicyPatchOptions{}, "policy-patch", "Patch policy", func(s *mcclient.ClientSession, args *PolicyPatchOptions) error { - params := jsonutils.NewDict() - if len(args.Type) > 0 { - params.Add(jsonutils.NewString(args.Type), "type") + policyId, err := getPolicyId(s, args.ID) + if err != nil { + return err } - if len(args.File) > 0 { - blob, err := ioutil.ReadFile(args.File) - if err != nil { - return err - } - jsonBlob, err := jsonutils.ParseYAML(string(blob)) - if err != nil { - return err - } - params.Add(jsonutils.NewString(jsonBlob.String()), "blob") - } - if params.Size() == 0 { - return InvalidUpdateError() - } - result, err := modules.Policies.Patch(s, args.ID, params) + result, err := patchPolicyYaml(s, policyId, args.Type, args.File) if err != nil { return err } @@ -101,11 +163,75 @@ 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 := getPolicyId(s, args.ID) + 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 { + policyId, err := getPolicyId(s, args.ID) + if err != nil { + return err + } + yaml, err := getPolicyYaml(s, policyId) + 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 { + policyId, err := getPolicyId(s, args.ID) + if err != nil { + return err + } + yaml, err := getPolicyYaml(s, policyId) + 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 + } + + result, err := patchPolicyYaml(s, policyId, "", tmpfile.Name()) + if err != nil { + return err + } + + printObject(result) + return nil + }) + } diff --git a/pkg/cloudcommon/db/db_dispatcher.go b/pkg/cloudcommon/db/db_dispatcher.go index 7cbcc168d5..27857d48e0 100644 --- a/pkg/cloudcommon/db/db_dispatcher.go +++ b/pkg/cloudcommon/db/db_dispatcher.go @@ -551,37 +551,6 @@ func (dispatcher *DBModelDispatcher) tryGetModelProperty(ctx context.Context, pr } } -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 (dispatcher *DBModelDispatcher) Get(ctx context.Context, idStr string, query jsonutils.JSONObject) (jsonutils.JSONObject, error) { // log.Debugf("Get %s", idStr) userCred := fetchUserCredential(ctx) 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 3d44199872..ed9aa8d207 100644 --- a/pkg/cloudcommon/db/global.go +++ b/pkg/cloudcommon/db/global.go @@ -36,3 +36,7 @@ func EnableGlobalRbac() { globalsRbacEnabled = true PolicyManager.start() } + +func IsGlobalRbacEnabled() bool { + return globalsRbacEnabled +} diff --git a/pkg/cloudcommon/db/quotas/handler.go b/pkg/cloudcommon/db/quotas/handler.go index 6399dbe295..fa787ec5fb 100644 --- a/pkg/cloudcommon/db/quotas/handler.go +++ b/pkg/cloudcommon/db/quotas/handler.go @@ -77,7 +77,7 @@ func getQuotaHanlder(ctx context.Context, w http.ResponseWriter, r *http.Request projectId = userCred.GetProjectId() } else { if !userCred.IsSystemAdmin() { - httperrors.ForbiddenError(w, "not allow to query quota") + httperrors.ForbiddenError(w, "not allow to delegate query quota") return } tenant, err := db.TenantCacheManager.FetchTenantByIdOrName(ctx, projectId) @@ -107,7 +107,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 } @@ -163,7 +171,7 @@ 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") + httperrors.ForbiddenError(w, "not allow to check quota") return } params := appctx.AppContextParams(ctx) 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 +} From d069a2a46996cf6f6f4e68d3fbea2c216242ce07 Mon Sep 17 00:00:00 2001 From: Qiu Jian Date: Sat, 27 Oct 2018 17:29:07 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20usages=20get=20?= =?UTF-8?q?=E5=92=8C=20quotas=20get/update=20=E7=9A=84RBAC=E6=A3=80?= =?UTF-8?q?=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/cloudcommon/auth.go | 4 +++- pkg/cloudcommon/db/global.go | 6 ++++-- pkg/cloudcommon/db/policy.go | 14 +++++++++----- pkg/cloudcommon/db/quotas/handler.go | 24 +++++++++++++++++++++++- pkg/cloudcommon/options.go | 4 +++- pkg/compute/usages/handler.go | 23 ++++++++++++++++++++++- 6 files changed, 64 insertions(+), 11 deletions(-) diff --git a/pkg/cloudcommon/auth.go b/pkg/cloudcommon/auth.go index bd937da888..0e13195096 100644 --- a/pkg/cloudcommon/auth.go +++ b/pkg/cloudcommon/auth.go @@ -6,6 +6,7 @@ import ( "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/mcclient/auth" + "time" ) func InitAuth(options *Options, authComplete auth.AuthCompletedCallback) { @@ -46,6 +47,7 @@ func InitAuth(options *Options, authComplete auth.AuthCompletedCallback) { } if options.EnableRbac { - db.EnableGlobalRbac() + db.EnableGlobalRbac(time.Duration(options.RbacPolicySyncPeriodSeconds)*time.Second, + time.Duration(options.RbacPolicySyncFailedRetrySeconds)*time.Second) } } diff --git a/pkg/cloudcommon/db/global.go b/pkg/cloudcommon/db/global.go index ed9aa8d207..86e3c59efc 100644 --- a/pkg/cloudcommon/db/global.go +++ b/pkg/cloudcommon/db/global.go @@ -1,5 +1,7 @@ package db +import "time" + /// Global virtual resource namespace var ( @@ -32,9 +34,9 @@ func GetGlobalServiceType() string { return globalServiceType } -func EnableGlobalRbac() { +func EnableGlobalRbac(refreshInterval time.Duration, retryInterval time.Duration) { globalsRbacEnabled = true - PolicyManager.start() + PolicyManager.start(refreshInterval, retryInterval) } func IsGlobalRbacEnabled() bool { diff --git a/pkg/cloudcommon/db/policy.go b/pkg/cloudcommon/db/policy.go index 93544fc89c..c754b69be8 100644 --- a/pkg/cloudcommon/db/policy.go +++ b/pkg/cloudcommon/db/policy.go @@ -12,9 +12,6 @@ import ( ) const ( - PolicyFailedRetryInterval = 15 * time.Second - PolicyRefreshInterval = 15 * time.Minute - PolicyDelegation = "delegate" PolicyActionList = "list" @@ -26,7 +23,12 @@ const ( PolicyActionPerform = "perform" ) -var PolicyManager *SPolicyManager +var ( + PolicyManager *SPolicyManager + + PolicyFailedRetryInterval = 15 * time.Second + PolicyRefreshInterval = 15 * time.Minute +) func init() { PolicyManager = &SPolicyManager{} @@ -106,8 +108,10 @@ func fetchPolicies() (map[string]rbacutils.SRbacPolicy, map[string]rbacutils.SRb return policies, adminPolicies, nil } -func (manager *SPolicyManager) start() { +func (manager *SPolicyManager) start(refreshInterval time.Duration, retryInterval time.Duration) { log.Infof("PolicyManager start to fetch policies ...") + PolicyRefreshInterval = refreshInterval + PolicyFailedRetryInterval = retryInterval manager.sync() } diff --git a/pkg/cloudcommon/db/quotas/handler.go b/pkg/cloudcommon/db/quotas/handler.go index fa787ec5fb..d81141624f 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() { + 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 { diff --git a/pkg/cloudcommon/options.go b/pkg/cloudcommon/options.go index 80a6174b66..a28253eb80 100644 --- a/pkg/cloudcommon/options.go +++ b/pkg/cloudcommon/options.go @@ -40,7 +40,9 @@ 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"` + 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/usages/handler.go b/pkg/compute/usages/handler.go index 5bc4f607b4..27eae7c03d 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 From 55645b9b1d0c7372fd70342529826e0559f681f0 Mon Sep 17 00:00:00 2001 From: Qiu Jian Date: Sat, 27 Oct 2018 17:37:23 +0800 Subject: [PATCH 4/7] fix quota check rbac check logic --- pkg/cloudcommon/db/policy.go | 1 + pkg/cloudcommon/db/quotas/handler.go | 20 ++++++++++++++++++-- pkg/mcclient/modules/base.go | 3 +-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/pkg/cloudcommon/db/policy.go b/pkg/cloudcommon/db/policy.go index c754b69be8..4ad3b5de0c 100644 --- a/pkg/cloudcommon/db/policy.go +++ b/pkg/cloudcommon/db/policy.go @@ -116,6 +116,7 @@ func (manager *SPolicyManager) start(refreshInterval time.Duration, retryInterva } func (manager *SPolicyManager) sync() { + log.Debugf("start synchronize RBAC policies ...") policies, adminPolicies, err := fetchPolicies() if err != nil { log.Errorf("sync policy fail %s", err) diff --git a/pkg/cloudcommon/db/quotas/handler.go b/pkg/cloudcommon/db/quotas/handler.go index d81141624f..24da096fb9 100644 --- a/pkg/cloudcommon/db/quotas/handler.go +++ b/pkg/cloudcommon/db/quotas/handler.go @@ -192,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 check 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/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 } From 11dddd8ad327b2afdbc68f9503a334b1cfc7dc18 Mon Sep 17 00:00:00 2001 From: Qiu Jian Date: Sat, 27 Oct 2018 19:12:38 +0800 Subject: [PATCH 5/7] remove slice nil check --- pkg/util/rbacutils/rabc.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/pkg/util/rbacutils/rabc.go b/pkg/util/rbacutils/rabc.go index 8ff872f0d2..a586a42dcf 100644 --- a/pkg/util/rbacutils/rabc.go +++ b/pkg/util/rbacutils/rabc.go @@ -44,7 +44,7 @@ func (rule *SRbacRule) contains(rule2 *SRbacRule) bool { if !isWildMatch(rule.Action) && rule.Action != rule2.Action { return false } - if rule.Extra != nil && len(rule.Extra) > 0 { + 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 @@ -81,17 +81,13 @@ func (rule *SRbacRule) match(service string, resource string, action string, ext matched += 1 weight += 100 } - if len(extra) > 0 { - if rule.Extra != nil { - 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) - } + 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 From 740d6800ef75c4c7e3aa90112e8a4da7082a5f1d Mon Sep 17 00:00:00 2001 From: Qiu Jian Date: Mon, 29 Oct 2018 14:54:01 +0800 Subject: [PATCH 6/7] make fmt --- pkg/cloudcommon/auth.go | 2 +- pkg/cloudcommon/db/quotas/handler.go | 10 +++++----- pkg/compute/usages/handler.go | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/cloudcommon/auth.go b/pkg/cloudcommon/auth.go index 0e13195096..22220a7e1c 100644 --- a/pkg/cloudcommon/auth.go +++ b/pkg/cloudcommon/auth.go @@ -4,9 +4,9 @@ import ( "fmt" "os" + "time" "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/mcclient/auth" - "time" ) func InitAuth(options *Options, authComplete auth.AuthCompletedCallback) { diff --git a/pkg/cloudcommon/db/quotas/handler.go b/pkg/cloudcommon/db/quotas/handler.go index 24da096fb9..80bfeedd21 100644 --- a/pkg/cloudcommon/db/quotas/handler.go +++ b/pkg/cloudcommon/db/quotas/handler.go @@ -76,7 +76,7 @@ func getQuotaHanlder(ctx context.Context, w http.ResponseWriter, r *http.Request if len(projectId) == 0 { projectId = userCred.GetProjectId() if db.IsGlobalRbacEnabled() { - if ! db.PolicyManager.Allow(false, userCred, db.GetGlobalServiceType(), + if !db.PolicyManager.Allow(false, userCred, db.GetGlobalServiceType(), "quotas", db.PolicyActionGet) { httperrors.ForbiddenError(w, "not allow to get quota") return @@ -90,12 +90,12 @@ func getQuotaHanlder(ctx context.Context, w http.ResponseWriter, r *http.Request } else { isAllow = userCred.IsSystemAdmin() } - if ! isAllow { + if !isAllow { httperrors.ForbiddenError(w, "not allow to delegate query quota") return } if db.IsGlobalRbacEnabled() { - if ! db.PolicyManager.Allow(true, userCred, db.GetGlobalServiceType(), + if !db.PolicyManager.Allow(true, userCred, db.GetGlobalServiceType(), "quotas", db.PolicyActionGet) { httperrors.ForbiddenError(w, "not allow to query quota") return @@ -200,12 +200,12 @@ func checkQuotaHanlder(ctx context.Context, w http.ResponseWriter, r *http.Reque } else { isAllow = userCred.IsSystemAdmin() } - if ! isAllow { + if !isAllow { httperrors.ForbiddenError(w, "not allow to delegate check quota") return } if db.IsGlobalRbacEnabled() { - if ! db.PolicyManager.Allow(true, userCred, db.GetGlobalServiceType(), + if !db.PolicyManager.Allow(true, userCred, db.GetGlobalServiceType(), "quotas", db.PolicyActionGet) { httperrors.ForbiddenError(w, "not allow to query quota") return diff --git a/pkg/compute/usages/handler.go b/pkg/compute/usages/handler.go index 27eae7c03d..71899df1ea 100644 --- a/pkg/compute/usages/handler.go +++ b/pkg/compute/usages/handler.go @@ -263,7 +263,7 @@ func ReportGeneralUsage(userCred mcclient.TokenCredential, rangeObj db.IStandalo } if db.IsGlobalRbacEnabled() { - if ! db.PolicyManager.Allow(false, userCred, db.GetGlobalServiceType(), + if !db.PolicyManager.Allow(false, userCred, db.GetGlobalServiceType(), "usages", db.PolicyActionGet) { err = httperrors.NewForbiddenError("not allow to get usages") return From 5a8bd4848509782e3ce953e9faae864ee09a0d8b Mon Sep 17 00:00:00 2001 From: Qiu Jian Date: Mon, 29 Oct 2018 22:10:00 +0800 Subject: [PATCH 7/7] =?UTF-8?q?=E6=94=B9=E8=BF=9B=EF=BC=9Arpc=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/climc/shell/policies.go | 159 ++++++++++++--------------- pkg/cloudcommon/db/policy.go | 58 +++++++++- pkg/mcclient/auth/auth.go | 12 ++ pkg/mcclient/modules/mod_policies.go | 140 ++++++++++++++++++++++- pkg/mcclient/modules/resource.go | 2 +- pkg/mcclient/session.go | 4 + pkg/util/conditionparser/parser.go | 3 + 7 files changed, 285 insertions(+), 93 deletions(-) diff --git a/cmd/climc/shell/policies.go b/cmd/climc/shell/policies.go index d2b28e9606..4f595012b3 100644 --- a/cmd/climc/shell/policies.go +++ b/cmd/climc/shell/policies.go @@ -5,84 +5,15 @@ import ( "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" - "yunion.io/x/onecloud/pkg/util/httputils" ) -func getPolicy(s *mcclient.ClientSession, id string) (jsonutils.JSONObject, error) { - result, err := modules.Policies.GetById(s, id, nil) - if err == nil { - return result, nil - } - jsonErr := err.(*httputils.JSONClientError) - if jsonErr.Code != 404 { - return nil, err - } - params := jsonutils.NewDict() - params.Add(jsonutils.NewString(id), "type") - listResult, err := modules.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", id)} - } else { - return nil, &httputils.JSONClientError{Code: 409, Class: "Conflict", - Details: fmt.Sprintf("multiple %d found", id)} - } -} - -func getPolicyId(s *mcclient.ClientSession, name string) (string, error) { - policyJson, err := getPolicy(s, name) - if err != nil { - return "", err - } - return policyJson.GetString("id") -} - -func getPolicyYaml(s *mcclient.ClientSession, id string) (string, error) { - result, err := modules.Policies.GetById(s, id, nil) - if err != nil { - return "", err - } - blobStr, err := result.GetString("blob") - if err != nil { - return "", err - } - blobJson, err := jsonutils.ParseString(blobStr) - if err != nil { - return "", err - } - return blobJson.YAMLString(), nil -} - -func patchPolicyYaml(s *mcclient.ClientSession, policyId string, typeStr string, fileName string) (jsonutils.JSONObject, error) { - params := jsonutils.NewDict() - if len(typeStr) > 0 { - params.Add(jsonutils.NewString(typeStr), "type") - } - if len(fileName) > 0 { - blob, err := ioutil.ReadFile(fileName) - if err != nil { - return nil, err - } - jsonBlob, err := jsonutils.ParseYAML(string(blob)) - if err != nil { - return nil, err - } - params.Add(jsonutils.NewString(jsonBlob.String()), "blob") - } - if params.Size() == 0 { - return nil, InvalidUpdateError() - } - return modules.Policies.Patch(s, policyId, params) -} - func init() { type PolicyListOptions struct { Limit int64 `help:"Limit, default 0, i.e. no limit"` @@ -117,17 +48,14 @@ func init() { 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) - if err != nil { - return err - } - jsonBlob, err := jsonutils.ParseYAML(string(blob)) + policyBytes, err := ioutil.ReadFile(args.FILE) if err != nil { return err } + params := jsonutils.NewDict() params.Add(jsonutils.NewString(args.TYPE), "type") - params.Add(jsonutils.NewString(jsonBlob.String()), "blob") + params.Add(jsonutils.NewString(string(policyBytes)), "policy") result, err := modules.Policies.Create(s, params) if err != nil { @@ -145,11 +73,22 @@ func init() { Type string `help:"policy type"` } R(&PolicyPatchOptions{}, "policy-patch", "Patch policy", func(s *mcclient.ClientSession, args *PolicyPatchOptions) error { - policyId, err := getPolicyId(s, args.ID) + policyId, err := modules.Policies.GetId(s, args.ID, nil) if err != nil { return err } - result, err := patchPolicyYaml(s, policyId, args.Type, args.File) + params := jsonutils.NewDict() + 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 } @@ -163,7 +102,7 @@ func init() { ID string `help:"ID of policy"` } R(&PolicyDeleteOptions{}, "policy-delete", "Delete policy", func(s *mcclient.ClientSession, args *PolicyDeleteOptions) error { - policyId, err := getPolicyId(s, args.ID) + policyId, err := modules.Policies.GetId(s, args.ID, nil) if err != nil { return err } @@ -179,11 +118,11 @@ func init() { ID string `help:"ID of policy"` } R(&PolicyShowOptions{}, "policy-show", "Show policy", func(s *mcclient.ClientSession, args *PolicyShowOptions) error { - policyId, err := getPolicyId(s, args.ID) + result, err := modules.Policies.Get(s, args.ID, nil) if err != nil { return err } - yaml, err := getPolicyYaml(s, policyId) + yaml, err := result.GetString("policy") if err != nil { return err } @@ -195,11 +134,15 @@ func init() { ID string `help:"ID of policy"` } R(&PolicyEditOptions{}, "policy-edit", "Edit and update policy", func(s *mcclient.ClientSession, args *PolicyEditOptions) error { - policyId, err := getPolicyId(s, args.ID) + result, err := modules.Policies.Get(s, args.ID, nil) if err != nil { return err } - yaml, err := getPolicyYaml(s, policyId) + policyId, err := result.GetString("id") + if err != nil { + return err + } + yaml, err := result.GetString("policy") if err != nil { return err } @@ -225,7 +168,14 @@ func init() { return err } - result, err := patchPolicyYaml(s, policyId, "", tmpfile.Name()) + 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 } @@ -234,4 +184,39 @@ func init() { 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 + } + printObject(result) + return nil + }) } diff --git a/pkg/cloudcommon/db/policy.go b/pkg/cloudcommon/db/policy.go index 4ad3b5de0c..84bd91c678 100644 --- a/pkg/cloudcommon/db/policy.go +++ b/pkg/cloudcommon/db/policy.go @@ -5,6 +5,7 @@ import ( "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" @@ -78,7 +79,7 @@ func fetchPolicies() (map[string]rbacutils.SRbacPolicy, map[string]rbacutils.SRb params := jsonutils.NewDict() params.Add(jsonutils.NewInt(2048), "limit") params.Add(jsonutils.NewInt(int64(offset)), "offset") - result, err := modules.Policies.List(s, params) + result, err := modules.Policies.ResourceManager.List(s, params) if err != nil { log.Errorf("fetch policy failed") @@ -149,3 +150,58 @@ func (manager *SPolicyManager) Allow(isAdmin bool, userCred mcclient.TokenCreden } 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/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/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/util/conditionparser/parser.go b/pkg/util/conditionparser/parser.go index b4831f5ad2..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)