diff --git a/automation/rest.yaml b/automation/rest.yaml index 795f48006..e5284b913 100644 --- a/automation/rest.yaml +++ b/automation/rest.yaml @@ -276,6 +276,7 @@ endpoints: - Client ID - Session ID imports: + - github.com/cortezaproject/corteza-server/pkg/filter - github.com/cortezaproject/corteza-server/pkg/rbac apis: - name: list @@ -318,6 +319,15 @@ endpoints: type: uint64 required: true title: Role ID + get: + - name: specific + required: false + title: Exclude (0, default), include (1) or return only (2) specific rules + type: "filter.State" + - name: resource + type: "[]string" + required: false + title: Show only rules for a specific resource - name: delete path: "/{roleID}/rules" method: DELETE diff --git a/automation/rest/permissions.go b/automation/rest/permissions.go index 3ae10fd10..ad60c34ca 100644 --- a/automation/rest/permissions.go +++ b/automation/rest/permissions.go @@ -2,11 +2,11 @@ package rest import ( "context" - "github.com/cortezaproject/corteza-server/automation/rest/request" "github.com/cortezaproject/corteza-server/automation/service" "github.com/cortezaproject/corteza-server/automation/types" "github.com/cortezaproject/corteza-server/pkg/api" + "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/cortezaproject/corteza-server/pkg/rbac" ) @@ -20,6 +20,7 @@ type ( Trace(context.Context, uint64, []uint64, ...string) ([]*rbac.Trace, error) List() []map[string]string FindRulesByRoleID(context.Context, uint64) (rbac.RuleSet, error) + FindRules(ctx context.Context, roleID uint64, specific filter.State, rr ...string) (rbac.RuleSet, error) Grant(ctx context.Context, rr ...*rbac.Rule) error } ) @@ -43,7 +44,7 @@ func (ctrl Permissions) List(ctx context.Context, r *request.PermissionsList) (i } func (ctrl Permissions) Read(ctx context.Context, r *request.PermissionsRead) (interface{}, error) { - return ctrl.ac.FindRulesByRoleID(ctx, r.RoleID) + return ctrl.ac.FindRules(ctx, r.RoleID, r.Specific, r.Resource...) } func (ctrl Permissions) Delete(ctx context.Context, r *request.PermissionsDelete) (interface{}, error) { diff --git a/automation/rest/request/permissions.go b/automation/rest/request/permissions.go index a2cc3cd54..9cf939907 100644 --- a/automation/rest/request/permissions.go +++ b/automation/rest/request/permissions.go @@ -11,6 +11,7 @@ package request import ( "encoding/json" "fmt" + "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/cortezaproject/corteza-server/pkg/payload" "github.com/cortezaproject/corteza-server/pkg/rbac" "github.com/go-chi/chi/v5" @@ -66,6 +67,16 @@ type ( // // Role ID RoleID uint64 `json:",string"` + + // Specific GET parameter + // + // Exclude (0, default), include (1) or return only (2) specific rules + Specific filter.State + + // Resource GET parameter + // + // Show only rules for a specific resource + Resource []string } PermissionsDelete struct { @@ -216,7 +227,9 @@ func NewPermissionsRead() *PermissionsRead { // Auditable returns all auditable/loggable parameters func (r PermissionsRead) Auditable() map[string]interface{} { return map[string]interface{}{ - "roleID": r.RoleID, + "roleID": r.RoleID, + "specific": r.Specific, + "resource": r.Resource, } } @@ -225,9 +238,42 @@ func (r PermissionsRead) GetRoleID() uint64 { return r.RoleID } +// Auditable returns all auditable/loggable parameters +func (r PermissionsRead) GetSpecific() filter.State { + return r.Specific +} + +// Auditable returns all auditable/loggable parameters +func (r PermissionsRead) GetResource() []string { + return r.Resource +} + // Fill processes request and fills internal variables func (r *PermissionsRead) Fill(req *http.Request) (err error) { + { + // GET params + tmp := req.URL.Query() + + if val, ok := tmp["specific"]; ok && len(val) > 0 { + r.Specific, err = payload.ParseFilterState(val[0]), nil + if err != nil { + return err + } + } + if val, ok := tmp["resource[]"]; ok { + r.Resource, err = val, nil + if err != nil { + return err + } + } else if val, ok := tmp["resource"]; ok { + r.Resource, err = val, nil + if err != nil { + return err + } + } + } + { var val string // path params diff --git a/automation/service/access_control.gen.go b/automation/service/access_control.gen.go index 64328a462..49c5bccab 100644 --- a/automation/service/access_control.gen.go +++ b/automation/service/access_control.gen.go @@ -12,6 +12,7 @@ import ( "github.com/cortezaproject/corteza-server/automation/types" "github.com/cortezaproject/corteza-server/pkg/actionlog" internalAuth "github.com/cortezaproject/corteza-server/pkg/auth" + "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/cortezaproject/corteza-server/pkg/rbac" systemTypes "github.com/cortezaproject/corteza-server/system/types" "github.com/spf13/cast" @@ -259,6 +260,63 @@ func (svc accessControl) logGrants(ctx context.Context, rr []*rbac.Rule) { } } +// FindRules find all rules based on filters +// +// This function is auto-generated +func (svc accessControl) FindRules(ctx context.Context, roleID uint64, specific filter.State, rr ...string) (out rbac.RuleSet, err error) { + if !svc.CanGrant(ctx) { + return nil, AccessControlErrNotAllowedToSetPermissions() + } + + rules, err := svc.FindRulesByRoleID(ctx, roleID) + if err != nil { + return + } + + var ( + resources []rbac.Resource + ruleMap = make(map[string]bool) + uniqRuleID = func(r *rbac.Rule) string { + return fmt.Sprintf("%s|%s|%d", r.Resource, r.Operation, r.RoleID) + } + ) + + // Filter based on resource + if len(rr) > 0 { + resources = make([]rbac.Resource, 0, len(rr)) + for _, r := range rr { + if err = rbacResourceValidator(r); err != nil { + return nil, fmt.Errorf("can not use resource %q: %w", r, err) + } + + resources = append(resources, rbac.NewResource(r)) + } + } else { + resources = svc.Resources() + } + + for _, res := range resources { + for _, rule := range rules.FilterResource(res.RbacResource()) { + if _, ok := ruleMap[uniqRuleID(rule)]; !ok { + out = append(out, rule) + ruleMap[uniqRuleID(rule)] = true + } + } + } + + // Filter for Excluded, Include, or Exclusive specific rules + switch specific { + // Exclude all the specific rules + case filter.StateExcluded: + out = out.FilterRules(false) + // Returns only all the specific rules + case filter.StateExclusive: + out = out.FilterRules(true) + } + + return +} + // FindRulesByRoleID find all rules for a specific role // // This function is auto-generated diff --git a/codegen/assets/templates/gocode/rbac/$component_access_control.go.tpl b/codegen/assets/templates/gocode/rbac/$component_access_control.go.tpl index cd8c92ee6..4ea69a25c 100644 --- a/codegen/assets/templates/gocode/rbac/$component_access_control.go.tpl +++ b/codegen/assets/templates/gocode/rbac/$component_access_control.go.tpl @@ -9,6 +9,7 @@ import ( "context" "github.com/cortezaproject/corteza-server/pkg/rbac" "github.com/cortezaproject/corteza-server/pkg/actionlog" + "github.com/cortezaproject/corteza-server/pkg/filter" systemTypes "github.com/cortezaproject/corteza-server/system/types" internalAuth "github.com/cortezaproject/corteza-server/pkg/auth" {{- range .imports }} @@ -204,6 +205,63 @@ func (svc accessControl) logGrants(ctx context.Context, rr []*rbac.Rule) { } } +// FindRules find all rules based on filters +// +// This function is auto-generated +func (svc accessControl) FindRules(ctx context.Context, roleID uint64, specific filter.State, rr ...string) (out rbac.RuleSet, err error) { + if !svc.CanGrant(ctx) { + return nil, AccessControlErrNotAllowedToSetPermissions() + } + + rules, err := svc.FindRulesByRoleID(ctx, roleID) + if err != nil { + return + } + + var ( + resources []rbac.Resource + ruleMap = make(map[string]bool) + uniqRuleID = func(r *rbac.Rule) string { + return fmt.Sprintf("%s|%s|%d", r.Resource, r.Operation, r.RoleID) + } + ) + + // Filter based on resource + if len(rr) > 0 { + resources = make([]rbac.Resource, 0, len(rr)) + for _, r := range rr { + if err = rbacResourceValidator(r); err != nil { + return nil, fmt.Errorf("can not use resource %q: %w", r, err) + } + + resources = append(resources, rbac.NewResource(r)) + } + } else { + resources = svc.Resources() + } + + for _, res := range resources { + for _, rule := range rules.FilterResource(res.RbacResource()) { + if _, ok := ruleMap[uniqRuleID(rule)]; !ok { + out = append(out, rule) + ruleMap[uniqRuleID(rule)] = true + } + } + } + + // Filter for Excluded, Include, or Exclusive specific rules + switch specific { + // Exclude all the specific rules + case filter.StateExcluded: + out = out.FilterRules(false) + // Returns only all the specific rules + case filter.StateExclusive: + out = out.FilterRules(true) + } + + return +} + // FindRulesByRoleID find all rules for a specific role // // This function is auto-generated diff --git a/compose/rest.yaml b/compose/rest.yaml index 88eecc61f..6b7dd0029 100644 --- a/compose/rest.yaml +++ b/compose/rest.yaml @@ -1291,6 +1291,7 @@ endpoints: - Client ID - Session ID imports: + - github.com/cortezaproject/corteza-server/pkg/filter - github.com/cortezaproject/corteza-server/pkg/rbac apis: - name: list @@ -1333,6 +1334,15 @@ endpoints: type: uint64 required: true title: Role ID + get: + - name: specific + required: false + title: Exclude (0, default), include (1) or return only (2) specific rules + type: "filter.State" + - name: resource + type: "[]string" + required: false + title: Show only rules for a specific resource - name: delete path: "/{roleID}/rules" method: DELETE diff --git a/compose/rest/permissions.go b/compose/rest/permissions.go index 45a95e209..9197ba550 100644 --- a/compose/rest/permissions.go +++ b/compose/rest/permissions.go @@ -2,11 +2,11 @@ package rest import ( "context" - "github.com/cortezaproject/corteza-server/compose/rest/request" "github.com/cortezaproject/corteza-server/compose/service" "github.com/cortezaproject/corteza-server/compose/types" "github.com/cortezaproject/corteza-server/pkg/api" + "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/cortezaproject/corteza-server/pkg/rbac" ) @@ -20,6 +20,7 @@ type ( Trace(context.Context, uint64, []uint64, ...string) ([]*rbac.Trace, error) List() []map[string]string FindRulesByRoleID(context.Context, uint64) (rbac.RuleSet, error) + FindRules(ctx context.Context, roleID uint64, specific filter.State, rr ...string) (rbac.RuleSet, error) Grant(ctx context.Context, rr ...*rbac.Rule) error } ) @@ -43,7 +44,7 @@ func (ctrl Permissions) List(ctx context.Context, r *request.PermissionsList) (i } func (ctrl Permissions) Read(ctx context.Context, r *request.PermissionsRead) (interface{}, error) { - return ctrl.ac.FindRulesByRoleID(ctx, r.RoleID) + return ctrl.ac.FindRules(ctx, r.RoleID, r.Specific, r.Resource...) } func (ctrl Permissions) Delete(ctx context.Context, r *request.PermissionsDelete) (interface{}, error) { diff --git a/compose/rest/request/permissions.go b/compose/rest/request/permissions.go index a2cc3cd54..9cf939907 100644 --- a/compose/rest/request/permissions.go +++ b/compose/rest/request/permissions.go @@ -11,6 +11,7 @@ package request import ( "encoding/json" "fmt" + "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/cortezaproject/corteza-server/pkg/payload" "github.com/cortezaproject/corteza-server/pkg/rbac" "github.com/go-chi/chi/v5" @@ -66,6 +67,16 @@ type ( // // Role ID RoleID uint64 `json:",string"` + + // Specific GET parameter + // + // Exclude (0, default), include (1) or return only (2) specific rules + Specific filter.State + + // Resource GET parameter + // + // Show only rules for a specific resource + Resource []string } PermissionsDelete struct { @@ -216,7 +227,9 @@ func NewPermissionsRead() *PermissionsRead { // Auditable returns all auditable/loggable parameters func (r PermissionsRead) Auditable() map[string]interface{} { return map[string]interface{}{ - "roleID": r.RoleID, + "roleID": r.RoleID, + "specific": r.Specific, + "resource": r.Resource, } } @@ -225,9 +238,42 @@ func (r PermissionsRead) GetRoleID() uint64 { return r.RoleID } +// Auditable returns all auditable/loggable parameters +func (r PermissionsRead) GetSpecific() filter.State { + return r.Specific +} + +// Auditable returns all auditable/loggable parameters +func (r PermissionsRead) GetResource() []string { + return r.Resource +} + // Fill processes request and fills internal variables func (r *PermissionsRead) Fill(req *http.Request) (err error) { + { + // GET params + tmp := req.URL.Query() + + if val, ok := tmp["specific"]; ok && len(val) > 0 { + r.Specific, err = payload.ParseFilterState(val[0]), nil + if err != nil { + return err + } + } + if val, ok := tmp["resource[]"]; ok { + r.Resource, err = val, nil + if err != nil { + return err + } + } else if val, ok := tmp["resource"]; ok { + r.Resource, err = val, nil + if err != nil { + return err + } + } + } + { var val string // path params diff --git a/compose/service/access_control.gen.go b/compose/service/access_control.gen.go index 696b85436..73a5a3bbf 100644 --- a/compose/service/access_control.gen.go +++ b/compose/service/access_control.gen.go @@ -12,6 +12,7 @@ import ( "github.com/cortezaproject/corteza-server/compose/types" "github.com/cortezaproject/corteza-server/pkg/actionlog" internalAuth "github.com/cortezaproject/corteza-server/pkg/auth" + "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/cortezaproject/corteza-server/pkg/rbac" systemTypes "github.com/cortezaproject/corteza-server/system/types" "github.com/spf13/cast" @@ -369,6 +370,63 @@ func (svc accessControl) logGrants(ctx context.Context, rr []*rbac.Rule) { } } +// FindRules find all rules based on filters +// +// This function is auto-generated +func (svc accessControl) FindRules(ctx context.Context, roleID uint64, specific filter.State, rr ...string) (out rbac.RuleSet, err error) { + if !svc.CanGrant(ctx) { + return nil, AccessControlErrNotAllowedToSetPermissions() + } + + rules, err := svc.FindRulesByRoleID(ctx, roleID) + if err != nil { + return + } + + var ( + resources []rbac.Resource + ruleMap = make(map[string]bool) + uniqRuleID = func(r *rbac.Rule) string { + return fmt.Sprintf("%s|%s|%d", r.Resource, r.Operation, r.RoleID) + } + ) + + // Filter based on resource + if len(rr) > 0 { + resources = make([]rbac.Resource, 0, len(rr)) + for _, r := range rr { + if err = rbacResourceValidator(r); err != nil { + return nil, fmt.Errorf("can not use resource %q: %w", r, err) + } + + resources = append(resources, rbac.NewResource(r)) + } + } else { + resources = svc.Resources() + } + + for _, res := range resources { + for _, rule := range rules.FilterResource(res.RbacResource()) { + if _, ok := ruleMap[uniqRuleID(rule)]; !ok { + out = append(out, rule) + ruleMap[uniqRuleID(rule)] = true + } + } + } + + // Filter for Excluded, Include, or Exclusive specific rules + switch specific { + // Exclude all the specific rules + case filter.StateExcluded: + out = out.FilterRules(false) + // Returns only all the specific rules + case filter.StateExclusive: + out = out.FilterRules(true) + } + + return +} + // FindRulesByRoleID find all rules for a specific role // // This function is auto-generated diff --git a/federation/rest.yaml b/federation/rest.yaml index f93e55f13..fce40a112 100644 --- a/federation/rest.yaml +++ b/federation/rest.yaml @@ -503,6 +503,7 @@ endpoints: - Client ID - Session ID imports: + - github.com/cortezaproject/corteza-server/pkg/filter - github.com/cortezaproject/corteza-server/pkg/rbac apis: - name: list @@ -545,6 +546,15 @@ endpoints: type: uint64 required: true title: Role ID + get: + - name: specific + required: false + title: Exclude (0, default), include (1) or return only (2) specific rules + type: "filter.State" + - name: resource + type: "[]string" + required: false + title: Show only rules for a specific resource - name: delete path: "/{roleID}/rules" method: DELETE diff --git a/federation/rest/permissions.go b/federation/rest/permissions.go index 478290253..47e8abec4 100644 --- a/federation/rest/permissions.go +++ b/federation/rest/permissions.go @@ -2,11 +2,11 @@ package rest import ( "context" - "github.com/cortezaproject/corteza-server/federation/rest/request" "github.com/cortezaproject/corteza-server/federation/service" "github.com/cortezaproject/corteza-server/federation/types" "github.com/cortezaproject/corteza-server/pkg/api" + "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/cortezaproject/corteza-server/pkg/rbac" ) @@ -20,6 +20,7 @@ type ( Trace(context.Context, uint64, []uint64, ...string) ([]*rbac.Trace, error) List() []map[string]string FindRulesByRoleID(context.Context, uint64) (rbac.RuleSet, error) + FindRules(ctx context.Context, roleID uint64, specific filter.State, rr ...string) (rbac.RuleSet, error) Grant(ctx context.Context, rr ...*rbac.Rule) error } ) @@ -43,7 +44,7 @@ func (ctrl Permissions) List(ctx context.Context, r *request.PermissionsList) (i } func (ctrl Permissions) Read(ctx context.Context, r *request.PermissionsRead) (interface{}, error) { - return ctrl.ac.FindRulesByRoleID(ctx, r.RoleID) + return ctrl.ac.FindRules(ctx, r.RoleID, r.Specific, r.Resource...) } func (ctrl Permissions) Delete(ctx context.Context, r *request.PermissionsDelete) (interface{}, error) { diff --git a/federation/rest/request/permissions.go b/federation/rest/request/permissions.go index a2cc3cd54..9cf939907 100644 --- a/federation/rest/request/permissions.go +++ b/federation/rest/request/permissions.go @@ -11,6 +11,7 @@ package request import ( "encoding/json" "fmt" + "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/cortezaproject/corteza-server/pkg/payload" "github.com/cortezaproject/corteza-server/pkg/rbac" "github.com/go-chi/chi/v5" @@ -66,6 +67,16 @@ type ( // // Role ID RoleID uint64 `json:",string"` + + // Specific GET parameter + // + // Exclude (0, default), include (1) or return only (2) specific rules + Specific filter.State + + // Resource GET parameter + // + // Show only rules for a specific resource + Resource []string } PermissionsDelete struct { @@ -216,7 +227,9 @@ func NewPermissionsRead() *PermissionsRead { // Auditable returns all auditable/loggable parameters func (r PermissionsRead) Auditable() map[string]interface{} { return map[string]interface{}{ - "roleID": r.RoleID, + "roleID": r.RoleID, + "specific": r.Specific, + "resource": r.Resource, } } @@ -225,9 +238,42 @@ func (r PermissionsRead) GetRoleID() uint64 { return r.RoleID } +// Auditable returns all auditable/loggable parameters +func (r PermissionsRead) GetSpecific() filter.State { + return r.Specific +} + +// Auditable returns all auditable/loggable parameters +func (r PermissionsRead) GetResource() []string { + return r.Resource +} + // Fill processes request and fills internal variables func (r *PermissionsRead) Fill(req *http.Request) (err error) { + { + // GET params + tmp := req.URL.Query() + + if val, ok := tmp["specific"]; ok && len(val) > 0 { + r.Specific, err = payload.ParseFilterState(val[0]), nil + if err != nil { + return err + } + } + if val, ok := tmp["resource[]"]; ok { + r.Resource, err = val, nil + if err != nil { + return err + } + } else if val, ok := tmp["resource"]; ok { + r.Resource, err = val, nil + if err != nil { + return err + } + } + } + { var val string // path params diff --git a/federation/service/access_control.gen.go b/federation/service/access_control.gen.go index 1dd1b8f25..eb457a2fd 100644 --- a/federation/service/access_control.gen.go +++ b/federation/service/access_control.gen.go @@ -12,6 +12,7 @@ import ( "github.com/cortezaproject/corteza-server/federation/types" "github.com/cortezaproject/corteza-server/pkg/actionlog" internalAuth "github.com/cortezaproject/corteza-server/pkg/auth" + "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/cortezaproject/corteza-server/pkg/rbac" systemTypes "github.com/cortezaproject/corteza-server/system/types" "github.com/spf13/cast" @@ -246,6 +247,63 @@ func (svc accessControl) logGrants(ctx context.Context, rr []*rbac.Rule) { } } +// FindRules find all rules based on filters +// +// This function is auto-generated +func (svc accessControl) FindRules(ctx context.Context, roleID uint64, specific filter.State, rr ...string) (out rbac.RuleSet, err error) { + if !svc.CanGrant(ctx) { + return nil, AccessControlErrNotAllowedToSetPermissions() + } + + rules, err := svc.FindRulesByRoleID(ctx, roleID) + if err != nil { + return + } + + var ( + resources []rbac.Resource + ruleMap = make(map[string]bool) + uniqRuleID = func(r *rbac.Rule) string { + return fmt.Sprintf("%s|%s|%d", r.Resource, r.Operation, r.RoleID) + } + ) + + // Filter based on resource + if len(rr) > 0 { + resources = make([]rbac.Resource, 0, len(rr)) + for _, r := range rr { + if err = rbacResourceValidator(r); err != nil { + return nil, fmt.Errorf("can not use resource %q: %w", r, err) + } + + resources = append(resources, rbac.NewResource(r)) + } + } else { + resources = svc.Resources() + } + + for _, res := range resources { + for _, rule := range rules.FilterResource(res.RbacResource()) { + if _, ok := ruleMap[uniqRuleID(rule)]; !ok { + out = append(out, rule) + ruleMap[uniqRuleID(rule)] = true + } + } + } + + // Filter for Excluded, Include, or Exclusive specific rules + switch specific { + // Exclude all the specific rules + case filter.StateExcluded: + out = out.FilterRules(false) + // Returns only all the specific rules + case filter.StateExclusive: + out = out.FilterRules(true) + } + + return +} + // FindRulesByRoleID find all rules for a specific role // // This function is auto-generated diff --git a/pkg/codegen/rest.go b/pkg/codegen/rest.go index 2242712dd..14a929f6e 100644 --- a/pkg/codegen/rest.go +++ b/pkg/codegen/rest.go @@ -220,6 +220,8 @@ func (d *restEndpointParamDef) Parser(arg string) string { return fmt.Sprintf("payload.Parse%s(%s), nil", export(d.Type), arg) case "string", "[]string": return fmt.Sprintf("%s, nil", arg) + case "filter.State": + return fmt.Sprintf("payload.ParseFilterState(%s), nil", arg) default: return fmt.Sprintf("%s(%s), nil", d.Type, arg) } diff --git a/pkg/payload/util.go b/pkg/payload/util.go index fe1dd814b..185c1a26b 100644 --- a/pkg/payload/util.go +++ b/pkg/payload/util.go @@ -2,6 +2,7 @@ package payload import ( "fmt" + "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/jmoiron/sqlx/types" "github.com/spf13/cast" "strconv" @@ -100,3 +101,7 @@ func ParseInt64(s string) int64 { func ParseBool(s string) bool { return cast.ToBool(s) } + +func ParseFilterState(s string) filter.State { + return filter.State(cast.ToUint(s)) +} diff --git a/pkg/payload/util_test.go b/pkg/payload/util_test.go new file mode 100644 index 000000000..630cc1f62 --- /dev/null +++ b/pkg/payload/util_test.go @@ -0,0 +1,50 @@ +package payload + +import ( + "github.com/cortezaproject/corteza-server/pkg/filter" + "github.com/stretchr/testify/require" + "testing" +) + +func TestParseFilterState(t *testing.T) { + var ( + req = require.New(t) + ) + tests := []struct { + name string + input string + expected filter.State + }{ + { + "invalid string should be Excluded", + "zero", + filter.StateExcluded, + }, + { + "empty string should be Excluded", + "0", + filter.StateExcluded, + }, + { + "Excluded", + "0", + filter.StateExcluded, + }, + { + "Excluded", + "1", + filter.StateInclusive, + }, + { + "Excluded", + "2", + filter.StateExclusive, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req.Equal(tt.expected, ParseFilterState(tt.input)) + }) + } +} diff --git a/pkg/rbac/resource.go b/pkg/rbac/resource.go index cbfbe6064..25b593956 100644 --- a/pkg/rbac/resource.go +++ b/pkg/rbac/resource.go @@ -102,3 +102,29 @@ func level(r string) (score int) { return } + +// isSpecific will return true if rule relates to a specific resource +// +// ns::cmp:res/xx/* returns true +// ns::cmp:res/xx/xx returns true +// ns::cmp:res/ returns false +// ns::cmp:res/*/* returns false +func isSpecific(r string) (out bool) { + out = false + parts := strings.Split(r, pathSep) + // remove resource name part + parts = parts[1:] + + // check parts for wildcard if it's not empty otherwise return false + for _, p := range parts { + if len(p) == 0 { + continue + } + + if p != wildcard && !out { + out = true + } + } + + return +} diff --git a/pkg/rbac/resource_test.go b/pkg/rbac/resource_test.go index add5b5fa2..9addd7ab1 100644 --- a/pkg/rbac/resource_test.go +++ b/pkg/rbac/resource_test.go @@ -115,3 +115,27 @@ func TestLevel(t *testing.T) { }) } } + +func TestIsSpecific(t *testing.T) { + var ( + tcc = []struct { + r string + e bool + }{ + {"corteza::test/a/b/c", true}, + {"corteza::test/a/b/*", true}, + {"corteza::test/a/*/*", true}, + {"corteza::test/a/*/123", true}, + {"corteza::test/*/*/*", false}, + {"corteza::test/*/*", false}, + {"corteza::test/*", false}, + {"corteza::test/", false}, + } + ) + + for _, tc := range tcc { + t.Run(tc.r, func(t *testing.T) { + require.Equal(t, tc.e, isSpecific(tc.r)) + }) + } +} diff --git a/pkg/rbac/rule.go b/pkg/rbac/rule.go index 27ad04ca0..8ab9d16db 100644 --- a/pkg/rbac/rule.go +++ b/pkg/rbac/rule.go @@ -62,7 +62,7 @@ func (set RuleSet) FilterAccess(a Access) (out RuleSet) { func (set RuleSet) FilterResource(res string) (out RuleSet) { for _, r := range set { - if !matchResource(r.Resource, res) { + if !matchResource(res, r.Resource) { continue } out = append(out, r) @@ -71,6 +71,18 @@ func (set RuleSet) FilterResource(res string) (out RuleSet) { return } +// FilterRules will filter the rules based on given parameter(specific), +// If params is true then it will return only the specific rules otherwise it will return non-specific rules +func (set RuleSet) FilterRules(specific bool) (out RuleSet) { + for _, r := range set { + if specific == isSpecific(r.Resource) { + out = append(out, r) + } + } + + return +} + // AllowRule helper func to create allow rule func AllowRule(id uint64, r, o string) *Rule { return &Rule{id, r, o, Allow, false} diff --git a/system/rest.yaml b/system/rest.yaml index afeff312d..c2ce337eb 100644 --- a/system/rest.yaml +++ b/system/rest.yaml @@ -1219,6 +1219,7 @@ endpoints: - Client ID - Session ID imports: + - github.com/cortezaproject/corteza-server/pkg/filter - github.com/cortezaproject/corteza-server/pkg/rbac apis: - name: list @@ -1262,6 +1263,15 @@ endpoints: type: uint64 required: true title: Role ID + get: + - name: specific + required: false + title: Exclude (0, default), include (1) or return only (2) specific rules + type: "filter.State" + - name: resource + type: "[]string" + required: false + title: Show only rules for a specific resource - name: delete path: "/{roleID}/rules" method: DELETE diff --git a/system/rest/permissions.go b/system/rest/permissions.go index c7a2ca0ca..e663776de 100644 --- a/system/rest/permissions.go +++ b/system/rest/permissions.go @@ -2,8 +2,8 @@ package rest import ( "context" - "github.com/cortezaproject/corteza-server/pkg/api" + "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/cortezaproject/corteza-server/pkg/rbac" "github.com/cortezaproject/corteza-server/system/rest/request" "github.com/cortezaproject/corteza-server/system/service" @@ -20,6 +20,7 @@ type ( Trace(context.Context, uint64, []uint64, ...string) ([]*rbac.Trace, error) List() []map[string]string FindRulesByRoleID(context.Context, uint64) (rbac.RuleSet, error) + FindRules(ctx context.Context, roleID uint64, specific filter.State, rr ...string) (rbac.RuleSet, error) Grant(ctx context.Context, rr ...*rbac.Rule) error } ) @@ -43,7 +44,7 @@ func (ctrl Permissions) List(ctx context.Context, r *request.PermissionsList) (i } func (ctrl Permissions) Read(ctx context.Context, r *request.PermissionsRead) (interface{}, error) { - return ctrl.ac.FindRulesByRoleID(ctx, r.RoleID) + return ctrl.ac.FindRules(ctx, r.RoleID, r.Specific, r.Resource...) } func (ctrl Permissions) Delete(ctx context.Context, r *request.PermissionsDelete) (interface{}, error) { diff --git a/system/rest/request/permissions.go b/system/rest/request/permissions.go index a2cc3cd54..99452e4cb 100644 --- a/system/rest/request/permissions.go +++ b/system/rest/request/permissions.go @@ -11,6 +11,7 @@ package request import ( "encoding/json" "fmt" + "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/cortezaproject/corteza-server/pkg/payload" "github.com/cortezaproject/corteza-server/pkg/rbac" "github.com/go-chi/chi/v5" @@ -66,6 +67,16 @@ type ( // // Role ID RoleID uint64 `json:",string"` + + // Specific GET parameter + // + // Exclude (0, default), include (1) or return only (2) specific rules + Specific filter.State + + // Resource GET parameter + // + // Show only rules for a specific resource + Resource []string } PermissionsDelete struct { @@ -216,7 +227,9 @@ func NewPermissionsRead() *PermissionsRead { // Auditable returns all auditable/loggable parameters func (r PermissionsRead) Auditable() map[string]interface{} { return map[string]interface{}{ - "roleID": r.RoleID, + "roleID": r.RoleID, + "specific": r.Specific, + "resource": r.Resource, } } @@ -225,9 +238,43 @@ func (r PermissionsRead) GetRoleID() uint64 { return r.RoleID } +// Auditable returns all auditable/loggable parameters +func (r PermissionsRead) GetSpecific() filter.State { + return r.Specific +} + +// Auditable returns all auditable/loggable parameters +func (r PermissionsRead) GetResource() []string { + return r.Resource +} + // Fill processes request and fills internal variables func (r *PermissionsRead) Fill(req *http.Request) (err error) { + { + // GET params + tmp := req.URL.Query() + + if val, ok := tmp["specific"]; ok && len(val) > 0 { + + r.Specific, err = payload.ParseFilterState(val[0]), nil + if err != nil { + return err + } + } + if val, ok := tmp["resource[]"]; ok { + r.Resource, err = val, nil + if err != nil { + return err + } + } else if val, ok := tmp["resource"]; ok { + r.Resource, err = val, nil + if err != nil { + return err + } + } + } + { var val string // path params diff --git a/system/service/access_control.gen.go b/system/service/access_control.gen.go index ad9175425..9056a9f19 100644 --- a/system/service/access_control.gen.go +++ b/system/service/access_control.gen.go @@ -11,6 +11,7 @@ import ( "fmt" "github.com/cortezaproject/corteza-server/pkg/actionlog" internalAuth "github.com/cortezaproject/corteza-server/pkg/auth" + "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/cortezaproject/corteza-server/pkg/rbac" "github.com/cortezaproject/corteza-server/system/types" systemTypes "github.com/cortezaproject/corteza-server/system/types" @@ -576,6 +577,63 @@ func (svc accessControl) logGrants(ctx context.Context, rr []*rbac.Rule) { } } +// FindRules find all rules based on filters +// +// This function is auto-generated +func (svc accessControl) FindRules(ctx context.Context, roleID uint64, specific filter.State, rr ...string) (out rbac.RuleSet, err error) { + if !svc.CanGrant(ctx) { + return nil, AccessControlErrNotAllowedToSetPermissions() + } + + rules, err := svc.FindRulesByRoleID(ctx, roleID) + if err != nil { + return + } + + var ( + resources []rbac.Resource + ruleMap = make(map[string]bool) + uniqRuleID = func(r *rbac.Rule) string { + return fmt.Sprintf("%s|%s|%d", r.Resource, r.Operation, r.RoleID) + } + ) + + // Filter based on resource + if len(rr) > 0 { + resources = make([]rbac.Resource, 0, len(rr)) + for _, r := range rr { + if err = rbacResourceValidator(r); err != nil { + return nil, fmt.Errorf("can not use resource %q: %w", r, err) + } + + resources = append(resources, rbac.NewResource(r)) + } + } else { + resources = svc.Resources() + } + + for _, res := range resources { + for _, rule := range rules.FilterResource(res.RbacResource()) { + if _, ok := ruleMap[uniqRuleID(rule)]; !ok { + out = append(out, rule) + ruleMap[uniqRuleID(rule)] = true + } + } + } + + // Filter for Excluded, Include, or Exclusive specific rules + switch specific { + // Exclude all the specific rules + case filter.StateExcluded: + out = out.FilterRules(false) + // Returns only all the specific rules + case filter.StateExclusive: + out = out.FilterRules(true) + } + + return +} + // FindRulesByRoleID find all rules for a specific role // // This function is auto-generated diff --git a/tests/system/permissions_test.go b/tests/system/permissions_test.go index e079e7e0a..921c1f82c 100644 --- a/tests/system/permissions_test.go +++ b/tests/system/permissions_test.go @@ -2,6 +2,7 @@ package system import ( "fmt" + "github.com/cortezaproject/corteza-server/pkg/id" "net/http" "strconv" "testing" @@ -30,7 +31,7 @@ func TestPermissionsList(t *testing.T) { helpers.AllowMe(h, types.ComponentRbacResource(), "grant") - h.apiInit(). + json := h.apiInit(). Get("/permissions/"). Header("Accept", "application/json"). Expect(t). @@ -38,6 +39,8 @@ func TestPermissionsList(t *testing.T) { Assert(helpers.AssertNoErrors). Assert(jsonpath.Present(fmt.Sprintf(`$.response[? @.type=="%s"]`, types.ComponentResourceType))). End() + + fmt.Println("json: ", json.Response.Body) } func TestPermissionsRead(t *testing.T) { @@ -54,6 +57,63 @@ func TestPermissionsRead(t *testing.T) { End() } +func TestPermissionsReadWithFilter(t *testing.T) { + h := newHelper(t) + + helpers.AllowMe(h, types.ComponentRbacResource(), "grant") + helpers.DenyMe(h, types.ComponentRbacResource(), "user.create") + + // Specific resource related rules + testID := id.Next() + helpers.AllowMe(h, types.UserRbacResource(testID), "read") + helpers.AllowMe(h, types.UserRbacResource(testID), "update") + + // Only non-specific resource rules with `specific: 0` filter + h.apiInit(). + Getf("/permissions/%d/rules", h.roleID). + Query("specific", "0"). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + Assert(jsonpath.Len(`$.response`, 2)). + End() + + // Including all specific and non-specific resource rules with `specific: 1` filter + h.apiInit(). + Getf("/permissions/%d/rules", h.roleID). + Query("specific", "1"). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + Assert(jsonpath.Len(`$.response`, 4)). + End() + + // Only specific resource rules with `specific: 2` filter + h.apiInit(). + Getf("/permissions/%d/rules", h.roleID). + Query("specific", "2"). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + Assert(jsonpath.Len(`$.response`, 2)). + End() + + // Only resource related rules with `resource: corteza::system:user/{ID}` + h.apiInit(). + Getf("/permissions/%d/rules", h.roleID). + Query("specific", "1"). + Query("resource", fmt.Sprintf("corteza::system:user/%d", testID)). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + Assert(jsonpath.Len(`$.response`, 2)). + End() +} + func TestPermissionsUpdate(t *testing.T) { h := newHelper(t) helpers.AllowMe(h, types.ComponentRbacResource(), "grant")