diff --git a/Depfile b/Depfile index 538a80f..c0f1078 100644 --- a/Depfile +++ b/Depfile @@ -8,4 +8,7 @@ go: version: "v1.10.1" golangci-lint: importPath: "github.com/golangci/golangci-lint/cmd/golangci-lint" - version: "v1.53.3" + version: "v1.54.1" + mockgen: + importPath: github.com/golang/mock/mockgen + version: "v1.6.0" diff --git a/cache/cache.go b/cache/cache.go index c3393ff..0f81788 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -4,6 +4,7 @@ import ( "sync" "github.com/aserto-dev/azm/model" + "github.com/aserto-dev/azm/model/diff" ) type Cache struct { @@ -27,6 +28,13 @@ func (c *Cache) UpdateModel(m *model.Model) error { return nil } +// Returns a diff struct resulted between the old and the new model. +func (c *Cache) Diff(other *model.Model) *diff.Diff { + c.mtx.Lock() + defer c.mtx.Unlock() + return c.model.Diff(other) +} + // ObjectExists, checks if given object type name exists in the model cache. func (c *Cache) ObjectExists(on model.ObjectName) bool { c.mtx.RLock() diff --git a/go.mod b/go.mod index 36f7d5f..0152531 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,8 @@ require ( github.com/aserto-dev/errors v0.0.6 github.com/aserto-dev/go-aserto v0.20.4 github.com/aserto-dev/go-directory v0.30.0 + github.com/golang/mock v1.1.1 + github.com/hashicorp/go-multierror v1.1.1 github.com/magefile/mage v1.15.0 github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 github.com/pkg/errors v0.9.1 @@ -26,6 +28,7 @@ require ( github.com/google/uuid v1.3.1 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index e5cdd25..3af91f9 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,7 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -46,6 +47,10 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 h1:RtRsiaGvWxcwd8y3BiRZxsylPT8hLWZ5SPcfI+3IDNk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0/go.mod h1:TzP6duP4Py2pHLVPPQp42aoYI92+PCrVotyR5e8Vqlk= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= diff --git a/magefiles/magefile.go b/magefiles/magefile.go index dd2a4f0..cb2c819 100644 --- a/magefiles/magefile.go +++ b/magefiles/magefile.go @@ -39,3 +39,8 @@ func Test() error { func Deps() { deps.GetAllDeps() } + +// Generate generates all code. +func Generate() error { + return common.Generate() +} diff --git a/model/diff/diff.go b/model/diff/diff.go new file mode 100644 index 0000000..b8dcf8e --- /dev/null +++ b/model/diff/diff.go @@ -0,0 +1,100 @@ +package diff + +import ( + "github.com/aserto-dev/go-directory/pkg/derr" + "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" + "github.com/samber/lo" +) + +//go:generate go run github.com/golang/mock/mockgen -destination=mock_instances.go -package=diff github.com/aserto-dev/azm/model/diff Instances + +type Diff struct { + Added Changes + Removed Changes +} + +type Changes struct { + Objects []string + Relations map[string][]string +} + +// Only the types of the relation instances are needed. +type RelationKind struct { + Object string + Relation string + Subject string + SubjectRelation string +} + +type Instances interface { + ObjectTypes() ([]string, error) + RelationTypes() ([]*RelationKind, error) +} + +func (d *Diff) Validate(dv Instances) error { + var errs error + var rels []*RelationKind + if len(d.Removed.Objects) > 0 { + objs, err := dv.ObjectTypes() + if err != nil { + return err + } + + rels, err = dv.RelationTypes() + if err != nil { + return err + } + + err = d.validateObjectTypes(objs, rels) + errs = multierror.Append(errs, err) + } + + if len(d.Removed.Relations) > 0 { + var err error + if len(rels) == 0 { + rels, err = dv.RelationTypes() + if err != nil { + return err + } + + } + err = d.validateRelationsTypes(rels) + errs = multierror.Append(errs, err) + } + + if merr, ok := errs.(*multierror.Error); ok && len(merr.Errors) > 0 { + return errs + } + + return nil +} + +func (d *Diff) validateObjectTypes(objs []string, rels []*RelationKind) error { + var errs error + for _, objType := range d.Removed.Objects { + if lo.Contains(objs, objType) { + errs = multierror.Append(errs, errors.Wrapf(derr.ErrObjectTypeInUse, "object type [%s]", objType)) + } + rel, found := lo.Find(rels, func(rel *RelationKind) bool { return rel.Object == objType || rel.Subject == objType }) + if found { + errs = multierror.Append(errs, errors.Wrapf(derr.ErrRelationTypeInUse, "object type [%s], relation type [%s]", objType, rel.Relation)) + } + } + return errs +} + +func (d *Diff) validateRelationsTypes(relations []*RelationKind) error { + var errs error + for objType, rels := range d.Removed.Relations { + for _, rel := range rels { + _, found := lo.Find(relations, func(rl *RelationKind) bool { + return (rl.Object == objType && rl.Relation == rel) || (rl.Subject == objType && rl.SubjectRelation == rel) + }) + if found { + errs = multierror.Append(errs, errors.Wrapf(derr.ErrRelationTypeInUse, "object type [%s], relation type [%s]", objType, rel)) + } + } + } + return errs +} diff --git a/model/diff/diff_test.go b/model/diff/diff_test.go new file mode 100644 index 0000000..07cda49 --- /dev/null +++ b/model/diff/diff_test.go @@ -0,0 +1,83 @@ +package diff_test + +import ( + "errors" + "testing" + + "github.com/aserto-dev/azm/model/diff" + "github.com/aserto-dev/go-directory/pkg/derr" + gomock "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" +) + +var ErrBoom = errors.New("Boom") + +func TestValidateDiffNoDeletion(t *testing.T) { + ctrl := gomock.NewController(t) + mockInstances := diff.NewMockInstances(ctrl) + + dif := diff.Diff{Removed: diff.Changes{}, Added: diff.Changes{}} + err := dif.Validate(mockInstances) + + require.NoError(t, err) +} + +func TestValidateDiffWithObjectTypeDeletion(t *testing.T) { + ctrl := gomock.NewController(t) + mockInstances := diff.NewMockInstances(ctrl) + objType := "user" + + dif := diff.Diff{Removed: diff.Changes{Objects: []string{objType}}, Added: diff.Changes{}} + + mockInstances.EXPECT().ObjectTypes().Return([]string{}, nil) + mockInstances.EXPECT().RelationTypes().Return([]*diff.RelationKind{}, nil) + err := dif.Validate(mockInstances) + + require.NoError(t, err) +} + +func TestValidateDiffWith2ObjectTypeDeletion(t *testing.T) { + ctrl := gomock.NewController(t) + mockInstances := diff.NewMockInstances(ctrl) + objTypes := []string{"user", "member"} + + dif := diff.Diff{Removed: diff.Changes{Objects: objTypes}, Added: diff.Changes{}} + + mockInstances.EXPECT().ObjectTypes().Return([]string{"user"}, nil) + mockInstances.EXPECT().RelationTypes().Return([]*diff.RelationKind{}, nil) + err := dif.Validate(mockInstances) + + require.Error(t, err) + require.Contains(t, err.Error(), derr.ErrObjectTypeInUse.Message) +} + +func TestValidateDiffWithRelationTypeDeletion(t *testing.T) { + ctrl := gomock.NewController(t) + mockInstances := diff.NewMockInstances(ctrl) + objTypes := []string{"user", "member"} + relationTypes := map[string][]string{"folder": {"parent_folder"}} + + dif := diff.Diff{Removed: diff.Changes{Objects: objTypes, Relations: relationTypes}, Added: diff.Changes{}} + + mockInstances.EXPECT().ObjectTypes().Return([]string{}, nil) + mockInstances.EXPECT().RelationTypes().Return([]*diff.RelationKind{{Object: "folder", Relation: "parent_folder"}}, nil) + err := dif.Validate(mockInstances) + + require.Error(t, err) + require.Contains(t, err.Error(), derr.ErrRelationTypeInUse.Message) +} + +func TestValidateDiffWithObjectInstances(t *testing.T) { + ctrl := gomock.NewController(t) + mockInstances := diff.NewMockInstances(ctrl) + objTypes := []string{"user", "member"} + relationTypes := map[string][]string{"folder": {"parent_folder"}} + + dif := diff.Diff{Removed: diff.Changes{Objects: objTypes, Relations: relationTypes}, Added: diff.Changes{}} + + mockInstances.EXPECT().ObjectTypes().Return([]string{}, ErrBoom) + err := dif.Validate(mockInstances) + + require.Error(t, err) + require.Contains(t, err.Error(), ErrBoom.Error()) +} diff --git a/model/diff/mock_instances.go b/model/diff/mock_instances.go new file mode 100644 index 0000000..3b50bfa --- /dev/null +++ b/model/diff/mock_instances.go @@ -0,0 +1,63 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/aserto-dev/azm/model/diff (interfaces: Instances) + +// Package diff is a generated GoMock package. +package diff + +import ( + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockInstances is a mock of Instances interface +type MockInstances struct { + ctrl *gomock.Controller + recorder *MockInstancesMockRecorder +} + +// MockInstancesMockRecorder is the mock recorder for MockInstances +type MockInstancesMockRecorder struct { + mock *MockInstances +} + +// NewMockInstances creates a new mock instance +func NewMockInstances(ctrl *gomock.Controller) *MockInstances { + mock := &MockInstances{ctrl: ctrl} + mock.recorder = &MockInstancesMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockInstances) EXPECT() *MockInstancesMockRecorder { + return m.recorder +} + +// ObjectTypes mocks base method +func (m *MockInstances) ObjectTypes() ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ObjectTypes") + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ObjectTypes indicates an expected call of ObjectTypes +func (mr *MockInstancesMockRecorder) ObjectTypes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObjectTypes", reflect.TypeOf((*MockInstances)(nil).ObjectTypes)) +} + +// RelationTypes mocks base method +func (m *MockInstances) RelationTypes() ([]*RelationKind, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RelationTypes") + ret0, _ := ret[0].([]*RelationKind) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RelationTypes indicates an expected call of RelationTypes +func (mr *MockInstancesMockRecorder) RelationTypes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RelationTypes", reflect.TypeOf((*MockInstances)(nil).RelationTypes)) +} diff --git a/model/model.go b/model/model.go index 9aef76d..c1d612d 100644 --- a/model/model.go +++ b/model/model.go @@ -7,6 +7,7 @@ import ( "time" "github.com/aserto-dev/azm/graph" + "github.com/aserto-dev/azm/model/diff" ) const ModelVersion int = 1 @@ -59,16 +60,6 @@ type Metadata struct { ETag string `json:"etag"` } -type Diff struct { - Added Changes - Removed Changes -} - -type Changes struct { - Objects []ObjectName - Relations map[ObjectName][]RelationName -} - func New(r io.Reader) (*Model, error) { m := Model{} dec := json.NewDecoder(r) @@ -113,43 +104,43 @@ func (m *Model) Write(w io.Writer) error { return enc.Encode(m) } -func (m *Model) Diff(newModel *Model) *Diff { - // newModel - m => additions +func (m *Model) Diff(newModel *Model) *diff.Diff { + // newmodel - m => additions added := newModel.subtract(m) // m - newModel => deletions deleted := m.subtract(newModel) - return &Diff{Added: *added, Removed: *deleted} + return &diff.Diff{Added: *added, Removed: *deleted} } -func (m *Model) subtract(newModel *Model) *Changes { - changes := &Changes{ - Objects: make([]ObjectName, 0), - Relations: make(map[ObjectName][]RelationName), +func (m *Model) subtract(newModel *Model) *diff.Changes { + chgs := &diff.Changes{ + Objects: make([]string, 0), + Relations: make(map[string][]string), } if m == nil { - return changes + return chgs } if newModel == nil { for objName := range m.Objects { - changes.Objects = append(changes.Objects, objName) + chgs.Objects = append(chgs.Objects, string(objName)) } - return changes + return chgs } for objName, obj := range m.Objects { if newModel.Objects[objName] == nil { - changes.Objects = append(changes.Objects, objName) + chgs.Objects = append(chgs.Objects, string(objName)) } else { - for relName := range obj.Relations { - if newModel.Objects[objName].Relations[relName] == nil { - changes.Relations[objName] = append(changes.Relations[objName], relName) + for relname := range obj.Relations { + if newModel.Objects[objName].Relations[relname] == nil { + chgs.Relations[string(objName)] = append(chgs.Relations[string(objName)], string(relname)) } } } } - return changes + return chgs } diff --git a/model/model_test.go b/model/model_test.go index 45ae065..8b63d2d 100644 --- a/model/model_test.go +++ b/model/model_test.go @@ -215,16 +215,16 @@ func TestDiff(t *testing.T) { stretch.Equal(t, len(diffNilWithNil.Added.Relations), 0) stretch.Equal(t, len(diffNilWithNil.Removed.Relations), 0) - diffM1WithM3 := m1.Diff(&m3) - stretch.Equal(t, len(diffM1WithM3.Added.Objects), 1) - stretch.Equal(t, diffM1WithM3.Added.Objects[0], model.ObjectName("new_user")) - stretch.Equal(t, len(diffM1WithM3.Removed.Objects), 1) - stretch.Equal(t, diffM1WithM3.Removed.Objects[0], model.ObjectName("user")) - - stretch.Equal(t, len(diffM1WithM3.Added.Relations), 1) - stretch.Equal(t, diffM1WithM3.Added.Relations["folder"], []model.RelationName{"viewer"}) - stretch.Equal(t, len(diffM1WithM3.Removed.Relations), 1) - stretch.Equal(t, diffM1WithM3.Removed.Relations["document"], []model.RelationName{"parent_folder"}) + diffm1m3 := m1.Diff(&m3) + stretch.Equal(t, len(diffm1m3.Added.Objects), 1) + stretch.Equal(t, diffm1m3.Added.Objects[0], "new_user") + stretch.Equal(t, len(diffm1m3.Removed.Objects), 1) + stretch.Equal(t, diffm1m3.Removed.Objects[0], "user") + + stretch.Equal(t, len(diffm1m3.Added.Relations), 1) + stretch.Equal(t, diffm1m3.Added.Relations["folder"], []string{"viewer"}) + stretch.Equal(t, len(diffm1m3.Removed.Relations), 1) + stretch.Equal(t, diffm1m3.Removed.Relations["document"], []string{"parent_folder"}) } func TestGraph(t *testing.T) {