Skip to content

Commit ed7d073

Browse files
committed
Add a route to revoke to recipient from a sharing
1 parent a4dc153 commit ed7d073

File tree

5 files changed

+182
-33
lines changed

5 files changed

+182
-33
lines changed

pkg/sharing/member.go

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -170,36 +170,11 @@ func (c *Credentials) Refresh(inst *instance.Instance, s *Sharing, m *Member) er
170170

171171
// RevokeMember revoke the access granted to a member and contact it
172172
func (s *Sharing) RevokeMember(inst *instance.Instance, m *Member, c *Credentials) error {
173-
u, err := url.Parse(m.Instance)
174-
if m.Instance == "" || err != nil {
175-
return ErrInvalidSharing
176-
}
177-
178173
// No need to contact the revoked member if the sharing is not ready
179174
if m.Status == MemberStatusReady {
180-
opts := &request.Options{
181-
Method: http.MethodDelete,
182-
Scheme: u.Scheme,
183-
Domain: u.Host,
184-
Path: "/sharings/" + s.SID,
185-
Headers: request.Headers{
186-
"Authorization": "Bearer " + c.AccessToken.AccessToken,
187-
},
188-
}
189-
var res *http.Response
190-
res, err := request.Req(opts)
191-
if err != nil {
192-
return err
193-
}
194-
res.Body.Close()
195-
if res.StatusCode/100 == 5 {
196-
return ErrInternalServerError
197-
}
198-
if res.StatusCode/100 == 4 {
199-
if res, err = RefreshToken(inst, s, m, c, opts, nil); err != nil {
200-
return err
201-
}
202-
res.Body.Close()
175+
if err := s.NotifyMemberRevocation(inst, m, c); err != nil {
176+
inst.Logger().WithField("nspace", "sharing").
177+
Warnf("Error on revocation notification: %s", err)
203178
}
204179

205180
if !s.ReadOnly() {
@@ -215,3 +190,37 @@ func (s *Sharing) RevokeMember(inst *instance.Instance, m *Member, c *Credential
215190

216191
return couchdb.UpdateDoc(inst, s)
217192
}
193+
194+
// NotifyMemberRevocation send a notification to this member that he/she was
195+
// revoked from this sharing
196+
func (s *Sharing) NotifyMemberRevocation(inst *instance.Instance, m *Member, c *Credentials) error {
197+
u, err := url.Parse(m.Instance)
198+
if m.Instance == "" || err != nil {
199+
return ErrInvalidSharing
200+
}
201+
202+
opts := &request.Options{
203+
Method: http.MethodDelete,
204+
Scheme: u.Scheme,
205+
Domain: u.Host,
206+
Path: "/sharings/" + s.SID,
207+
Headers: request.Headers{
208+
"Authorization": "Bearer " + c.AccessToken.AccessToken,
209+
},
210+
}
211+
res, err := request.Req(opts)
212+
if err != nil {
213+
return err
214+
}
215+
res.Body.Close()
216+
if res.StatusCode/100 == 5 {
217+
return ErrInternalServerError
218+
}
219+
if res.StatusCode/100 == 4 {
220+
if res, err = RefreshToken(inst, s, m, c, opts, nil); err != nil {
221+
return err
222+
}
223+
res.Body.Close()
224+
}
225+
return nil
226+
}

pkg/sharing/oauth.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ func DeleteOAuthClient(inst *instance.Instance, m *Member, cred *Credentials) er
163163
return ErrInvalidURL
164164
}
165165
clientID := cred.InboundClientID
166+
if clientID == "" {
167+
return nil
168+
}
166169
client, err := oauth.FindClient(inst, clientID)
167170
if err != nil {
168171
return err

pkg/sharing/sharing.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,10 +211,8 @@ func (s *Sharing) Revoke(inst *instance.Instance) error {
211211
errm = multierror.Append(errm, err)
212212
}
213213
}
214-
if s.WithPropagation() {
215-
if err := s.RemoveTriggers(inst); err != nil {
216-
return err
217-
}
214+
if err := s.RemoveTriggers(inst); err != nil {
215+
return err
218216
}
219217
if err := RemoveSharedRefs(inst, s.SID); err != nil {
220218
return err
@@ -226,6 +224,32 @@ func (s *Sharing) Revoke(inst *instance.Instance) error {
226224
return errm
227225
}
228226

227+
// RevokeRecipient revoke only one recipient on the sharer. After that, if the
228+
// sharing has still at least one active member, we keep it as is. Else, we
229+
// desactive the sharing.
230+
func (s *Sharing) RevokeRecipient(inst *instance.Instance, index int) error {
231+
if !s.Owner {
232+
return ErrInvalidSharing
233+
}
234+
if err := s.RevokeMember(inst, &s.Members[index], &s.Credentials[index-1]); err != nil {
235+
return err
236+
}
237+
238+
for _, m := range s.Members {
239+
if m.Status == MemberStatusReady {
240+
return nil
241+
}
242+
}
243+
if err := s.RemoveTriggers(inst); err != nil {
244+
return err
245+
}
246+
if err := RemoveSharedRefs(inst, s.SID); err != nil {
247+
return err
248+
}
249+
s.Active = false
250+
return couchdb.UpdateDoc(inst, s)
251+
}
252+
229253
// RemoveTriggers remove all the triggers associated to this sharing
230254
func (s *Sharing) RemoveTriggers(inst *instance.Instance) error {
231255
if err := removeSharingTrigger(inst, s.Triggers.TrackID); err != nil {

web/sharings/sharings.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package sharings
33
import (
44
"errors"
55
"net/http"
6+
"strconv"
67
"strings"
78

89
"github.com/cozy/cozy-stack/pkg/consts"
@@ -186,6 +187,28 @@ func RevokeSharing(c echo.Context) error {
186187
return c.NoContent(http.StatusNoContent)
187188
}
188189

190+
// RevokeRecipient is used by the owner to revoke a recipient
191+
func RevokeRecipient(c echo.Context) error {
192+
inst := middlewares.GetInstance(c)
193+
sharingID := c.Param("sharing-id")
194+
s, err := sharing.FindSharing(inst, sharingID)
195+
if err != nil {
196+
return wrapErrors(err)
197+
}
198+
_, err = checkCreatePermissions(c, s)
199+
if err != nil {
200+
return echo.NewHTTPError(http.StatusForbidden)
201+
}
202+
index, err := strconv.Atoi(c.Param("index"))
203+
if err != nil || index == 0 || index >= len(s.Members) {
204+
return jsonapi.InvalidParameter("index", err)
205+
}
206+
if err = s.RevokeRecipient(inst, index); err != nil {
207+
return wrapErrors(err)
208+
}
209+
return c.NoContent(http.StatusNoContent)
210+
}
211+
189212
// RevocationNotif is used to inform a recipient that the sharing is revoked
190213
func RevocationNotif(c echo.Context) error {
191214
inst := middlewares.GetInstance(c)
@@ -292,7 +315,9 @@ func Routes(router *echo.Group) {
292315
router.GET("/:sharing-id", GetSharing)
293316
router.POST("/:sharing-id/answer", AnswerSharing)
294317

318+
// Revocations
295319
router.DELETE("/:sharing-id/recipients", RevokeSharing) // On the sharer
320+
router.DELETE("/:sharing-id/recipients/:index", RevokeRecipient) // On the sharer
296321
router.DELETE("/:sharing-id", RevocationNotif, checkSharingPermissions) // On the recipient
297322

298323
router.GET("/doctype/:doctype", GetSharingsInfoByDocType)

web/sharings/sharings_test.go

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,9 @@ func TestRevokeSharing(t *testing.T) {
662662
assert.NoError(t, err)
663663
req.Header.Add(echo.HeaderContentType, "application/vnd.api+json")
664664
req.Header.Add(echo.HeaderAuthorization, "Bearer "+aliceAppToken)
665-
_, _ = http.DefaultClient.Do(req)
665+
res, err := http.DefaultClient.Do(req)
666+
assert.NoError(t, err)
667+
assert.Equal(t, 204, res.StatusCode)
666668

667669
var sRevoke sharing.Sharing
668670
err = couchdb.GetDoc(aliceInstance, s.DocType(), s.SID, &sRevoke)
@@ -680,6 +682,92 @@ func TestRevokeSharing(t *testing.T) {
680682
assert.EqualError(t, err, "CouchDB(not_found): deleted")
681683
}
682684

685+
func TestRevokeRecipient(t *testing.T) {
686+
sharedDocs := []string{"mygreatid3", "mygreatid4"}
687+
sharedRefs := []*sharing.SharedRef{}
688+
s := createSharing(t, aliceInstance, sharedDocs)
689+
for _, id := range sharedDocs {
690+
sid := iocozytests + "/" + id
691+
sd, errs := createSharedDoc(aliceInstance, sid, s.SID)
692+
sharedRefs = append(sharedRefs, sd)
693+
assert.NoError(t, errs)
694+
assert.NotNil(t, sd)
695+
}
696+
697+
cli, err := sharing.CreateOAuthClient(aliceInstance, &s.Members[1])
698+
assert.NoError(t, err)
699+
s.Credentials[0].Client = sharing.ConvertOAuthClient(cli)
700+
token, err := sharing.CreateAccessToken(aliceInstance, cli, s.SID)
701+
assert.NoError(t, err)
702+
s.Credentials[0].AccessToken = token
703+
s.Members[1].Status = sharing.MemberStatusReady
704+
705+
s.Members = append(s.Members, sharing.Member{
706+
Status: sharing.MemberStatusReady,
707+
Name: "Charlie",
708+
Email: "charlie@cozy.local",
709+
Instance: tsB.URL,
710+
})
711+
clientC, err := sharing.CreateOAuthClient(aliceInstance, &s.Members[2])
712+
assert.NoError(t, err)
713+
tokenC, err := sharing.CreateAccessToken(aliceInstance, clientC, s.SID)
714+
assert.NoError(t, err)
715+
s.Credentials = append(s.Credentials, sharing.Credentials{
716+
Client: sharing.ConvertOAuthClient(clientC),
717+
AccessToken: tokenC,
718+
})
719+
720+
err = couchdb.UpdateDoc(aliceInstance, s)
721+
assert.NoError(t, err)
722+
723+
err = s.AddTrackTriggers(aliceInstance)
724+
assert.NoError(t, err)
725+
err = s.AddReplicateTrigger(aliceInstance)
726+
assert.NoError(t, err)
727+
728+
req, err := http.NewRequest(http.MethodDelete, tsA.URL+"/sharings/"+s.ID()+"/recipients/1", nil)
729+
assert.NoError(t, err)
730+
req.Header.Add(echo.HeaderContentType, "application/vnd.api+json")
731+
req.Header.Add(echo.HeaderAuthorization, "Bearer "+aliceAppToken)
732+
res, err := http.DefaultClient.Do(req)
733+
assert.NoError(t, err)
734+
assert.Equal(t, 204, res.StatusCode)
735+
736+
var sRevokedBob sharing.Sharing
737+
err = couchdb.GetDoc(aliceInstance, s.DocType(), s.SID, &sRevokedBob)
738+
assert.NoError(t, err)
739+
740+
assert.Equal(t, sharing.MemberStatusRevoked, sRevokedBob.Members[1].Status)
741+
assert.Equal(t, sharing.MemberStatusReady, sRevokedBob.Members[2].Status)
742+
assert.NotEmpty(t, sRevokedBob.Triggers.TrackID)
743+
assert.NotEmpty(t, sRevokedBob.Triggers.ReplicateID)
744+
assert.True(t, sRevokedBob.Active)
745+
746+
req2, err := http.NewRequest(http.MethodDelete, tsA.URL+"/sharings/"+s.ID()+"/recipients/2", nil)
747+
assert.NoError(t, err)
748+
req2.Header.Add(echo.HeaderContentType, "application/vnd.api+json")
749+
req2.Header.Add(echo.HeaderAuthorization, "Bearer "+aliceAppToken)
750+
res2, err := http.DefaultClient.Do(req2)
751+
assert.NoError(t, err)
752+
assert.Equal(t, 204, res2.StatusCode)
753+
754+
var sRevokedCharlie sharing.Sharing
755+
err = couchdb.GetDoc(aliceInstance, s.DocType(), s.SID, &sRevokedCharlie)
756+
assert.NoError(t, err)
757+
758+
assert.Equal(t, sharing.MemberStatusRevoked, sRevokedCharlie.Members[1].Status)
759+
assert.Equal(t, sharing.MemberStatusRevoked, sRevokedCharlie.Members[2].Status)
760+
assert.Empty(t, sRevokedCharlie.Triggers.TrackID)
761+
assert.Empty(t, sRevokedCharlie.Triggers.ReplicateID)
762+
assert.False(t, sRevokedCharlie.Active)
763+
764+
var sdoc sharing.SharedRef
765+
err = couchdb.GetDoc(aliceInstance, sharedRefs[0].DocType(), sharedRefs[0].ID(), &sdoc)
766+
assert.EqualError(t, err, "CouchDB(not_found): deleted")
767+
err = couchdb.GetDoc(aliceInstance, sharedRefs[1].DocType(), sharedRefs[1].ID(), &sdoc)
768+
assert.EqualError(t, err, "CouchDB(not_found): deleted")
769+
}
770+
683771
func TestMain(m *testing.M) {
684772
config.UseTestFile()
685773
config.GetConfig().Assets = "../../assets"

0 commit comments

Comments
 (0)