From 6f054a7c7d859f41985ed9cbc475cd6fddcb809e Mon Sep 17 00:00:00 2001 From: akiyatomohiro <83905169+akiyatomohiro@users.noreply.github.com> Date: Thu, 27 Feb 2025 17:36:58 +0700 Subject: [PATCH] feat: add migration to AddUserMember interactor [FLOW-BE-34] (#74) * add role and permittable domain * add role and permittable mongo * add role and permittable mongo * add role and permittable accountrepo * add role and permittable accountrepo * add migration to AddUserMember interactor * add role and permittable accountmemory * add TestWorkspace_AddMember_Migration * add comment * fix comment * fix Filtered * fix Filtered * Revert "fix Filtered" This reverts commit 2063494762f3ea31b161b424907c5927c8ad6cf0. * Revert "fix Filtered" This reverts commit 2425d8956305312a6196ec90ffe73b243792b9dc. --- account/accountdomain/id.go | 35 +++ account/accountdomain/permittable/builder.go | 54 ++++ account/accountdomain/permittable/id.go | 17 ++ account/accountdomain/permittable/list.go | 5 + .../accountdomain/permittable/permittable.go | 43 +++ account/accountdomain/role/builder.go | 44 +++ account/accountdomain/role/id.go | 19 ++ account/accountdomain/role/list.go | 5 + account/accountdomain/role/role.go | 35 +++ .../accountmemory/container.go | 2 + .../accountmemory/permittable.go | 93 +++++++ .../accountmemory/role.go | 83 ++++++ .../accountmongo/mongodoc/permittable.go | 64 +++++ .../accountmongo/mongodoc/role.go | 44 +++ .../accountmongo/permittable.go | 80 ++++++ .../accountmongo/role.go | 85 ++++++ .../accountinteractor/workspace.go | 102 +++++++ .../accountinteractor/workspace_test.go | 260 +++++++++++++++++- .../accountusecase/accountrepo/container.go | 2 + .../accountusecase/accountrepo/permittable.go | 18 ++ account/accountusecase/accountrepo/role.go | 18 ++ 21 files changed, 1096 insertions(+), 12 deletions(-) create mode 100644 account/accountdomain/permittable/builder.go create mode 100644 account/accountdomain/permittable/id.go create mode 100644 account/accountdomain/permittable/list.go create mode 100644 account/accountdomain/permittable/permittable.go create mode 100644 account/accountdomain/role/builder.go create mode 100644 account/accountdomain/role/id.go create mode 100644 account/accountdomain/role/list.go create mode 100644 account/accountdomain/role/role.go create mode 100644 account/accountinfrastructure/accountmemory/permittable.go create mode 100644 account/accountinfrastructure/accountmemory/role.go create mode 100644 account/accountinfrastructure/accountmongo/mongodoc/permittable.go create mode 100644 account/accountinfrastructure/accountmongo/mongodoc/role.go create mode 100644 account/accountinfrastructure/accountmongo/permittable.go create mode 100644 account/accountinfrastructure/accountmongo/role.go create mode 100644 account/accountusecase/accountrepo/permittable.go create mode 100644 account/accountusecase/accountrepo/role.go diff --git a/account/accountdomain/id.go b/account/accountdomain/id.go index 289a984f..e36c7296 100644 --- a/account/accountdomain/id.go +++ b/account/accountdomain/id.go @@ -35,3 +35,38 @@ var IntegrationIDFrom = idx.From[Integration] var IntegrationIDFromRef = idx.FromRef[Integration] var ErrInvalidID = idx.ErrInvalidID + +// TODO: Delete the below once the permission check migration is complete. + +type Role struct{} +type Permittable struct{} + +func (Role) Type() string { return "role" } +func (Permittable) Type() string { return "permittable" } + +type RoleID = idx.ID[Role] +type PermittableID = idx.ID[Permittable] + +var NewRoleID = idx.New[Role] +var NewPermittableID = idx.New[Permittable] + +var MustRoleID = idx.Must[Role] +var MustPermittableID = idx.Must[Permittable] + +var RoleIDFrom = idx.From[Role] +var PermittableIDFrom = idx.From[Permittable] + +var RoleIDFromRef = idx.FromRef[Role] +var PermittableIDFromRef = idx.FromRef[Permittable] + +type RoleIDList = idx.List[Role] +type PermittableIDList = idx.List[Permittable] + +var RoleIDListFrom = idx.ListFrom[Role] +var PermittableIDListFrom = idx.ListFrom[Permittable] + +type RoleIDSet = idx.Set[Role] +type PermittableIDSet = idx.Set[Permittable] + +var NewRoleIDSet = idx.NewSet[Role] +var NewPermittableIDSet = idx.NewSet[Permittable] diff --git a/account/accountdomain/permittable/builder.go b/account/accountdomain/permittable/builder.go new file mode 100644 index 00000000..53213166 --- /dev/null +++ b/account/accountdomain/permittable/builder.go @@ -0,0 +1,54 @@ +// TODO: Delete this file once the permission check migration is complete. + +package permittable + +import ( + "github.com/reearth/reearthx/account/accountdomain" + "github.com/reearth/reearthx/account/accountdomain/user" +) + +type Builder struct { + p *Permittable +} + +func New() *Builder { + return &Builder{p: &Permittable{}} +} + +func (b *Builder) Build() (*Permittable, error) { + if b.p.id.IsNil() { + return nil, ErrInvalidID + } + if b.p.userID.IsNil() { + return nil, ErrInvalidID + } + return b.p, nil +} + +func (b *Builder) MustBuild() *Permittable { + u, err := b.Build() + if err != nil { + panic(err) + } + return u +} + +func (b *Builder) ID(id ID) *Builder { + b.p.id = id + return b +} + +func (b *Builder) NewID() *Builder { + b.p.id = NewID() + return b +} + +func (b *Builder) UserID(userID user.ID) *Builder { + b.p.userID = userID + return b +} + +func (b *Builder) RoleIDs(roleIDs []accountdomain.RoleID) *Builder { + b.p.roleIDs = roleIDs + return b +} diff --git a/account/accountdomain/permittable/id.go b/account/accountdomain/permittable/id.go new file mode 100644 index 00000000..7e76580b --- /dev/null +++ b/account/accountdomain/permittable/id.go @@ -0,0 +1,17 @@ +// TODO: Delete this file once the permission check migration is complete. + +package permittable + +import "github.com/reearth/reearthx/account/accountdomain" + +type ID = accountdomain.PermittableID + +var NewID = accountdomain.NewPermittableID + +var MustID = accountdomain.MustPermittableID + +var IDFrom = accountdomain.PermittableIDFrom + +var IDFromRef = accountdomain.PermittableIDFromRef + +var ErrInvalidID = accountdomain.ErrInvalidID diff --git a/account/accountdomain/permittable/list.go b/account/accountdomain/permittable/list.go new file mode 100644 index 00000000..6bbbd050 --- /dev/null +++ b/account/accountdomain/permittable/list.go @@ -0,0 +1,5 @@ +// TODO: Delete this file once the permission check migration is complete. + +package permittable + +type List []*Permittable diff --git a/account/accountdomain/permittable/permittable.go b/account/accountdomain/permittable/permittable.go new file mode 100644 index 00000000..bc568d3e --- /dev/null +++ b/account/accountdomain/permittable/permittable.go @@ -0,0 +1,43 @@ +// TODO: Delete this file once the permission check migration is complete. + +package permittable + +import ( + "github.com/reearth/reearthx/account/accountdomain" + "github.com/reearth/reearthx/account/accountdomain/role" + "github.com/reearth/reearthx/account/accountdomain/user" +) + +type Permittable struct { + id ID + userID user.ID + roleIDs []role.ID +} + +func (p *Permittable) ID() ID { + if p == nil { + return ID{} + } + return p.id +} + +func (p *Permittable) UserID() user.ID { + if p == nil { + return user.ID{} + } + return p.userID +} + +func (p *Permittable) RoleIDs() []accountdomain.RoleID { + if p == nil { + return nil + } + return p.roleIDs +} + +func (p *Permittable) EditRoleIDs(roleIDs accountdomain.RoleIDList) { + if p == nil { + return + } + p.roleIDs = roleIDs +} diff --git a/account/accountdomain/role/builder.go b/account/accountdomain/role/builder.go new file mode 100644 index 00000000..2d29fc99 --- /dev/null +++ b/account/accountdomain/role/builder.go @@ -0,0 +1,44 @@ +// TODO: Delete this file once the permission check migration is complete. + +package role + +type Builder struct { + r *Role +} + +func New() *Builder { + return &Builder{r: &Role{}} +} + +func (b *Builder) Build() (*Role, error) { + if b.r.id.IsNil() { + return nil, ErrInvalidID + } + if b.r.name == "" { + return nil, ErrEmptyName + } + return b.r, nil +} + +func (b *Builder) MustBuild() *Role { + g, err := b.Build() + if err != nil { + panic(err) + } + return g +} + +func (b *Builder) ID(id ID) *Builder { + b.r.id = id + return b +} + +func (b *Builder) NewID() *Builder { + b.r.id = NewID() + return b +} + +func (b *Builder) Name(name string) *Builder { + b.r.name = name + return b +} diff --git a/account/accountdomain/role/id.go b/account/accountdomain/role/id.go new file mode 100644 index 00000000..bd968b09 --- /dev/null +++ b/account/accountdomain/role/id.go @@ -0,0 +1,19 @@ +// TODO: Delete this file once the permission check migration is complete. + +package role + +import ( + "github.com/reearth/reearthx/account/accountdomain" +) + +type ID = accountdomain.RoleID + +var NewID = accountdomain.NewRoleID + +var MustID = accountdomain.MustRoleID + +var IDFrom = accountdomain.RoleIDFrom + +var IDFromRef = accountdomain.RoleIDFromRef + +var ErrInvalidID = accountdomain.ErrInvalidID diff --git a/account/accountdomain/role/list.go b/account/accountdomain/role/list.go new file mode 100644 index 00000000..af0b550b --- /dev/null +++ b/account/accountdomain/role/list.go @@ -0,0 +1,5 @@ +// TODO: Delete this file once the permission check migration is complete. + +package role + +type List []*Role diff --git a/account/accountdomain/role/role.go b/account/accountdomain/role/role.go new file mode 100644 index 00000000..2db43bfe --- /dev/null +++ b/account/accountdomain/role/role.go @@ -0,0 +1,35 @@ +// TODO: Delete this file once the permission check migration is complete. + +package role + +import "errors" + +var ( + ErrEmptyName = errors.New("role name can't be empty") +) + +type Role struct { + id ID + name string +} + +func (r *Role) ID() ID { + if r == nil { + return ID{} + } + return r.id +} + +func (r *Role) Name() string { + if r == nil { + return "" + } + return r.name +} + +func (r *Role) Rename(name string) { + if r == nil { + return + } + r.name = name +} diff --git a/account/accountinfrastructure/accountmemory/container.go b/account/accountinfrastructure/accountmemory/container.go index 965e3b27..7f68b9b4 100644 --- a/account/accountinfrastructure/accountmemory/container.go +++ b/account/accountinfrastructure/accountmemory/container.go @@ -9,6 +9,8 @@ func New() *accountrepo.Container { return &accountrepo.Container{ User: NewUser(), Workspace: NewWorkspace(), + Role: NewRole(), // TODO: Delete this once the permission check migration is complete. + Permittable: NewPermittable(), // TODO: Delete this once the permission check migration is complete. Transaction: &usecasex.NopTransaction{}, } } diff --git a/account/accountinfrastructure/accountmemory/permittable.go b/account/accountinfrastructure/accountmemory/permittable.go new file mode 100644 index 00000000..b479344c --- /dev/null +++ b/account/accountinfrastructure/accountmemory/permittable.go @@ -0,0 +1,93 @@ +// TODO: Delete this file once the permission check migration is complete. + +package accountmemory + +import ( + "context" + "slices" + "sync" + + "github.com/reearth/reearthx/account/accountdomain" + "github.com/reearth/reearthx/account/accountdomain/permittable" + "github.com/reearth/reearthx/account/accountdomain/user" + "github.com/reearth/reearthx/rerror" +) + +type Permittable struct { + lock sync.Mutex + data map[accountdomain.PermittableID]*permittable.Permittable +} + +func NewPermittable() *Permittable { + return &Permittable{ + data: map[accountdomain.PermittableID]*permittable.Permittable{}, + } +} + +func NewPermittableWith(items ...*permittable.Permittable) *Permittable { + p := NewPermittable() + ctx := context.Background() + for _, i := range items { + _ = p.Save(ctx, *i) + } + return p +} + +func (p *Permittable) FindByUserID(ctx context.Context, userID user.ID) (*permittable.Permittable, error) { + p.lock.Lock() + defer p.lock.Unlock() + + for _, perm := range p.data { + if perm.UserID() == userID { + return perm, nil + } + } + return nil, rerror.ErrNotFound +} + +func (p *Permittable) FindByUserIDs(ctx context.Context, userIDs user.IDList) (permittable.List, error) { + p.lock.Lock() + defer p.lock.Unlock() + + results := make(permittable.List, 0, len(userIDs)) + for _, userID := range userIDs { + for _, perm := range p.data { + if perm.UserID() == userID { + results = append(results, perm) + break + } + } + } + + if len(results) == 0 { + return nil, rerror.ErrNotFound + } + + return results, nil +} + +func (p *Permittable) FindByRoleID(ctx context.Context, roleID accountdomain.RoleID) (permittable.List, error) { + p.lock.Lock() + defer p.lock.Unlock() + + results := make(permittable.List, 0, len(p.data)) + for _, perm := range p.data { + if slices.Contains(perm.RoleIDs(), roleID) { + results = append(results, perm) + } + } + + if len(results) == 0 { + return nil, rerror.ErrNotFound + } + + return results, nil +} + +func (r *Permittable) Save(ctx context.Context, p permittable.Permittable) error { + r.lock.Lock() + defer r.lock.Unlock() + + r.data[p.ID()] = &p + return nil +} diff --git a/account/accountinfrastructure/accountmemory/role.go b/account/accountinfrastructure/accountmemory/role.go new file mode 100644 index 00000000..f0f9bae0 --- /dev/null +++ b/account/accountinfrastructure/accountmemory/role.go @@ -0,0 +1,83 @@ +// TODO: Delete this file once the permission check migration is complete. + +package accountmemory + +import ( + "context" + "sync" + + "github.com/reearth/reearthx/account/accountdomain" + "github.com/reearth/reearthx/account/accountdomain/role" + "github.com/reearth/reearthx/rerror" +) + +type Role struct { + lock sync.Mutex + data map[accountdomain.RoleID]*role.Role +} + +func NewRole() *Role { + return &Role{ + data: map[accountdomain.RoleID]*role.Role{}, + } +} + +func NewRoleWith(items ...*role.Role) *Role { + r := NewRole() + ctx := context.Background() + for _, i := range items { + _ = r.Save(ctx, *i) + } + return r +} + +func (r *Role) FindAll(ctx context.Context) (role.List, error) { + r.lock.Lock() + defer r.lock.Unlock() + + res := make(role.List, 0, len(r.data)) + for _, v := range r.data { + res = append(res, v) + } + return res, nil +} + +func (r *Role) FindByID(ctx context.Context, id accountdomain.RoleID) (*role.Role, error) { + r.lock.Lock() + defer r.lock.Unlock() + + res, ok := r.data[id] + if ok { + return res, nil + } + return nil, rerror.ErrNotFound +} + +func (r *Role) FindByIDs(ctx context.Context, ids accountdomain.RoleIDList) (role.List, error) { + r.lock.Lock() + defer r.lock.Unlock() + + res := make(role.List, 0, len(ids)) + for _, id := range ids { + if v, ok := r.data[id]; ok { + res = append(res, v) + } + } + return res, nil +} + +func (r *Role) Save(ctx context.Context, rl role.Role) error { + r.lock.Lock() + defer r.lock.Unlock() + + r.data[rl.ID()] = &rl + return nil +} + +func (r *Role) Remove(ctx context.Context, id accountdomain.RoleID) error { + r.lock.Lock() + defer r.lock.Unlock() + + delete(r.data, id) + return nil +} diff --git a/account/accountinfrastructure/accountmongo/mongodoc/permittable.go b/account/accountinfrastructure/accountmongo/mongodoc/permittable.go new file mode 100644 index 00000000..18fd3711 --- /dev/null +++ b/account/accountinfrastructure/accountmongo/mongodoc/permittable.go @@ -0,0 +1,64 @@ +// TODO: Delete this file once the permission check migration is complete. + +package mongodoc + +import ( + "github.com/reearth/reearthx/account/accountdomain" + "github.com/reearth/reearthx/account/accountdomain/permittable" + "github.com/reearth/reearthx/account/accountdomain/user" + "github.com/reearth/reearthx/mongox" +) + +type PermittableDocument struct { + ID string + UserID string + RoleIDs []string +} + +type PermittableConsumer = mongox.SliceFuncConsumer[*PermittableDocument, *permittable.Permittable] + +func NewPermittableConsumer() *PermittableConsumer { + return NewConsumer[*PermittableDocument, *permittable.Permittable]() +} + +func NewPermittable(p permittable.Permittable) (*PermittableDocument, string) { + id := p.ID().String() + + roleIds := make([]string, 0, len(p.RoleIDs())) + for _, r := range p.RoleIDs() { + roleIds = append(roleIds, r.String()) + } + + return &PermittableDocument{ + ID: id, + UserID: p.UserID().String(), + RoleIDs: roleIds, + }, id +} + +func (d *PermittableDocument) Model() (*permittable.Permittable, error) { + if d == nil { + return nil, nil + } + + uid, err := accountdomain.PermittableIDFrom(d.ID) + if err != nil { + return nil, err + } + + userId, err := user.IDFrom(d.UserID) + if err != nil { + return nil, err + } + + roleIds, err := accountdomain.RoleIDListFrom(d.RoleIDs) + if err != nil { + return nil, err + } + + return permittable.New(). + ID(uid). + UserID(userId). + RoleIDs(roleIds). + Build() +} diff --git a/account/accountinfrastructure/accountmongo/mongodoc/role.go b/account/accountinfrastructure/accountmongo/mongodoc/role.go new file mode 100644 index 00000000..dd072d3c --- /dev/null +++ b/account/accountinfrastructure/accountmongo/mongodoc/role.go @@ -0,0 +1,44 @@ +// TODO: Delete this file once the permission check migration is complete. + +package mongodoc + +import ( + "github.com/reearth/reearthx/account/accountdomain" + "github.com/reearth/reearthx/account/accountdomain/role" + "github.com/reearth/reearthx/mongox" +) + +type RoleDocument struct { + ID string + Name string +} + +type RoleConsumer = mongox.SliceFuncConsumer[*RoleDocument, *role.Role] + +func NewRoleConsumer() *RoleConsumer { + return NewConsumer[*RoleDocument, *role.Role]() +} + +func NewRole(g role.Role) (*RoleDocument, string) { + id := g.ID().String() + return &RoleDocument{ + ID: id, + Name: g.Name(), + }, id +} + +func (d *RoleDocument) Model() (*role.Role, error) { + if d == nil { + return nil, nil + } + + rid, err := accountdomain.RoleIDFrom(d.ID) + if err != nil { + return nil, err + } + + return role.New(). + ID(rid). + Name(d.Name). + Build() +} diff --git a/account/accountinfrastructure/accountmongo/permittable.go b/account/accountinfrastructure/accountmongo/permittable.go new file mode 100644 index 00000000..9760ff43 --- /dev/null +++ b/account/accountinfrastructure/accountmongo/permittable.go @@ -0,0 +1,80 @@ +// TODO: Delete this file once the permission check migration is complete. + +package accountmongo + +import ( + "context" + + "github.com/reearth/reearthx/account/accountdomain" + "github.com/reearth/reearthx/account/accountdomain/permittable" + "github.com/reearth/reearthx/account/accountdomain/user" + "github.com/reearth/reearthx/account/accountinfrastructure/accountmongo/mongodoc" + "github.com/reearth/reearthx/account/accountusecase/accountrepo" + "github.com/reearth/reearthx/mongox" + "github.com/reearth/reearthx/rerror" + "go.mongodb.org/mongo-driver/bson" +) + +var ( + newPermittableIndexes = []string{} + newPermittableUniqueIndexes = []string{"id", "userid"} +) + +type Permittable struct { + client *mongox.Collection +} + +func NewPermittable(client *mongox.Client) accountrepo.Permittable { + return &Permittable{ + client: client.WithCollection("permittable"), + } +} + +func (r *Permittable) Init(ctx context.Context) error { + return createIndexes(ctx, r.client, newPermittableIndexes, newPermittableUniqueIndexes) +} + +func (r *Permittable) FindByUserID(ctx context.Context, id user.ID) (*permittable.Permittable, error) { + return r.findOne(ctx, bson.M{ + "userid": id.String(), + }) +} + +func (r *Permittable) FindByUserIDs(ctx context.Context, ids user.IDList) (permittable.List, error) { + return r.find(ctx, bson.M{ + "userid": bson.M{"$in": ids.Strings()}, + }) +} + +func (r *Permittable) FindByRoleID(ctx context.Context, roleId accountdomain.RoleID) (permittable.List, error) { + return r.find(ctx, bson.M{ + "roleids": bson.M{"$in": []string{roleId.String()}}, + }) +} + +func (r *Permittable) Save(ctx context.Context, permittable permittable.Permittable) error { + doc, gId := mongodoc.NewPermittable(permittable) + return r.client.SaveOne(ctx, gId, doc) +} + +func (r *Permittable) find(ctx context.Context, filter any) (permittable.List, error) { + c := mongodoc.NewPermittableConsumer() + if err := r.client.Find(ctx, filter, c); err != nil { + return nil, err + } + if len(c.Result) == 0 { + return nil, rerror.ErrNotFound + } + return (permittable.List)(c.Result), nil +} + +func (r *Permittable) findOne(ctx context.Context, filter any) (*permittable.Permittable, error) { + c := mongodoc.NewPermittableConsumer() + if err := r.client.FindOne(ctx, filter, c); err != nil { + return nil, err + } + if len(c.Result) == 0 { + return nil, rerror.ErrNotFound + } + return c.Result[0], nil +} diff --git a/account/accountinfrastructure/accountmongo/role.go b/account/accountinfrastructure/accountmongo/role.go new file mode 100644 index 00000000..f581eee8 --- /dev/null +++ b/account/accountinfrastructure/accountmongo/role.go @@ -0,0 +1,85 @@ +// TODO: Delete this file once the permission check migration is complete. + +package accountmongo + +import ( + "context" + + "github.com/reearth/reearthx/account/accountdomain" + "github.com/reearth/reearthx/account/accountdomain/role" + "github.com/reearth/reearthx/account/accountinfrastructure/accountmongo/mongodoc" + "github.com/reearth/reearthx/account/accountusecase/accountrepo" + "github.com/reearth/reearthx/mongox" + "go.mongodb.org/mongo-driver/bson" +) + +var ( + roleIndexes = []string{} + roleUniqueIndexes = []string{"id", "name"} +) + +type Role struct { + client *mongox.Collection +} + +func NewRole(client *mongox.Client) accountrepo.Role { + return &Role{ + client: client.WithCollection("role"), + } +} + +func (r *Role) Init(ctx context.Context) error { + return createIndexes(ctx, r.client, roleIndexes, roleUniqueIndexes) +} + +func (r *Role) FindAll(ctx context.Context) (role.List, error) { + filter := bson.M{} + return r.find(ctx, filter) +} + +func (r *Role) FindByID(ctx context.Context, id accountdomain.RoleID) (*role.Role, error) { + return r.findOne(ctx, bson.M{ + "id": id.String(), + }) +} + +func (r *Role) FindByIDs(ctx context.Context, ids accountdomain.RoleIDList) (role.List, error) { + if len(ids) == 0 { + return nil, nil + } + + filter := bson.M{ + "id": bson.M{ + "$in": ids.Strings(), + }, + } + return r.find(ctx, filter) +} + +func (r *Role) Save(ctx context.Context, role role.Role) error { + doc, gId := mongodoc.NewRole(role) + return r.client.SaveOne(ctx, gId, doc) +} + +func (r *Role) Remove(ctx context.Context, id accountdomain.RoleID) error { + return r.client.RemoveOne(ctx, bson.M{"id": id.String()}) +} + +func (r *Role) find(ctx context.Context, filter any) (role.List, error) { + c := mongodoc.NewRoleConsumer() + if err := r.client.Find(ctx, filter, c); err != nil { + return nil, err + } + if len(c.Result) == 0 { + return role.List{}, nil + } + return (role.List)(c.Result), nil +} + +func (r *Role) findOne(ctx context.Context, filter any) (*role.Role, error) { + c := mongodoc.NewRoleConsumer() + if err := r.client.FindOne(ctx, filter, c); err != nil { + return nil, err + } + return c.Result[0], nil +} diff --git a/account/accountusecase/accountinteractor/workspace.go b/account/accountusecase/accountinteractor/workspace.go index 84c31be4..67b1689c 100644 --- a/account/accountusecase/accountinteractor/workspace.go +++ b/account/accountusecase/accountinteractor/workspace.go @@ -2,13 +2,18 @@ package accountinteractor import ( "context" + "fmt" "strings" + "github.com/reearth/reearthx/account/accountdomain" + "github.com/reearth/reearthx/account/accountdomain/permittable" + "github.com/reearth/reearthx/account/accountdomain/role" "github.com/reearth/reearthx/account/accountdomain/user" "github.com/reearth/reearthx/account/accountdomain/workspace" "github.com/reearth/reearthx/account/accountusecase" "github.com/reearth/reearthx/account/accountusecase/accountinterfaces" "github.com/reearth/reearthx/account/accountusecase/accountrepo" + "github.com/reearth/reearthx/log" "github.com/reearth/reearthx/rerror" "github.com/samber/lo" "golang.org/x/exp/maps" @@ -140,11 +145,22 @@ func (i *Workspace) AddUserMember(ctx context.Context, workspaceID workspace.ID, } } + // TODO: Delete this once the permission check migration is complete. + maintainerRole, err := i.getMaintainerRole(ctx) + if err != nil { + return nil, err + } + for _, m := range ul { if m == nil { continue } + // TODO: Delete this once the permission check migration is complete. + if err := i.ensureUserHasMaintainerRole(ctx, m.ID(), maintainerRole.ID()); err != nil { + return nil, err + } + err = ws.Members().Join(m, users[m.ID()], *operator.User) if err != nil { return nil, err @@ -396,3 +412,89 @@ func filterWorkspaces( return workspaces, nil } + +// TODO: Delete this once the permission check migration is complete. +func (i *Workspace) getMaintainerRole(ctx context.Context) (*role.Role, error) { + // check and create maintainer role + roles, err := i.repos.Role.FindAll(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get roles: %w", err) + } + + var maintainerRole *role.Role + for _, r := range roles { + if r.Name() == "maintainer" { + maintainerRole = r + log.Info("Found maintainer role") + break + } + } + + if maintainerRole == nil { + r, err := role.New(). + NewID(). + Name("maintainer"). + Build() + if err != nil { + return nil, fmt.Errorf("failed to create maintainer role domain: %w", err) + } + + err = i.repos.Role.Save(ctx, *r) + if err != nil { + return nil, fmt.Errorf("failed to save maintainer role: %w", err) + } + + maintainerRole = r + log.Info("Created maintainer role") + } + + return maintainerRole, nil +} + +// TODO: Delete this once the permission check migration is complete. +func (i *Workspace) ensureUserHasMaintainerRole(ctx context.Context, userID user.ID, maintainerRoleID accountdomain.RoleID) error { + var p *permittable.Permittable + var err error + + p, err = i.repos.Permittable.FindByUserID(ctx, userID) + if err != nil && err != rerror.ErrNotFound { + return err + } + + if hasRole(p, maintainerRoleID) { + return nil + } + + if p == nil { + p, err = permittable.New(). + NewID(). + UserID(userID). + RoleIDs([]accountdomain.RoleID{maintainerRoleID}). + Build() + if err != nil { + return err + } + } else { + p.EditRoleIDs(append(p.RoleIDs(), maintainerRoleID)) + } + + err = i.repos.Permittable.Save(ctx, *p) + if err != nil { + return err + } + + return nil +} + +// TODO: Delete this once the permission check migration is complete. +func hasRole(p *permittable.Permittable, roleID role.ID) bool { + if p == nil { + return false + } + for _, r := range p.RoleIDs() { + if r == roleID { + return true + } + } + return false +} diff --git a/account/accountusecase/accountinteractor/workspace_test.go b/account/accountusecase/accountinteractor/workspace_test.go index 422c7483..83eac915 100644 --- a/account/accountusecase/accountinteractor/workspace_test.go +++ b/account/accountusecase/accountinteractor/workspace_test.go @@ -6,11 +6,15 @@ import ( "testing" "github.com/reearth/reearthx/account/accountdomain" + "github.com/reearth/reearthx/account/accountdomain/permittable" + "github.com/reearth/reearthx/account/accountdomain/role" "github.com/reearth/reearthx/account/accountdomain/user" "github.com/reearth/reearthx/account/accountdomain/workspace" "github.com/reearth/reearthx/account/accountinfrastructure/accountmemory" "github.com/reearth/reearthx/account/accountusecase" "github.com/reearth/reearthx/account/accountusecase/accountinterfaces" + "github.com/reearth/reearthx/account/accountusecase/accountrepo" + "github.com/reearth/reearthx/idx" "github.com/reearth/reearthx/rerror" "github.com/samber/lo" "github.com/stretchr/testify/assert" @@ -953,21 +957,21 @@ func TestWorkspace_RemoveMultipleMembers(t *testing.T) { id2 := accountdomain.NewWorkspaceID() w2 := workspace.New().ID(id2).Name("W2"). Members(map[user.ID]workspace.Member{ - userID: {Role: workspace.RoleOwner}, + userID: {Role: workspace.RoleOwner}, userID2: {Role: workspace.RoleReader}, }).Personal(true).MustBuild() id3 := accountdomain.NewWorkspaceID() w3 := workspace.New().ID(id3).Name("W3"). Members(map[user.ID]workspace.Member{ - userID: {Role: workspace.RoleOwner}, + userID: {Role: workspace.RoleOwner}, userID2: {Role: workspace.RoleReader}, }).Personal(false).MustBuild() id4 := accountdomain.NewWorkspaceID() w4 := workspace.New().ID(id4).Name("W4"). Members(map[user.ID]workspace.Member{ - userID: {Role: workspace.RoleOwner}, + userID: {Role: workspace.RoleOwner}, userID2: {Role: workspace.RoleReader}, }).Personal(false).MustBuild() @@ -1000,7 +1004,7 @@ func TestWorkspace_RemoveMultipleMembers(t *testing.T) { operator *accountusecase.Operator }{ wId: id1, - uIds: workspace.UserIDList{accountdomain.NewUserID()}, + uIds: workspace.UserIDList{accountdomain.NewUserID()}, operator: op, }, wantErr: workspace.ErrTargetUserNotInTheWorkspace, @@ -1021,7 +1025,7 @@ func TestWorkspace_RemoveMultipleMembers(t *testing.T) { }, wantErr: nil, want: workspace.NewMembersWith(map[user.ID]workspace.Member{ - userID: {Role: workspace.RoleOwner}, + userID: {Role: workspace.RoleOwner}, userID4: {Role: workspace.RoleReader}, }, nil, false), }, @@ -1040,7 +1044,7 @@ func TestWorkspace_RemoveMultipleMembers(t *testing.T) { }, wantErr: accountinterfaces.ErrInvalidOperator, want: workspace.NewMembersWith(map[user.ID]workspace.Member{ - userID: {Role: workspace.RoleOwner}, + userID: {Role: workspace.RoleOwner}, userID4: {Role: workspace.RoleReader}, }, nil, false), }, @@ -1053,8 +1057,8 @@ func TestWorkspace_RemoveMultipleMembers(t *testing.T) { uIds workspace.UserIDList operator *accountusecase.Operator }{ - wId: id1, - uIds: workspace.UserIDList{userID2}, + wId: id1, + uIds: workspace.UserIDList{userID2}, operator: &accountusecase.Operator{ User: &userID3, ReadableWorkspaces: []workspace.ID{id1}, @@ -1062,7 +1066,7 @@ func TestWorkspace_RemoveMultipleMembers(t *testing.T) { }, wantErr: accountinterfaces.ErrOperationDenied, want: workspace.NewMembersWith(map[user.ID]workspace.Member{ - userID: {Role: workspace.RoleOwner}, + userID: {Role: workspace.RoleOwner}, userID3: {Role: workspace.RoleReader}, userID4: {Role: workspace.RoleReader}, }, nil, false), @@ -1082,7 +1086,7 @@ func TestWorkspace_RemoveMultipleMembers(t *testing.T) { }, wantErr: workspace.ErrCannotModifyPersonalWorkspace, want: workspace.NewMembersWith(map[user.ID]workspace.Member{ - userID: {Role: workspace.RoleOwner}, + userID: {Role: workspace.RoleOwner}, userID2: {Role: workspace.RoleReader}, }, nil, false), }, @@ -1101,7 +1105,7 @@ func TestWorkspace_RemoveMultipleMembers(t *testing.T) { }, wantErr: accountinterfaces.ErrOwnerCannotLeaveTheWorkspace, want: workspace.NewMembersWith(map[user.ID]workspace.Member{ - userID: {Role: workspace.RoleOwner}, + userID: {Role: workspace.RoleOwner}, userID2: {Role: workspace.RoleReader}, }, nil, false), }, @@ -1120,7 +1124,7 @@ func TestWorkspace_RemoveMultipleMembers(t *testing.T) { }, wantErr: workspace.ErrNoSpecifiedUsers, want: workspace.NewMembersWith(map[user.ID]workspace.Member{ - userID: {Role: workspace.RoleOwner}, + userID: {Role: workspace.RoleOwner}, userID2: {Role: workspace.RoleReader}, }, nil, false), }, @@ -1316,3 +1320,235 @@ func TestWorkspace_UpdateMember(t *testing.T) { }) } } + +// TODO: Delete this once the permission check migration is complete. +func TestWorkspace_AddMember_Migration(t *testing.T) { + // prepare + ctx := context.Background() + + uId1 := user.NewID() + uId2 := user.NewID() + uId3 := user.NewID() + u1 := user.New().ID(uId1).Name("user1").Email("user1@test.com").MustBuild() + u2 := user.New().ID(uId2).Name("user2").Email("user2@test.com").MustBuild() + u3 := user.New().ID(uId3).Name("user3").Email("user3@test.com").MustBuild() + + roleOwner := workspace.Member{ + Role: workspace.RoleOwner, + InvitedBy: uId1, + } + + wId1 := workspace.NewID() + wId2 := workspace.NewID() + wId3 := workspace.NewID() + wId4 := workspace.NewID() + w1 := workspace.New().ID(wId1). + Name("w1"). + Members(map[idx.ID[accountdomain.User]]workspace.Member{ + uId1: roleOwner, + }). + MustBuild() + w2 := workspace.New().ID(wId2). + Name("w2"). + Members(map[idx.ID[accountdomain.User]]workspace.Member{ + uId1: roleOwner, + }). + MustBuild() + w3 := workspace.New().ID(wId3). + Name("w3"). + Members(map[idx.ID[accountdomain.User]]workspace.Member{ + uId1: roleOwner, + }). + MustBuild() + w4 := workspace.New().ID(wId4). + Name("w4"). + Members(map[idx.ID[accountdomain.User]]workspace.Member{ + uId1: roleOwner, + }). + MustBuild() + + users := map[user.ID]workspace.Role{ + uId2: workspace.RoleOwner, + uId3: workspace.RoleOwner, + } + + op := &accountusecase.Operator{ + User: &uId1, + ReadableWorkspaces: []workspace.ID{wId1, wId2, wId3, wId4}, + OwningWorkspaces: []workspace.ID{wId1, wId2, wId3, wId4}, + } + + tests := []struct { + name string + wId workspace.ID + setup func(ctx context.Context, repos *accountrepo.Container) + assert func(t *testing.T, ctx context.Context, repos *accountrepo.Container) + wantErr bool + }{ + { + name: "should create maintainer role and assign it to workspace users", + wId: wId1, + setup: func(ctx context.Context, repos *accountrepo.Container) { + userRepo := accountrepo.NewMultiUser(accountmemory.NewUserWith(u1, u2, u3)) + workspaceRepo := accountmemory.NewWorkspaceWith(w1) + repos.User = userRepo + repos.Workspace = workspaceRepo + }, + assert: func(t *testing.T, ctx context.Context, repos *accountrepo.Container) { + assertPermittablesAndRoles(t, ctx, repos, user.IDList{uId2, uId3}) + }, + }, + { + name: "should not duplicate maintainer role when it already exists", + wId: wId2, + setup: func(ctx context.Context, repos *accountrepo.Container) { + existingRole, _ := role.New().NewID().Name("maintainer").Build() + err := repos.Role.Save(ctx, *existingRole) + if err != nil { + t.Fatal(err) + } + + userRepo := accountrepo.NewMultiUser(accountmemory.NewUserWith(u1, u2, u3)) + workspaceRepo := accountmemory.NewWorkspaceWith(w2) + repos.User = userRepo + repos.Workspace = workspaceRepo + }, + assert: func(t *testing.T, ctx context.Context, repos *accountrepo.Container) { + assertPermittablesAndRoles(t, ctx, repos, user.IDList{uId2, uId3}) + }, + }, + { + name: "should not add maintainer role if user already has it", + wId: wId3, + setup: func(ctx context.Context, repos *accountrepo.Container) { + existingRole, _ := role.New().NewID().Name("maintainer").Build() + err := repos.Role.Save(ctx, *existingRole) + if err != nil { + t.Fatal(err) + } + + p, _ := permittable.New(). + NewID(). + UserID(uId2). + RoleIDs([]accountdomain.RoleID{existingRole.ID()}). + Build() + err = repos.Permittable.Save(ctx, *p) + if err != nil { + t.Fatal(err) + } + + userRepo := accountrepo.NewMultiUser(accountmemory.NewUserWith(u1, u2, u3)) + workspaceRepo := accountmemory.NewWorkspaceWith(w3) + repos.User = userRepo + repos.Workspace = workspaceRepo + }, + assert: func(t *testing.T, ctx context.Context, repos *accountrepo.Container) { + permittable, err := repos.Permittable.FindByUserID(ctx, uId2) + assert.NoError(t, err) + assert.Equal(t, 1, len(permittable.RoleIDs())) + + assertPermittablesAndRoles(t, ctx, repos, user.IDList{uId2, uId3}) + }, + }, + { + name: "should add maintainer role when user has other roles", + wId: wId4, + setup: func(ctx context.Context, repos *accountrepo.Container) { + otherRole, _ := role.New().NewID().Name("other_role").Build() + err := repos.Role.Save(ctx, *otherRole) + if err != nil { + t.Fatal(err) + } + + p, _ := permittable.New(). + NewID(). + UserID(uId2). + RoleIDs([]accountdomain.RoleID{otherRole.ID()}). + Build() + err = repos.Permittable.Save(ctx, *p) + if err != nil { + t.Fatal(err) + } + + userRepo := accountrepo.NewMultiUser(accountmemory.NewUserWith(u1, u2, u3)) + workspaceRepo := accountmemory.NewWorkspaceWith(w4) + repos.User = userRepo + repos.Workspace = workspaceRepo + }, + assert: func(t *testing.T, ctx context.Context, repos *accountrepo.Container) { + roles, err := repos.Role.FindAll(ctx) + assert.NoError(t, err) + assert.Equal(t, 2, len(roles)) + + permittable, err := repos.Permittable.FindByUserID(ctx, uId2) + assert.NoError(t, err) + assert.Equal(t, 2, len(permittable.RoleIDs())) + + assertPermittablesAndRoles(t, ctx, repos, user.IDList{uId2, uId3}) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + memoryRepo := accountmemory.New() + + if tt.setup != nil { + tt.setup(ctx, memoryRepo) + } + + enforcer := func(_ context.Context, _ *workspace.Workspace, _ user.List, _ *accountusecase.Operator) error { + return nil + } + workspaceUC := NewWorkspace(memoryRepo, enforcer) + + _, err := workspaceUC.AddUserMember(ctx, tt.wId, users, op) + + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + if tt.assert != nil { + tt.assert(t, ctx, memoryRepo) + } + }) + } +} + +// TODO: Delete this once the permission check migration is complete. +func assertPermittablesAndRoles(t *testing.T, ctx context.Context, repos *accountrepo.Container, expectedUserIDs user.IDList) { + // role + roles, err := repos.Role.FindAll(ctx) + assert.NoError(t, err) + var maintainerRole *role.Role + for _, r := range roles { + if r.Name() == "maintainer" { + if maintainerRole != nil { + t.Fatal("maintainer role already exists") + } + maintainerRole = r + } + } + assert.NotNil(t, maintainerRole) + + // permittable + permittables, err := repos.Permittable.FindByUserIDs(ctx, expectedUserIDs) + assert.NoError(t, err) + assert.Equal(t, len(expectedUserIDs), len(permittables)) + + // userID + userIds := make(user.IDList, 0, len(permittables)) + for _, p := range permittables { + userIds = append(userIds, p.UserID()) + } + for _, expectedID := range expectedUserIDs { + assert.Contains(t, userIds, expectedID) + } + + // role assignment + for _, p := range permittables { + assert.Contains(t, p.RoleIDs(), maintainerRole.ID()) + } +} diff --git a/account/accountusecase/accountrepo/container.go b/account/accountusecase/accountrepo/container.go index 1b68f505..c2f6899c 100644 --- a/account/accountusecase/accountrepo/container.go +++ b/account/accountusecase/accountrepo/container.go @@ -12,6 +12,8 @@ import ( type Container struct { User User Workspace Workspace + Role Role // TODO: Delete this once the permission check migration is complete. + Permittable Permittable // TODO: Delete this once the permission check migration is complete. Transaction usecasex.Transaction Users []User } diff --git a/account/accountusecase/accountrepo/permittable.go b/account/accountusecase/accountrepo/permittable.go new file mode 100644 index 00000000..4e717565 --- /dev/null +++ b/account/accountusecase/accountrepo/permittable.go @@ -0,0 +1,18 @@ +// TODO: Delete this file once the permission check migration is complete. + +package accountrepo + +import ( + "context" + + "github.com/reearth/reearthx/account/accountdomain" + "github.com/reearth/reearthx/account/accountdomain/permittable" + "github.com/reearth/reearthx/account/accountdomain/user" +) + +type Permittable interface { + FindByUserID(context.Context, user.ID) (*permittable.Permittable, error) + FindByUserIDs(context.Context, user.IDList) (permittable.List, error) + FindByRoleID(context.Context, accountdomain.RoleID) (permittable.List, error) + Save(context.Context, permittable.Permittable) error +} diff --git a/account/accountusecase/accountrepo/role.go b/account/accountusecase/accountrepo/role.go new file mode 100644 index 00000000..62ac5784 --- /dev/null +++ b/account/accountusecase/accountrepo/role.go @@ -0,0 +1,18 @@ +// TODO: Delete this file once the permission check migration is complete. + +package accountrepo + +import ( + "context" + + "github.com/reearth/reearthx/account/accountdomain" + "github.com/reearth/reearthx/account/accountdomain/role" +) + +type Role interface { + FindAll(context.Context) (role.List, error) + FindByID(context.Context, accountdomain.RoleID) (*role.Role, error) + FindByIDs(context.Context, accountdomain.RoleIDList) (role.List, error) + Save(context.Context, role.Role) error + Remove(context.Context, accountdomain.RoleID) error +}