From 89ae50dbae6a6e16f5d7a98294eb3769f2040249 Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Tue, 13 Jul 2021 08:51:32 +0200 Subject: [PATCH] Add RBAC functions for workflow for check&grant --- pkg/rbac/session.go | 9 +-- pkg/wfexec/session.go | 9 ++- system/automation/rbac_handler.go | 54 ++++++++++++--- system/service/service.go | 2 + tests/workflows/0002_rbac_fn_test.go | 34 ++++++++++ tests/workflows/main_test.go | 1 + .../testdata/S0002_rbac_fn/workflow.yaml | 66 +++++++++++++++++++ 7 files changed, 162 insertions(+), 13 deletions(-) create mode 100644 tests/workflows/0002_rbac_fn_test.go create mode 100644 tests/workflows/testdata/S0002_rbac_fn/workflow.yaml diff --git a/pkg/rbac/session.go b/pkg/rbac/session.go index f73052624..fd0a50202 100644 --- a/pkg/rbac/session.go +++ b/pkg/rbac/session.go @@ -38,12 +38,13 @@ func (s session) Context() context.Context { return s.ctx } var _ Session = &session{} func ContextToSession(ctx context.Context) *session { - i := auth.GetIdentityFromContext(ctx) - s := &session{ + return NewSession(ctx, auth.GetIdentityFromContext(ctx)) +} + +func NewSession(ctx context.Context, i auth.Identifiable) *session { + return &session{ id: i.Identity(), rr: i.Roles(), ctx: ctx, } - - return s } diff --git a/pkg/wfexec/session.go b/pkg/wfexec/session.go index 881b8a041..85fe7f63e 100644 --- a/pkg/wfexec/session.go +++ b/pkg/wfexec/session.go @@ -230,6 +230,9 @@ func (s *Session) Result() *expr.Vars { } func (s *Session) Exec(ctx context.Context, step Step, scope *expr.Vars) error { + s.mux.RLock() + defer s.mux.RUnlock() + err := func() error { if s.g.Len() == 0 { return fmt.Errorf("refusing to execute without steps") @@ -466,7 +469,9 @@ func (s *Session) worker(ctx context.Context) { st.err, ) - // when the error handler is defined, the error was handled and should not kill the workflow + s.mux.Lock() + + // when the err handler is defined, the error was handled and should not kill the workflow if !st.errHandled { // We need to force failed session status // because it's not set early enough to pick it up with s.Status() @@ -476,6 +481,8 @@ func (s *Session) worker(ctx context.Context) { // to break worker loop s.qErr <- st.err } + + s.mux.Unlock() } s.log.Debug( diff --git a/system/automation/rbac_handler.go b/system/automation/rbac_handler.go index 0612c3205..2320df957 100644 --- a/system/automation/rbac_handler.go +++ b/system/automation/rbac_handler.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/cortezaproject/corteza-server/pkg/rbac" + "github.com/cortezaproject/corteza-server/system/types" ) type ( @@ -13,9 +14,19 @@ type ( Grant(ctx context.Context, rules ...*rbac.Rule) (err error) } + rbacUserService interface { + FindByAny(ctx context.Context, identifier interface{}) (*types.User, error) + } + + rbacRoleService interface { + Membership(ctx context.Context, userID uint64) (types.RoleMemberSet, error) + } + rbacHandler struct { - reg rbacHandlerRegistry - svc rbacService + reg rbacHandlerRegistry + rbac rbacService + user rbacUserService + role rbacRoleService } auxResource struct{ res string } @@ -27,10 +38,12 @@ func (r auxResource) RbacResource() string { return r.res } -func RbacHandler(reg rbacHandlerRegistry, svc rbacService) *rbacHandler { +func RbacHandler(reg rbacHandlerRegistry, rbac rbacService, user rbacUserService, role rbacRoleService) *rbacHandler { h := &rbacHandler{ - reg: reg, - svc: svc, + reg: reg, + rbac: rbac, + user: user, + role: role, } h.register() @@ -56,16 +69,41 @@ func (h rbacHandler) grant(ctx context.Context, r *rbac.Rule) (err error) { // no extra metadata hidden under it, cmpRes := &auxResource{rbac.ResourceComponent(r.Resource)} - if !h.svc.Can(rbac.ContextToSession(ctx), "grant", cmpRes) { + if !h.rbac.Can(rbac.ContextToSession(ctx), "grant", cmpRes) { return fmt.Errorf("not allowed to grant %s", r.String()) } - return h.svc.Grant(ctx, r) + return h.rbac.Grant(ctx, r) } func (h rbacHandler) check(ctx context.Context, args *rbacCheckArgs) (b *rbacCheckResults, err error) { + var secses rbac.Session + b = &rbacCheckResults{} - b.Can = h.svc.Can(rbac.ContextToSession(ctx), args.Operation, args.Resource) + if args.hasUser { + // user was explicitly set to rbac checking function + // + // load all roles that this user is member of + // and create new security session out of it + var ( + mm types.RoleMemberSet + ids []uint64 + ) + + mm, err = h.role.Membership(ctx, args.User.ID) + _ = mm.Walk(func(m *types.RoleMember) error { + ids = append(ids, m.RoleID) + return nil + }) + + args.User.SetRoles(ids...) + + secses = rbac.NewSession(ctx, args.User) + } else { + secses = rbac.ContextToSession(ctx) + } + + b.Can = h.rbac.Can(secses, args.Operation, args.Resource) return } diff --git a/system/service/service.go b/system/service/service.go index e92fce0e8..d1ddc9810 100644 --- a/system/service/service.go +++ b/system/service/service.go @@ -202,6 +202,8 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, ws websock automation.RbacHandler( automationService.Registry(), rbac.Global(), + DefaultUser, + DefaultRole, ) return diff --git a/tests/workflows/0002_rbac_fn_test.go b/tests/workflows/0002_rbac_fn_test.go new file mode 100644 index 000000000..26c2ff10f --- /dev/null +++ b/tests/workflows/0002_rbac_fn_test.go @@ -0,0 +1,34 @@ +package workflows + +import ( + "context" + "testing" + + "github.com/cortezaproject/corteza-server/automation/types" + "github.com/cortezaproject/corteza-server/pkg/rbac" + "github.com/stretchr/testify/require" +) + +func Test0002_rbac_fn(t *testing.T) { + var ( + ctx = bypassRBAC(context.Background()) + req = require.New(t) + ) + + loadScenario(ctx, t) + + req.Len(rbac.Global().Rules(), 0) + + var ( + aux = struct { + CanCurrentRead string + CanOtherRead string + }{} + vars, _ = mustExecWorkflow(ctx, t, "check-and-grant", types.WorkflowExecParams{}) + ) + + req.NoError(vars.Decode(&aux)) + req.Equal("y", aux.CanCurrentRead) + req.Equal("n", aux.CanOtherRead) + req.Len(rbac.Global().Rules(), 1) +} diff --git a/tests/workflows/main_test.go b/tests/workflows/main_test.go index cea534a65..de6600e0e 100644 --- a/tests/workflows/main_test.go +++ b/tests/workflows/main_test.go @@ -39,6 +39,7 @@ func TestMain(m *testing.M) { ctx := context.Background() defApp = helpers.NewIntegrationTestApp(ctx, func(app *app.CortezaApp) (err error) { + //app.Opt.Workflow.ExecDebug = true defStore = app.Store eventbus.Set(eventBus) return nil diff --git a/tests/workflows/testdata/S0002_rbac_fn/workflow.yaml b/tests/workflows/testdata/S0002_rbac_fn/workflow.yaml new file mode 100644 index 000000000..24cd81cef --- /dev/null +++ b/tests/workflows/testdata/S0002_rbac_fn/workflow.yaml @@ -0,0 +1,66 @@ +roles: + testers: + +users: + tester: + email: tester@testing-samples.tld + +workflows: + check-and-grant: + enabled: true + trace: true + triggers: + - enabled: true + stepID: 1 + + steps: + - stepID: 1 + kind: function + ref: rolesLookup + arguments: + - { target: lookup, type: Handle, value: "testers" } + results: + - { target: res, expr: role } + + - stepID: 2 + kind: function + ref: rbacCheck + arguments: + - { target: resource, type: RbacResource, expr: "res" } + - { target: operation, type: String, value: "read" } + results: + - { target: canCurrentRead, type: String, expr: 'can ? "y":"n"' } + + + - stepID: 3 + kind: function + ref: usersLookup + arguments: + - { target: lookup, type: Handle, value: "tester" } + results: + - { target: usr, expr: user } + + - stepID: 4 + kind: function + ref: rbacCheck + arguments: + - { target: resource, type: RbacResource, expr: "res" } + - { target: operation, type: String, value: "read" } + - { target: user, type: User, expr: "usr" } + results: + - { target: canOtherRead, type: String, expr: 'can ? "y":"n"' } + + + - stepID: 5 + kind: function + ref: rbacAllow + arguments: + - { target: resource, type: RbacResource, expr: "res" } + - { target: operation, type: String, value: "read" } + - { target: role, type: Role, expr: "res" } + + paths: + - { parentID: 1, childID: 2 } + - { parentID: 2, childID: 3 } + - { parentID: 3, childID: 4 } + - { parentID: 4, childID: 5 }