Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions docs/sharing.md
Original file line number Diff line number Diff line change
Expand Up @@ -1246,6 +1246,8 @@ This route is used to add the read-only flag on a recipient of a sharing.

**Note**: 0 is not accepted for `index`, as it is the sharer him-self.

It returns 400 if the member belongs to any non-revoked group.

##### Request

```http
Expand Down Expand Up @@ -1300,6 +1302,8 @@ This route is used to remove the read-only flag on a recipient of a sharing.

**Note**: 0 is not accepted for `index`, as it is the sharer him-self.

It returns 400 if the member belongs to any non-revoked group.

##### Request

```http
Expand All @@ -1313,6 +1317,60 @@ Host: alice.example.net
HTTP/1.1 204 No Content
```

### POST /sharings/:sharing-id/groups/:group-index/readonly

Downgrade all the members of a group to read-only.

- The owner can always call this route.
- A non-owner can call it only if:
- they are a member of the target group,
- the group is currently read-write,
- the sharing is not an OrgDrive.
- The change is propagated to recipients via the standard credentials
exchange.

#### Request

```http
POST /sharings/ce8835a061d0ef68947afe69a0046722/groups/0/readonly HTTP/1.1
Host: alice.example.net
```

#### Response

```http
HTTP/1.1 204 No Content
```

It returns 400 `group_read_only_conflict` if:

- a member of the group also belongs to another group whose read-only state
differs from the target, or
- a member of the group was added individually (`only_in_groups: false`) and
their individual `read_only` flag conflicts with the target state.

### DELETE /sharings/:sharing-id/groups/:group-index/readonly

Upgrade all the members of a group to read-write.

- Only the owner can call this route.

It returns 400 `group_read_only_conflict` under the same conditions as
`POST /sharings/:sharing-id/groups/:group-index/readonly`.

#### Request

```http
DELETE /sharings/ce8835a061d0ef68947afe69a0046722/groups/0/readonly HTTP/1.1
Host: alice.example.net
```

#### Response

```http
HTTP/1.1 204 No Content
```

### DELETE /sharings/:sharing-id/recipients/self/readonly

This is an internal route for the stack. It's used to inform the recipient's
Expand Down
3 changes: 3 additions & 0 deletions model/sharing/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,7 @@ var (
ErrFileInTrash = errors.New("Cannot share trashed file")
// ErrSystemFolder is used when trying to share a system folder
ErrSystemFolder = errors.New("Cannot share system folder")
// ErrGroupReadOnlyConflict is used when changing a group's read-only flag
// would conflict with another group's state for a member belonging to both.
ErrGroupReadOnlyConflict = errors.New("Cannot change group read-only flag: a member also belongs to another group with a conflicting read-only state")
)
215 changes: 215 additions & 0 deletions model/sharing/group_readonly.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package sharing

import (
"fmt"
"net/http"
"strings"

"github.com/cozy/cozy-stack/client/request"
"github.com/cozy/cozy-stack/model/instance"
"github.com/cozy/cozy-stack/pkg/couchdb"
multierror "github.com/hashicorp/go-multierror"
"github.com/labstack/echo/v4"
)

func (s *Sharing) CheckMemberGroupReadOnlyConsistency(memberIndex int) error {
for _, gidx := range s.Members[memberIndex].Groups {
if gidx < 0 || gidx >= len(s.Groups) {
continue
}
if s.Groups[gidx].Revoked {
continue
}
return ErrGroupReadOnlyConflict
}
return nil
}

func (s *Sharing) checkGroupReadOnlyChangeConsistency(groupIndex int, targetReadOnly bool) error {
for _, m := range s.Members {
inGroup := false
for _, idx := range m.Groups {
if idx == groupIndex {
inGroup = true
break
}
}
if !inGroup {
continue
}
for _, otherIdx := range m.Groups {
if otherIdx == groupIndex {
continue
}
if otherIdx < 0 || otherIdx >= len(s.Groups) {
continue
}
if s.Groups[otherIdx].Revoked {
continue
}
if s.Groups[otherIdx].ReadOnly != targetReadOnly {
return ErrGroupReadOnlyConflict
}
}
}
return nil
}

func (s *Sharing) checkGroupMembersIndividualConsistency(groupIndex int, targetReadOnly bool) error {
for _, m := range s.Members {
inGroup := false
for _, idx := range m.Groups {
if idx == groupIndex {
inGroup = true
break
}
}
if !inGroup {
continue
}
if !m.OnlyInGroups && m.ReadOnly != targetReadOnly {
return ErrGroupReadOnlyConflict
}
}
return nil
}

func (s *Sharing) AddReadOnlyFlagToGroup(inst *instance.Instance, groupIndex int) error {
if !s.Owner {
return ErrInvalidSharing
}
if groupIndex < 0 || groupIndex >= len(s.Groups) {
return ErrInvalidSharing
}
if s.Groups[groupIndex].Revoked {
return ErrInvalidSharing
}
if s.Groups[groupIndex].ReadOnly {
return nil
}
if err := s.checkGroupReadOnlyChangeConsistency(groupIndex, true); err != nil {
return err
}
if err := s.checkGroupMembersIndividualConsistency(groupIndex, true); err != nil {
return err
}
var errm error
for i, m := range s.Members {
if i == 0 {
continue
}
inGroup := false
for _, idx := range m.Groups {
if idx == groupIndex {
inGroup = true
break
}
}
if !inGroup {
continue
}
if s.Members[i].ReadOnly {
continue
}
if err := s.AddReadOnlyFlag(inst, i); err != nil {
errm = multierror.Append(errm, err)
}
}
if errm == nil {
s.Groups[groupIndex].ReadOnly = true
if err := couchdb.UpdateDoc(inst, s); err != nil {
errm = err
}
}
return errm
}

func (s *Sharing) RemoveReadOnlyFlagFromGroup(inst *instance.Instance, groupIndex int) error {
if !s.Owner {
return ErrInvalidSharing
}
if groupIndex < 0 || groupIndex >= len(s.Groups) {
return ErrInvalidSharing
}
if s.Groups[groupIndex].Revoked {
return ErrInvalidSharing
}
if !s.Groups[groupIndex].ReadOnly {
return nil
}
if err := s.checkGroupReadOnlyChangeConsistency(groupIndex, false); err != nil {
return err
}
if err := s.checkGroupMembersIndividualConsistency(groupIndex, false); err != nil {
return err
}
var errm error
for i, m := range s.Members {
if i == 0 {
continue
}
inGroup := false
for _, idx := range m.Groups {
if idx == groupIndex {
inGroup = true
break
}
}
if !inGroup {
continue
}
if !s.Members[i].ReadOnly {
continue
}
if err := s.RemoveReadOnlyFlag(inst, i); err != nil {
errm = multierror.Append(errm, err)
}
}
if errm == nil {
s.Groups[groupIndex].ReadOnly = false
if err := couchdb.UpdateDoc(inst, s); err != nil {
errm = err
}
}
return errm
}

func (s *Sharing) DelegateAddReadOnlyFlagToGroup(inst *instance.Instance, groupIndex int) error {
if len(s.Credentials) != 1 {
return ErrInvalidSharing
}
m := &s.Members[0]
u, ok := m.InstanceURL()
if !ok {
return ErrInvalidSharing
}
c := &s.Credentials[0]
if c.AccessToken == nil {
return ErrInvalidSharing
}
opts := &request.Options{
Method: http.MethodPost,
Scheme: u.Scheme,
Domain: u.Host,
Path: fmt.Sprintf("/sharings/%s/groups/%d/readonly", s.SID, groupIndex),
Headers: request.Headers{
echo.HeaderAuthorization: "Bearer " + c.AccessToken.AccessToken,
},
ParseError: ParseRequestError,
}
res, err := request.Req(opts)
if res != nil && res.StatusCode/100 == 4 {
res, err = RefreshToken(inst, res, err, s, m, c, opts, nil)
}
if err != nil {
if res != nil && res.StatusCode == http.StatusBadRequest {
if reqErr, ok := err.(*request.Error); ok && strings.Contains(reqErr.Detail, ErrGroupReadOnlyConflict.Error()) {
return ErrGroupReadOnlyConflict
}
return ErrInvalidURL
}
return err
}
res.Body.Close()
return nil
}
Loading
Loading