Skip to content

Commit

Permalink
Add a share-group worker
Browse files Browse the repository at this point in the history
  • Loading branch information
nono committed Feb 19, 2024
1 parent da970c0 commit e02e9de
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 10 deletions.
3 changes: 2 additions & 1 deletion cozy.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ jobs:
# - "push": sending push notifications
# - "sms": sending SMS notifications
# - "sendmail": sending mails
# - "share-replicate": for cozy to cozy sharing
# - "share-group": for cozy to cozy sharing
# - "share-replicate": idem
# - "share-track": idem
# - "share-upload": idem
# - "thumbnail": creatings and deleting thumbnails for images
Expand Down
15 changes: 11 additions & 4 deletions docs/workers.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,11 +343,18 @@ in the config file, via the `fs.auto_clean_trashed_after` parameter.

## share workers

The stack have 3 workers to power the sharings (internal usage only):
The stack have 4 workers to power the sharings (internal usage only):

1. `share-track`, to update the `io.cozy.shared` database
2. `share-replicate`, to start a replicator for most documents
3. `share-upload`, to upload files
1. `share-group`, to add/remove members to a sharing
2. `share-track`, to update the `io.cozy.shared` database
3. `share-replicate`, to start a replicator for most documents
4. `share-upload`, to upload files

### Share-group

When a contact is added or removed to a group, they should be added to the
sharings of this group. The message is composed of the contact ID, the list
of groups added and the list of groups removed.

### Share-track

Expand Down
124 changes: 119 additions & 5 deletions model/sharing/group.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
package sharing

import (
"sort"

"github.com/cozy/cozy-stack/model/contact"
"github.com/cozy/cozy-stack/model/instance"
"github.com/cozy/cozy-stack/pkg/couchdb"
multierror "github.com/hashicorp/go-multierror"
)

// Group contains the information about a group of members of the sharing.
type Group struct {
ID string `json:"id,omitempty"` // Only present on the instance where the group was added
Name string `json:"name"`
AddedBy int `json:"addedBy"` // The index of the member who have added the group
Removed bool `json:"removed,omitempty"`
ID string `json:"id,omitempty"` // Only present on the instance where the group was added
Name string `json:"name"`
AddedBy int `json:"addedBy"` // The index of the member who have added the group
ReadOnly bool `json:"read_only"`
Removed bool `json:"removed,omitempty"`
}

// GroupMessage is used for jobs on the share-group worker.
type GroupMessage struct {
ContactID string `json:"contact_id"`
GroupsAdded []string `json:"added"`
GroupsRemoved []string `json:"removed"`
}

// AddGroup adds a group of contacts identified by its ID to the members of the
Expand All @@ -38,9 +49,10 @@ func (s *Sharing) AddGroup(inst *instance.Instance, groupID string, readOnly boo
return err
}
s.Members[idx].Groups = append(s.Members[idx].Groups, groupIndex)
sort.Ints(s.Members[idx].Groups)
}

g := Group{ID: groupID, Name: group.Name(), AddedBy: 0}
g := Group{ID: groupID, Name: group.Name(), AddedBy: 0, ReadOnly: readOnly}
s.Groups = append(s.Groups, g)
return nil
}
Expand Down Expand Up @@ -83,3 +95,105 @@ func (s *Sharing) RevokeGroup(inst *instance.Instance, index int) error {
s.Groups[index].Removed = true
return couchdb.UpdateDoc(inst, s)
}

// UpdateGroups is called when a contact is added or removed to a group. It
// finds the sharings for this group, and adds or removes the member to those
// sharings.
func UpdateGroups(inst *instance.Instance, msg GroupMessage) error {
sharings, err := FindActive(inst)
if err != nil {
return err
}

var errm error
for _, s := range sharings {
for _, added := range msg.GroupsAdded {
for idx, group := range s.Groups {
if group.ID == added {
if err := s.AddMemberToGroup(inst, idx, msg.ContactID); err != nil {
errm = multierror.Append(errm, err)
}
}
}
}
for _, removed := range msg.GroupsRemoved {
for idx, group := range s.Groups {
if group.ID == removed {
if err := s.RemoveMemberFromGroup(inst, idx, msg.ContactID); err != nil {
errm = multierror.Append(errm, err)
}
}
}
}
}

return errm
}

// AddMemberToGroup adds a contact to a sharing via a group.
func (s *Sharing) AddMemberToGroup(inst *instance.Instance, groupIndex int, contactID string) error {
contact, err := contact.Find(inst, contactID)
if err != nil {
return err
}

readOnly := s.Groups[groupIndex].ReadOnly
m, err := buildMemberFromContact(contact, readOnly)
if err != nil {
return err
}
m.OnlyInGroups = true
_, idx, err := s.addMember(inst, m)
if err != nil {
return err
}
s.Members[idx].Groups = append(s.Members[idx].Groups, groupIndex)
sort.Ints(s.Members[idx].Groups)

return couchdb.UpdateDoc(inst, s)
}

// RemoveMemberFromGroup removes a member of a group.
func (s *Sharing) RemoveMemberFromGroup(inst *instance.Instance, groupIndex int, contactID string) error {
contact, err := contact.Find(inst, contactID)
if err != nil {
return err
}
var email string
if addr, err := contact.ToMailAddress(); err == nil {
email = addr.Email
}
cozyURL := contact.PrimaryCozyURL()

matchMember := func(m Member) bool {
if m.Email != "" && m.Email == email {
return true
}
if m.Instance != "" && m.Instance == cozyURL {
return true
}
return false
}

for i, m := range s.Members {
if !matchMember(m) {
continue
}

var groups []int
for _, idx := range m.Groups {
if idx != groupIndex {
groups = append(groups, idx)
}
}
s.Members[i].Groups = groups

if m.OnlyInGroups && len(s.Members[i].Groups) == 0 {
return s.RevokeRecipient(inst, i)
} else {
return couchdb.UpdateDoc(inst, s)
}
}

return nil
}
77 changes: 77 additions & 0 deletions model/sharing/group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,83 @@ func TestGroups(t *testing.T) {
assert.True(t, s.Groups[0].Removed)
assert.True(t, s.Groups[1].Removed)
})

t.Run("UpdateGroups", func(t *testing.T) {
now := time.Now()
friends := createGroup(t, inst, "Friends")
football := createGroup(t, inst, "Football")
_ = createContactInGroups(t, inst, "Bob", []string{friends.ID()})
charlie := createContactInGroups(t, inst, "Charlie", []string{football.ID()})

s := &Sharing{
Active: true,
Owner: true,
Description: "Just testing groups",
Members: []Member{
{Status: MemberStatusOwner, Name: "Alice", Email: "[email protected]"},
},
CreatedAt: now,
UpdatedAt: now,
}
require.NoError(t, couchdb.CreateDoc(inst, s))
require.NoError(t, s.AddGroup(inst, friends.ID(), false))
require.NoError(t, s.AddGroup(inst, football.ID(), false))
require.NoError(t, couchdb.UpdateDoc(inst, s))
sid := s.ID()

require.Len(t, s.Members, 3)
require.Equal(t, s.Members[0].Name, "Alice")
require.Equal(t, s.Members[1].Name, "Bob")
assert.True(t, s.Members[1].OnlyInGroups)
assert.Equal(t, s.Members[1].Groups, []int{0})
require.Equal(t, s.Members[2].Name, "Charlie")
assert.True(t, s.Members[2].OnlyInGroups)
assert.Equal(t, s.Members[2].Groups, []int{1})

require.Len(t, s.Groups, 2)
require.Equal(t, s.Groups[0].Name, "Friends")
assert.False(t, s.Groups[0].Removed)
require.Equal(t, s.Groups[1].Name, "Football")
assert.False(t, s.Groups[1].Removed)

// Charlie is added to the friends group
msg1 := GroupMessage{
ContactID: charlie.ID(),
GroupsAdded: []string{friends.ID()},
}
require.NoError(t, UpdateGroups(inst, msg1))

s = &Sharing{}
require.NoError(t, couchdb.GetDoc(inst, consts.Sharings, sid, s))

require.Len(t, s.Members, 3)
require.Equal(t, s.Members[0].Name, "Alice")
require.Equal(t, s.Members[1].Name, "Bob")
assert.True(t, s.Members[1].OnlyInGroups)
assert.Equal(t, s.Members[1].Groups, []int{0})
require.Equal(t, s.Members[2].Name, "Charlie")
assert.True(t, s.Members[2].OnlyInGroups)
assert.Equal(t, s.Members[2].Groups, []int{0, 1})

// Charlie is removed of the football group
msg2 := GroupMessage{
ContactID: charlie.ID(),
GroupsRemoved: []string{football.ID()},
}
require.NoError(t, UpdateGroups(inst, msg2))

s = &Sharing{}
require.NoError(t, couchdb.GetDoc(inst, consts.Sharings, sid, s))

require.Len(t, s.Members, 3)
require.Equal(t, s.Members[0].Name, "Alice")
require.Equal(t, s.Members[1].Name, "Bob")
assert.True(t, s.Members[1].OnlyInGroups)
assert.Equal(t, s.Members[1].Groups, []int{0})
require.Equal(t, s.Members[2].Name, "Charlie")
assert.True(t, s.Members[2].OnlyInGroups)
assert.Equal(t, s.Members[2].Groups, []int{0})
})
}

func createGroup(t *testing.T, inst *instance.Instance, name string) *contact.Group {
Expand Down
15 changes: 15 additions & 0 deletions model/sharing/sharing.go
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,21 @@ func FindSharings(db prefixer.Prefixer, sharingIDs []string) ([]*Sharing, error)
return res, nil
}

// FindActive returns the list of active sharings.
func FindActive(db prefixer.Prefixer) ([]*Sharing, error) {
req := &couchdb.FindRequest{
UseIndex: "active",
Selector: mango.Equal("active", true),
Limit: 1000,
}
var res []*Sharing
err := couchdb.FindDocs(db, consts.Sharings, req, &res)
if err != nil {
return nil, err
}
return res, nil
}

// GetSharingsByDocType returns all the sharings for the given doctype
func GetSharingsByDocType(inst *instance.Instance, docType string) (map[string]*Sharing, error) {
req := &couchdb.ViewRequest{
Expand Down
3 changes: 3 additions & 0 deletions pkg/couchdb/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ var Indexes = []*mango.Index{

// Used to find the contacts in a group
mango.MakeIndex(consts.Contacts, "by-groups", mango.IndexDef{Fields: []string{"relationships.groups.data"}}),

// Used to find the active sharings
mango.MakeIndex(consts.Sharings, "active", mango.IndexDef{Fields: []string{"active"}}),
}

// DiskUsageView is the view used for computing the disk usage for files
Expand Down
22 changes: 22 additions & 0 deletions worker/share/share.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package share is where the workers for Cozy to Cozy sharings are defined.
package share

import (
Expand All @@ -9,6 +10,15 @@ import (
)

func init() {
job.AddWorker(&job.WorkerConfig{
WorkerType: "share-group",
Concurrency: runtime.NumCPU(),
MaxExecCount: 2,
Reserved: true,
Timeout: 30 * time.Second,
WorkerFunc: WorkerGroup,
})

job.AddWorker(&job.WorkerConfig{
WorkerType: "share-track",
Concurrency: runtime.NumCPU(),
Expand Down Expand Up @@ -43,6 +53,18 @@ func init() {
})
}

// WorkerGroup is used to update the list of members of sharings for a group
// when someone is added or removed to this group.
func WorkerGroup(ctx *job.TaskContext) error {
var msg sharing.GroupMessage
if err := ctx.UnmarshalMessage(&msg); err != nil {
return err
}
ctx.Instance.Logger().WithNamespace("share").
Debugf("Group %#v", msg)
return sharing.UpdateGroups(ctx.Instance, msg)
}

// WorkerTrack is used to update the io.cozy.shared database when a document
// that matches a sharing rule is created/updated/remove
func WorkerTrack(ctx *job.TaskContext) error {
Expand Down

0 comments on commit e02e9de

Please sign in to comment.