Skip to content

Commit 3ffe388

Browse files
committed
feat: Introduce route to change group permission
Similar to route to change member permission. On existing member or group add, it is forbidden to add a member or a group that contains a member with a different permissions than an existing member. I kept this behavior when changing permissions to avoid any secondary effect.
1 parent a4857f8 commit 3ffe388

8 files changed

Lines changed: 1371 additions & 1 deletion

File tree

docs/sharing.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,6 +1246,8 @@ This route is used to add the read-only flag on a recipient of a sharing.
12461246

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

1249+
It returns 400 if the member belongs to any non-revoked group.
1250+
12491251
##### Request
12501252

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

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

1305+
It returns 400 if the member belongs to any non-revoked group.
1306+
13031307
##### Request
13041308

13051309
```http
@@ -1313,6 +1317,60 @@ Host: alice.example.net
13131317
HTTP/1.1 204 No Content
13141318
```
13151319

1320+
### POST /sharings/:sharing-id/groups/:group-index/readonly
1321+
1322+
Downgrade all the members of a group to read-only.
1323+
1324+
- The owner can always call this route.
1325+
- A non-owner can call it only if:
1326+
- they are a member of the target group,
1327+
- the group is currently read-write,
1328+
- the sharing is not an OrgDrive.
1329+
- The change is propagated to recipients via the standard credentials
1330+
exchange.
1331+
1332+
#### Request
1333+
1334+
```http
1335+
POST /sharings/ce8835a061d0ef68947afe69a0046722/groups/0/readonly HTTP/1.1
1336+
Host: alice.example.net
1337+
```
1338+
1339+
#### Response
1340+
1341+
```http
1342+
HTTP/1.1 204 No Content
1343+
```
1344+
1345+
It returns 400 `group_read_only_conflict` if:
1346+
1347+
- a member of the group also belongs to another group whose read-only state
1348+
differs from the target, or
1349+
- a member of the group was added individually (`only_in_groups: false`) and
1350+
their individual `read_only` flag conflicts with the target state.
1351+
1352+
### DELETE /sharings/:sharing-id/groups/:group-index/readonly
1353+
1354+
Upgrade all the members of a group to read-write.
1355+
1356+
- Only the owner can call this route.
1357+
1358+
It returns 400 `group_read_only_conflict` under the same conditions as
1359+
`POST /sharings/:sharing-id/groups/:group-index/readonly`.
1360+
1361+
#### Request
1362+
1363+
```http
1364+
DELETE /sharings/ce8835a061d0ef68947afe69a0046722/groups/0/readonly HTTP/1.1
1365+
Host: alice.example.net
1366+
```
1367+
1368+
#### Response
1369+
1370+
```http
1371+
HTTP/1.1 204 No Content
1372+
```
1373+
13161374
### DELETE /sharings/:sharing-id/recipients/self/readonly
13171375

13181376
This is an internal route for the stack. It's used to inform the recipient's

model/sharing/error.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,7 @@ var (
7979
ErrFileInTrash = errors.New("Cannot share trashed file")
8080
// ErrSystemFolder is used when trying to share a system folder
8181
ErrSystemFolder = errors.New("Cannot share system folder")
82+
// ErrGroupReadOnlyConflict is used when changing a group's read-only flag
83+
// would conflict with another group's state for a member belonging to both.
84+
ErrGroupReadOnlyConflict = errors.New("Cannot change group read-only flag: a member also belongs to another group with a conflicting read-only state")
8285
)

model/sharing/group_readonly.go

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package sharing
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"strings"
7+
8+
"github.com/cozy/cozy-stack/client/request"
9+
"github.com/cozy/cozy-stack/model/instance"
10+
"github.com/cozy/cozy-stack/pkg/couchdb"
11+
multierror "github.com/hashicorp/go-multierror"
12+
"github.com/labstack/echo/v4"
13+
)
14+
15+
func (s *Sharing) CheckMemberGroupReadOnlyConsistency(memberIndex int) error {
16+
for _, gidx := range s.Members[memberIndex].Groups {
17+
if gidx < 0 || gidx >= len(s.Groups) {
18+
continue
19+
}
20+
if s.Groups[gidx].Revoked {
21+
continue
22+
}
23+
return ErrGroupReadOnlyConflict
24+
}
25+
return nil
26+
}
27+
28+
func (s *Sharing) checkGroupReadOnlyChangeConsistency(groupIndex int, targetReadOnly bool) error {
29+
for _, m := range s.Members {
30+
inGroup := false
31+
for _, idx := range m.Groups {
32+
if idx == groupIndex {
33+
inGroup = true
34+
break
35+
}
36+
}
37+
if !inGroup {
38+
continue
39+
}
40+
for _, otherIdx := range m.Groups {
41+
if otherIdx == groupIndex {
42+
continue
43+
}
44+
if otherIdx < 0 || otherIdx >= len(s.Groups) {
45+
continue
46+
}
47+
if s.Groups[otherIdx].Revoked {
48+
continue
49+
}
50+
if s.Groups[otherIdx].ReadOnly != targetReadOnly {
51+
return ErrGroupReadOnlyConflict
52+
}
53+
}
54+
}
55+
return nil
56+
}
57+
58+
func (s *Sharing) checkGroupMembersIndividualConsistency(groupIndex int, targetReadOnly bool) error {
59+
for _, m := range s.Members {
60+
inGroup := false
61+
for _, idx := range m.Groups {
62+
if idx == groupIndex {
63+
inGroup = true
64+
break
65+
}
66+
}
67+
if !inGroup {
68+
continue
69+
}
70+
if !m.OnlyInGroups && m.ReadOnly != targetReadOnly {
71+
return ErrGroupReadOnlyConflict
72+
}
73+
}
74+
return nil
75+
}
76+
77+
func (s *Sharing) AddReadOnlyFlagToGroup(inst *instance.Instance, groupIndex int) error {
78+
if !s.Owner {
79+
return ErrInvalidSharing
80+
}
81+
if groupIndex < 0 || groupIndex >= len(s.Groups) {
82+
return ErrInvalidSharing
83+
}
84+
if s.Groups[groupIndex].Revoked {
85+
return ErrInvalidSharing
86+
}
87+
if s.Groups[groupIndex].ReadOnly {
88+
return nil
89+
}
90+
if err := s.checkGroupReadOnlyChangeConsistency(groupIndex, true); err != nil {
91+
return err
92+
}
93+
if err := s.checkGroupMembersIndividualConsistency(groupIndex, true); err != nil {
94+
return err
95+
}
96+
var errm error
97+
for i, m := range s.Members {
98+
if i == 0 {
99+
continue
100+
}
101+
inGroup := false
102+
for _, idx := range m.Groups {
103+
if idx == groupIndex {
104+
inGroup = true
105+
break
106+
}
107+
}
108+
if !inGroup {
109+
continue
110+
}
111+
if s.Members[i].ReadOnly {
112+
continue
113+
}
114+
if err := s.AddReadOnlyFlag(inst, i); err != nil {
115+
errm = multierror.Append(errm, err)
116+
}
117+
}
118+
if errm == nil {
119+
s.Groups[groupIndex].ReadOnly = true
120+
if err := couchdb.UpdateDoc(inst, s); err != nil {
121+
errm = err
122+
}
123+
}
124+
return errm
125+
}
126+
127+
func (s *Sharing) RemoveReadOnlyFlagFromGroup(inst *instance.Instance, groupIndex int) error {
128+
if !s.Owner {
129+
return ErrInvalidSharing
130+
}
131+
if groupIndex < 0 || groupIndex >= len(s.Groups) {
132+
return ErrInvalidSharing
133+
}
134+
if s.Groups[groupIndex].Revoked {
135+
return ErrInvalidSharing
136+
}
137+
if !s.Groups[groupIndex].ReadOnly {
138+
return nil
139+
}
140+
if err := s.checkGroupReadOnlyChangeConsistency(groupIndex, false); err != nil {
141+
return err
142+
}
143+
if err := s.checkGroupMembersIndividualConsistency(groupIndex, false); err != nil {
144+
return err
145+
}
146+
var errm error
147+
for i, m := range s.Members {
148+
if i == 0 {
149+
continue
150+
}
151+
inGroup := false
152+
for _, idx := range m.Groups {
153+
if idx == groupIndex {
154+
inGroup = true
155+
break
156+
}
157+
}
158+
if !inGroup {
159+
continue
160+
}
161+
if !s.Members[i].ReadOnly {
162+
continue
163+
}
164+
if err := s.RemoveReadOnlyFlag(inst, i); err != nil {
165+
errm = multierror.Append(errm, err)
166+
}
167+
}
168+
if errm == nil {
169+
s.Groups[groupIndex].ReadOnly = false
170+
if err := couchdb.UpdateDoc(inst, s); err != nil {
171+
errm = err
172+
}
173+
}
174+
return errm
175+
}
176+
177+
func (s *Sharing) DelegateAddReadOnlyFlagToGroup(inst *instance.Instance, groupIndex int) error {
178+
if len(s.Credentials) != 1 {
179+
return ErrInvalidSharing
180+
}
181+
m := &s.Members[0]
182+
u, ok := m.InstanceURL()
183+
if !ok {
184+
return ErrInvalidSharing
185+
}
186+
c := &s.Credentials[0]
187+
if c.AccessToken == nil {
188+
return ErrInvalidSharing
189+
}
190+
opts := &request.Options{
191+
Method: http.MethodPost,
192+
Scheme: u.Scheme,
193+
Domain: u.Host,
194+
Path: fmt.Sprintf("/sharings/%s/groups/%d/readonly", s.SID, groupIndex),
195+
Headers: request.Headers{
196+
echo.HeaderAuthorization: "Bearer " + c.AccessToken.AccessToken,
197+
},
198+
ParseError: ParseRequestError,
199+
}
200+
res, err := request.Req(opts)
201+
if res != nil && res.StatusCode/100 == 4 {
202+
res, err = RefreshToken(inst, res, err, s, m, c, opts, nil)
203+
}
204+
if err != nil {
205+
if res != nil && res.StatusCode == http.StatusBadRequest {
206+
if reqErr, ok := err.(*request.Error); ok && strings.Contains(reqErr.Detail, ErrGroupReadOnlyConflict.Error()) {
207+
return ErrGroupReadOnlyConflict
208+
}
209+
return ErrInvalidURL
210+
}
211+
return err
212+
}
213+
res.Body.Close()
214+
return nil
215+
}

0 commit comments

Comments
 (0)