diff --git a/cmd/server/main.go b/cmd/server/main.go index ab685af0..d2cd81fd 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -131,7 +131,7 @@ func newChatConfig(c *Config) *server.ChatConfig { } func NewInteractor(c *Config) server.Interactor { - return interactor.NewInteractor( + i := interactor.NewInteractor( &interactor.InteractorConfig{ // Server Configurations: ServerHost: c.ServerHost, @@ -150,6 +150,10 @@ func NewInteractor(c *Config) server.Interactor { SCM: newSCM(c), }, ) + + i.Init() + + return i } func newStore(c *Config) interactor.Store { diff --git a/internal/interactor/_mock.sh b/internal/interactor/_mock.sh index 9053570c..e5daea2a 100644 --- a/internal/interactor/_mock.sh +++ b/internal/interactor/_mock.sh @@ -2,6 +2,7 @@ mockgen \ -aux_files \ github.com/gitploy-io/gitploy/internal/interactor=user.go\ ,github.com/gitploy-io/gitploy/internal/interactor=repo.go\ +,github.com/gitploy-io/gitploy/internal/interactor=perm.go\ ,github.com/gitploy-io/gitploy/internal/interactor=config.go\ ,github.com/gitploy-io/gitploy/internal/interactor=deployment.go\ ,github.com/gitploy-io/gitploy/internal/interactor=deploymentstatistics.go\ diff --git a/internal/interactor/interactor.go b/internal/interactor/interactor.go index 9c491777..224c01e9 100644 --- a/internal/interactor/interactor.go +++ b/internal/interactor/interactor.go @@ -1,68 +1,86 @@ package interactor import ( + "context" "fmt" + "time" evbus "github.com/asaskevich/EventBus" "go.uber.org/zap" ) -type ( - Interactor struct { - Store - SCM - - // The channel to stop background workers. - stopCh chan struct{} - - common *service - - // services used for talking to different parts of the entities. - *ConfigInteractor - *DeploymentInteractor - *DeploymentStatisticsInteractor - *EventInteractor - *LicenseInteractor - *LockInteractor - *RepoInteractor - *ReviewInteractor - *UserInteractor - } +type InteractorConfig struct { + ServerHost string + ServerProto string + ServerProxyHost string + ServerProxyProto string - InteractorConfig struct { - ServerHost string - ServerProto string - ServerProxyHost string - ServerProxyProto string + OrgEntries []string + MemberEntries []string + AdminUsers []string - OrgEntries []string - MemberEntries []string - AdminUsers []string + WebhookSecret string - WebhookSecret string + LicenseKey string - LicenseKey string + Store + SCM +} - Store - SCM +func (c *InteractorConfig) BuildWebhookURL() string { + if c.ServerProxyProto != "" && c.ServerProxyHost != "" { + return fmt.Sprintf("%s://%s/hooks", c.ServerProxyProto, c.ServerProxyHost) } - service struct { - store Store - scm SCM - log *zap.Logger + return fmt.Sprintf("%s://%s/hooks", c.ServerProto, c.ServerHost) +} + +func (c *InteractorConfig) CheckWebhookSSL() bool { + if c.ServerProxyProto != "" && c.ServerProxyHost != "" { + return c.ServerProxyProto == "https" } -) + + return c.ServerProto == "https" +} + +type service struct { + store Store + scm SCM + log *zap.Logger +} + +type Interactor struct { + Store + SCM + + // The channel to stop background workers. + stopCh chan struct{} + + common *service + + // services used for talking to different parts of the entities. + *ConfigInteractor + *DeploymentInteractor + *DeploymentStatisticsInteractor + *EventInteractor + *LicenseInteractor + *LockInteractor + *RepoInteractor + *ReviewInteractor + *UserInteractor + *PermInteractor +} func NewInteractor(c *InteractorConfig) *Interactor { + log := zap.L().Named("interactor") + defer log.Sync() + i := &Interactor{ Store: c.Store, SCM: c.SCM, stopCh: make(chan struct{}), } - log := zap.L().Named("interactor") - i.common = &service{ store: c.Store, scm: c.SCM, @@ -94,37 +112,38 @@ func NewInteractor(c *InteractorConfig) *Interactor { orgEntries: c.OrgEntries, memberEntries: c.MemberEntries, } + i.PermInteractor = &PermInteractor{ + service: i.common, + orgEntries: c.OrgEntries, + } + + return i +} + +func (i *Interactor) Init() { + log := zap.L().Named("interactor") + defer log.Sync() + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + log.Debug("Resync organization entries.") + if err := i.ResyncPerms(ctx); err != nil { + log.Fatal("Failed to resynchronize with the perms.", zap.Error(err)) + } go func() { - log.Info("Start the working publishing events.") + log.Debug("Start the working publishing events.") i.runPublishingEvents(i.stopCh) }() go func() { - log.Info("Start the worker canceling inactive deployments.") + log.Debug("Start the worker canceling inactive deployments.") i.runClosingInactiveDeployment(i.stopCh) }() go func() { - log.Info("Start the worker for the auto unlock.") + log.Debug("Start the worker for the auto unlock.") i.runAutoUnlock(i.stopCh) }() - - return i -} - -func (c *InteractorConfig) BuildWebhookURL() string { - if c.ServerProxyProto != "" && c.ServerProxyHost != "" { - return fmt.Sprintf("%s://%s/hooks", c.ServerProxyProto, c.ServerProxyHost) - } - - return fmt.Sprintf("%s://%s/hooks", c.ServerProto, c.ServerHost) -} - -func (c *InteractorConfig) CheckWebhookSSL() bool { - if c.ServerProxyProto != "" && c.ServerProxyHost != "" { - return c.ServerProxyProto == "https" - } - - return c.ServerProto == "https" } diff --git a/internal/interactor/interface.go b/internal/interactor/interface.go index 8c2469c4..4b187f12 100644 --- a/internal/interactor/interface.go +++ b/internal/interactor/interface.go @@ -4,7 +4,6 @@ package interactor import ( "context" - "time" "github.com/gitploy-io/gitploy/model/ent" "github.com/gitploy-io/gitploy/model/extent" @@ -24,22 +23,6 @@ type ( EventStore } - // PermStore defines operations for working with perms. - PermStore interface { - ListPermsOfRepo(ctx context.Context, r *ent.Repo, opt *ListPermsOfRepoOptions) ([]*ent.Perm, error) - FindPermOfRepo(ctx context.Context, r *ent.Repo, u *ent.User) (*ent.Perm, error) - CreatePerm(ctx context.Context, p *ent.Perm) (*ent.Perm, error) - UpdatePerm(ctx context.Context, p *ent.Perm) (*ent.Perm, error) - DeletePermsOfUserLessThanSyncedAt(ctx context.Context, u *ent.User, t time.Time) (int, error) - } - - ListPermsOfRepoOptions struct { - ListOptions - - // Query search the 'login' contains the query. - Query string - } - // PermStore defines operations for working with deployment_statuses. DeploymentStatusStore interface { ListDeploymentStatuses(ctx context.Context, d *ent.Deployment) ([]*ent.DeploymentStatus, error) diff --git a/internal/interactor/mock/pkg.go b/internal/interactor/mock/pkg.go index 2e7ad932..ddef757b 100644 --- a/internal/interactor/mock/pkg.go +++ b/internal/interactor/mock/pkg.go @@ -305,6 +305,20 @@ func (mr *MockStoreMockRecorder) DeleteLock(ctx, l interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLock", reflect.TypeOf((*MockStore)(nil).DeleteLock), ctx, l) } +// DeletePerm mocks base method. +func (m *MockStore) DeletePerm(ctx context.Context, p *ent.Perm) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeletePerm", ctx, p) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeletePerm indicates an expected call of DeletePerm. +func (mr *MockStoreMockRecorder) DeletePerm(ctx, p interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePerm", reflect.TypeOf((*MockStore)(nil).DeletePerm), ctx, p) +} + // DeletePermsOfUserLessThanSyncedAt mocks base method. func (m *MockStore) DeletePermsOfUserLessThanSyncedAt(ctx context.Context, u *ent.User, t time.Time) (int, error) { m.ctrl.T.Helper() @@ -769,6 +783,21 @@ func (mr *MockStoreMockRecorder) ListLocksOfRepo(ctx, r interface{}) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLocksOfRepo", reflect.TypeOf((*MockStore)(nil).ListLocksOfRepo), ctx, r) } +// ListPerms mocks base method. +func (m *MockStore) ListPerms(ctx context.Context, opt *interactor.ListOptions) ([]*ent.Perm, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListPerms", ctx, opt) + ret0, _ := ret[0].([]*ent.Perm) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListPerms indicates an expected call of ListPerms. +func (mr *MockStoreMockRecorder) ListPerms(ctx, opt interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPerms", reflect.TypeOf((*MockStore)(nil).ListPerms), ctx, opt) +} + // ListPermsOfRepo mocks base method. func (m *MockStore) ListPermsOfRepo(ctx context.Context, r *ent.Repo, opt *interactor.ListPermsOfRepoOptions) ([]*ent.Perm, error) { m.ctrl.T.Helper() @@ -994,104 +1023,6 @@ func (mr *MockStoreMockRecorder) UpdateUser(ctx, u interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockStore)(nil).UpdateUser), ctx, u) } -// MockPermStore is a mock of PermStore interface. -type MockPermStore struct { - ctrl *gomock.Controller - recorder *MockPermStoreMockRecorder -} - -// MockPermStoreMockRecorder is the mock recorder for MockPermStore. -type MockPermStoreMockRecorder struct { - mock *MockPermStore -} - -// NewMockPermStore creates a new mock instance. -func NewMockPermStore(ctrl *gomock.Controller) *MockPermStore { - mock := &MockPermStore{ctrl: ctrl} - mock.recorder = &MockPermStoreMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockPermStore) EXPECT() *MockPermStoreMockRecorder { - return m.recorder -} - -// CreatePerm mocks base method. -func (m *MockPermStore) CreatePerm(ctx context.Context, p *ent.Perm) (*ent.Perm, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreatePerm", ctx, p) - ret0, _ := ret[0].(*ent.Perm) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreatePerm indicates an expected call of CreatePerm. -func (mr *MockPermStoreMockRecorder) CreatePerm(ctx, p interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePerm", reflect.TypeOf((*MockPermStore)(nil).CreatePerm), ctx, p) -} - -// DeletePermsOfUserLessThanSyncedAt mocks base method. -func (m *MockPermStore) DeletePermsOfUserLessThanSyncedAt(ctx context.Context, u *ent.User, t time.Time) (int, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeletePermsOfUserLessThanSyncedAt", ctx, u, t) - ret0, _ := ret[0].(int) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// DeletePermsOfUserLessThanSyncedAt indicates an expected call of DeletePermsOfUserLessThanSyncedAt. -func (mr *MockPermStoreMockRecorder) DeletePermsOfUserLessThanSyncedAt(ctx, u, t interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePermsOfUserLessThanSyncedAt", reflect.TypeOf((*MockPermStore)(nil).DeletePermsOfUserLessThanSyncedAt), ctx, u, t) -} - -// FindPermOfRepo mocks base method. -func (m *MockPermStore) FindPermOfRepo(ctx context.Context, r *ent.Repo, u *ent.User) (*ent.Perm, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindPermOfRepo", ctx, r, u) - ret0, _ := ret[0].(*ent.Perm) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindPermOfRepo indicates an expected call of FindPermOfRepo. -func (mr *MockPermStoreMockRecorder) FindPermOfRepo(ctx, r, u interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindPermOfRepo", reflect.TypeOf((*MockPermStore)(nil).FindPermOfRepo), ctx, r, u) -} - -// ListPermsOfRepo mocks base method. -func (m *MockPermStore) ListPermsOfRepo(ctx context.Context, r *ent.Repo, opt *interactor.ListPermsOfRepoOptions) ([]*ent.Perm, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListPermsOfRepo", ctx, r, opt) - ret0, _ := ret[0].([]*ent.Perm) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListPermsOfRepo indicates an expected call of ListPermsOfRepo. -func (mr *MockPermStoreMockRecorder) ListPermsOfRepo(ctx, r, opt interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPermsOfRepo", reflect.TypeOf((*MockPermStore)(nil).ListPermsOfRepo), ctx, r, opt) -} - -// UpdatePerm mocks base method. -func (m *MockPermStore) UpdatePerm(ctx context.Context, p *ent.Perm) (*ent.Perm, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdatePerm", ctx, p) - ret0, _ := ret[0].(*ent.Perm) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpdatePerm indicates an expected call of UpdatePerm. -func (mr *MockPermStoreMockRecorder) UpdatePerm(ctx, p interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePerm", reflect.TypeOf((*MockPermStore)(nil).UpdatePerm), ctx, p) -} - // MockDeploymentStatusStore is a mock of DeploymentStatusStore interface. type MockDeploymentStatusStore struct { ctrl *gomock.Controller diff --git a/internal/interactor/perm.go b/internal/interactor/perm.go new file mode 100644 index 00000000..ad2ec36d --- /dev/null +++ b/internal/interactor/perm.go @@ -0,0 +1,89 @@ +package interactor + +import ( + "context" + "time" + + "github.com/gitploy-io/gitploy/model/ent" + "go.uber.org/zap" +) + +// PermStore defines operations for working with perms. +type PermStore interface { + ListPerms(ctx context.Context, opt *ListOptions) ([]*ent.Perm, error) + ListPermsOfRepo(ctx context.Context, r *ent.Repo, opt *ListPermsOfRepoOptions) ([]*ent.Perm, error) + FindPermOfRepo(ctx context.Context, r *ent.Repo, u *ent.User) (*ent.Perm, error) + CreatePerm(ctx context.Context, p *ent.Perm) (*ent.Perm, error) + UpdatePerm(ctx context.Context, p *ent.Perm) (*ent.Perm, error) + DeletePermsOfUserLessThanSyncedAt(ctx context.Context, u *ent.User, t time.Time) (int, error) + DeletePerm(ctx context.Context, p *ent.Perm) error +} + +type ListPermsOfRepoOptions struct { + ListOptions + + // Query search the 'login' contains the query. + Query string +} + +type PermInteractor struct { + *service + + orgEntries []string +} + +// ResyncPerms delete all permissions not included in the organization entries. +func (i *PermInteractor) ResyncPerms(ctx context.Context) error { + if i.orgEntries == nil { + i.log.Debug("Skip to resync perms, the organization entries is emtpy.") + return nil + } + + const perPage = 100 + + page := 1 + for { + perms, err := i.store.ListPerms(ctx, &ListOptions{ + Page: page, + PerPage: perPage, + }) + if err != nil { + return err + } + + for _, p := range perms { + if p.Edges.Repo == nil { + i.log.Warn("Failed to eager loading for the perm.", zap.Int("perm_id", p.ID)) + continue + } + + if i.matchOrg(p.Edges.Repo.Namespace) { + continue + } + + i.log.Debug("Delete the perm.", zap.String("repo_fullname", p.Edges.Repo.GetFullName())) + if err := i.store.DeletePerm(ctx, p); err != nil { + i.log.Error("Failed to delete the perm.", zap.Error(err)) + } + } + + // Stop the loop if it is the last page. + if len(perms) < perPage { + break + } + + page += 1 + } + + return nil +} + +func (i *PermInteractor) matchOrg(namespace string) bool { + for _, org := range i.orgEntries { + if namespace == org { + return true + } + } + + return false +} diff --git a/internal/interactor/perm_test.go b/internal/interactor/perm_test.go new file mode 100644 index 00000000..c1e41776 --- /dev/null +++ b/internal/interactor/perm_test.go @@ -0,0 +1,74 @@ +package interactor_test + +import ( + "context" + "fmt" + "testing" + + "github.com/golang/mock/gomock" + + i "github.com/gitploy-io/gitploy/internal/interactor" + "github.com/gitploy-io/gitploy/internal/interactor/mock" + "github.com/gitploy-io/gitploy/model/ent" +) + +type PermMatcher struct { + perm *ent.Perm +} + +func newPermMatcher(p *ent.Perm) *PermMatcher { + return &PermMatcher{ + perm: p, + } +} + +func (m *PermMatcher) Matches(x interface{}) bool { + px, _ := x.(*ent.Perm) + return px.ID == m.perm.ID +} + +func (m *PermMatcher) String() string { + return fmt.Sprintf("has same id as %d", m.perm.ID) +} + +func TestPermInteractor_ResyncPerms(t *testing.T) { + t.Run("Delete the perms.", func(t *testing.T) { + ctrl := gomock.NewController(t) + store := mock.NewMockStore(ctrl) + + t.Log("Get the perms of gitploy-io organization and non-gitploy-io organization.") + store.EXPECT(). + ListPerms(gomock.Any(), gomock.AssignableToTypeOf(&i.ListOptions{})). + Return([]*ent.Perm{ + { + ID: 1, + Edges: ent.PermEdges{ + Repo: &ent.Repo{ + Namespace: "gitploy-io", + Name: "gitploy", + }, + }, + }, + { + ID: 2, + Edges: ent.PermEdges{ + Repo: &ent.Repo{ + Namespace: "non-gitploy-io", + Name: "gitploy", + }, + }, + }, + }, nil) + + store.EXPECT(). + DeletePerm(gomock.Any(), newPermMatcher(&ent.Perm{ID: 2})) + + intr := i.NewInteractor(&i.InteractorConfig{ + Store: store, + OrgEntries: []string{"gitploy-io"}, + }) + if err := intr.ResyncPerms(context.Background()); err != nil { + t.Fatalf("ResyncPerms returns an error: %s", err) + } + }) +} diff --git a/internal/pkg/store/perm.go b/internal/pkg/store/perm.go index 2dca22ea..a0ae530a 100644 --- a/internal/pkg/store/perm.go +++ b/internal/pkg/store/perm.go @@ -13,6 +13,15 @@ import ( "github.com/gitploy-io/gitploy/pkg/e" ) +func (s *Store) ListPerms(ctx context.Context, opt *i.ListOptions) ([]*ent.Perm, error) { + return s.c.Perm.Query(). + Limit(opt.PerPage). + Offset(offset(opt.Page, opt.PerPage)). + WithRepo(). + WithUser(). + All(ctx) +} + func (s *Store) ListPermsOfRepo(ctx context.Context, r *ent.Repo, opt *i.ListPermsOfRepoOptions) ([]*ent.Perm, error) { perms, err := s.c.Perm. Query(). @@ -122,3 +131,7 @@ func (s *Store) DeletePermsOfUserLessThanSyncedAt(ctx context.Context, u *ent.Us return cnt, nil } + +func (s *Store) DeletePerm(ctx context.Context, p *ent.Perm) error { + return s.c.Perm.DeleteOne(p).Exec(ctx) +}