github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/sharings/sharings.go (about)

     1  // Package sharings is the HTTP routes for the sharing. We have two types of
     2  // routes, some routes are used by the clients to create, list, revoke sharings
     3  // and add/remove recipients, and other routes are reserved for an internal
     4  // usage, mostly to synchronize the documents between the Cozys of the members
     5  // of the sharings.
     6  package sharings
     7  
     8  import (
     9  	"encoding/json"
    10  	"errors"
    11  	"net/http"
    12  	"net/url"
    13  	"strconv"
    14  	"strings"
    15  
    16  	"github.com/cozy/cozy-stack/model/contact"
    17  	"github.com/cozy/cozy-stack/model/instance"
    18  	"github.com/cozy/cozy-stack/model/oauth"
    19  	"github.com/cozy/cozy-stack/model/permission"
    20  	"github.com/cozy/cozy-stack/model/settings"
    21  	"github.com/cozy/cozy-stack/model/sharing"
    22  	"github.com/cozy/cozy-stack/model/vfs"
    23  	"github.com/cozy/cozy-stack/pkg/avatar"
    24  	"github.com/cozy/cozy-stack/pkg/config/config"
    25  	"github.com/cozy/cozy-stack/pkg/consts"
    26  	"github.com/cozy/cozy-stack/pkg/couchdb"
    27  	"github.com/cozy/cozy-stack/pkg/crypto"
    28  	"github.com/cozy/cozy-stack/pkg/jsonapi"
    29  	"github.com/cozy/cozy-stack/pkg/logger"
    30  	"github.com/cozy/cozy-stack/pkg/safehttp"
    31  	"github.com/cozy/cozy-stack/web/middlewares"
    32  	"github.com/hashicorp/go-multierror"
    33  	"github.com/labstack/echo/v4"
    34  )
    35  
    36  // CreateSharing initializes a new sharing (on the sharer)
    37  func CreateSharing(c echo.Context) error {
    38  	inst := middlewares.GetInstance(c)
    39  
    40  	var s sharing.Sharing
    41  	obj, err := jsonapi.Bind(c.Request().Body, &s)
    42  	if err != nil {
    43  		return jsonapi.BadJSON()
    44  	}
    45  
    46  	slug, err := checkCreatePermissions(c, &s)
    47  	if err != nil {
    48  		return echo.NewHTTPError(http.StatusForbidden)
    49  	}
    50  
    51  	if err = s.BeOwner(inst, slug); err != nil {
    52  		return wrapErrors(err)
    53  	}
    54  
    55  	if rel, ok := obj.GetRelationship("recipients"); ok {
    56  		if data, ok := rel.Data.([]interface{}); ok {
    57  			for _, ref := range data {
    58  				if t, _ := ref.(map[string]interface{})["type"].(string); t == consts.Groups {
    59  					if id, ok := ref.(map[string]interface{})["id"].(string); ok {
    60  						if err = s.AddGroup(inst, id, false); err != nil {
    61  							return err
    62  						}
    63  					}
    64  				} else {
    65  					if id, ok := ref.(map[string]interface{})["id"].(string); ok {
    66  						if err = s.AddContact(inst, id, false); err != nil {
    67  							return err
    68  						}
    69  					}
    70  				}
    71  			}
    72  		}
    73  	}
    74  
    75  	if rel, ok := obj.GetRelationship("read_only_recipients"); ok {
    76  		if data, ok := rel.Data.([]interface{}); ok {
    77  			for _, ref := range data {
    78  				if t, _ := ref.(map[string]interface{})["type"].(string); t == consts.Groups {
    79  					if id, ok := ref.(map[string]interface{})["id"].(string); ok {
    80  						if err = s.AddGroup(inst, id, true); err != nil {
    81  							return err
    82  						}
    83  					}
    84  				} else {
    85  					if id, ok := ref.(map[string]interface{})["id"].(string); ok {
    86  						if err = s.AddContact(inst, id, true); err != nil {
    87  							return err
    88  						}
    89  					}
    90  				}
    91  			}
    92  		}
    93  	}
    94  
    95  	perms, err := s.Create(inst)
    96  	if err != nil {
    97  		return wrapErrors(err)
    98  	}
    99  	if err = s.SendInvitations(inst, perms); err != nil {
   100  		return wrapErrors(err)
   101  	}
   102  	as := &sharing.APISharing{
   103  		Sharing:     &s,
   104  		Credentials: nil,
   105  		SharedDocs:  nil,
   106  	}
   107  	return jsonapi.Data(c, http.StatusCreated, as, nil)
   108  }
   109  
   110  // PutSharing creates a sharing request (on the recipient's cozy)
   111  func PutSharing(c echo.Context) error {
   112  	inst := middlewares.GetInstance(c)
   113  
   114  	var s sharing.Sharing
   115  	obj, err := jsonapi.Bind(c.Request().Body, &s)
   116  	if err != nil {
   117  		return jsonapi.BadJSON()
   118  	}
   119  	s.SID = obj.ID
   120  	s.ShortcutID = ""
   121  
   122  	if err := s.CreateRequest(inst); err != nil {
   123  		return wrapErrors(err)
   124  	}
   125  
   126  	if c.QueryParam("shortcut") == "true" {
   127  		u := c.QueryParam("url")
   128  		if err := s.CreateShortcut(inst, u, false); err != nil {
   129  			return wrapErrors(err)
   130  		}
   131  	}
   132  
   133  	as := &sharing.APISharing{
   134  		Sharing:     &s,
   135  		Credentials: nil,
   136  		SharedDocs:  nil,
   137  	}
   138  	return jsonapi.Data(c, http.StatusCreated, as, nil)
   139  }
   140  
   141  // jsonapiSharingWithDocs is an helper to send a JSON-API response for a
   142  // sharing with its shared docs
   143  func jsonapiSharingWithDocs(c echo.Context, s *sharing.Sharing) error {
   144  	inst := middlewares.GetInstance(c)
   145  	sharedDocs, err := sharing.GetSharedDocsBySharingIDs(inst, []string{s.SID})
   146  	if err != nil {
   147  		return wrapErrors(err)
   148  	}
   149  	docs := sharedDocs[s.SID]
   150  	as := &sharing.APISharing{
   151  		Sharing:     s,
   152  		Credentials: nil,
   153  		SharedDocs:  docs,
   154  	}
   155  	return jsonapi.Data(c, http.StatusOK, as, nil)
   156  }
   157  
   158  // GetSharing returns the sharing document associated to the given sharingID
   159  // and which documents have been shared.
   160  // The requester must have the permission on at least one doctype declared in
   161  // the sharing document.
   162  func GetSharing(c echo.Context) error {
   163  	inst := middlewares.GetInstance(c)
   164  	sharingID := c.Param("sharing-id")
   165  	s, err := sharing.FindSharing(inst, sharingID)
   166  	if err != nil {
   167  		return wrapErrors(err)
   168  	}
   169  	if err = checkGetPermissions(c, s); err != nil {
   170  		return wrapErrors(err)
   171  	}
   172  	return jsonapiSharingWithDocs(c, s)
   173  }
   174  
   175  // CountNewShortcuts returns the number of shortcuts to a sharing that have not
   176  // been seen.
   177  func CountNewShortcuts(c echo.Context) error {
   178  	if _, err := middlewares.GetPermission(c); err != nil {
   179  		return echo.NewHTTPError(http.StatusForbidden)
   180  	}
   181  
   182  	inst := middlewares.GetInstance(c)
   183  	count, err := sharing.CountNewShortcuts(inst)
   184  	if err != nil {
   185  		return wrapErrors(err)
   186  	}
   187  	body := map[string]interface{}{
   188  		"meta": map[string]int{
   189  			"count": count,
   190  		},
   191  	}
   192  	return c.JSON(http.StatusOK, body)
   193  }
   194  
   195  // GetSharingsInfoByDocType returns, for a given doctype, all the sharing
   196  // information, i.e. the involved sharings and the shared documents
   197  func GetSharingsInfoByDocType(c echo.Context) error {
   198  	inst := middlewares.GetInstance(c)
   199  	docType := c.Param("doctype")
   200  
   201  	sharings, err := sharing.GetSharingsByDocType(inst, docType)
   202  	if err != nil {
   203  		inst.Logger().WithNamespace("sharing").Errorf("GetSharingsByDocType error: %s", err)
   204  		return wrapErrors(err)
   205  	}
   206  	if err := middlewares.AllowWholeType(c, permission.GET, docType); err != nil {
   207  		return wrapErrors(err)
   208  	}
   209  	if len(sharings) == 0 {
   210  		return jsonapi.DataList(c, http.StatusOK, nil, nil)
   211  	}
   212  	sharingIDs := make([]string, 0, len(sharings))
   213  	for sID := range sharings {
   214  		sharingIDs = append(sharingIDs, sID)
   215  	}
   216  	sDocs, err := sharing.GetSharedDocsBySharingIDs(inst, sharingIDs)
   217  	if err != nil {
   218  		inst.Logger().WithNamespace("sharing").Errorf("GetSharedDocsBySharingIDs error: %s", err)
   219  		return wrapErrors(err)
   220  	}
   221  
   222  	res := make([]*sharing.APISharing, 0, len(sharings))
   223  	for sID, s := range sharings {
   224  		as := &sharing.APISharing{
   225  			Sharing:     s,
   226  			SharedDocs:  sDocs[sID],
   227  			Credentials: nil,
   228  		}
   229  		res = append(res, as)
   230  	}
   231  	return sharing.InfoByDocTypeData(c, http.StatusOK, res)
   232  }
   233  
   234  // AnswerSharing is used to exchange credentials between 2 cozys, after the
   235  // recipient has accepted a sharing.
   236  func AnswerSharing(c echo.Context) error {
   237  	inst := middlewares.GetInstance(c)
   238  	sharingID := c.Param("sharing-id")
   239  	s, err := sharing.FindSharing(inst, sharingID)
   240  	if err != nil {
   241  		return wrapErrors(err)
   242  	}
   243  	var creds sharing.APICredentials
   244  	if _, err = jsonapi.Bind(c.Request().Body, &creds); err != nil {
   245  		return jsonapi.BadJSON()
   246  	}
   247  	ac, err := s.ProcessAnswer(inst, &creds)
   248  	if err != nil {
   249  		return wrapErrors(err)
   250  	}
   251  	return jsonapi.Data(c, http.StatusOK, ac, nil)
   252  }
   253  
   254  // ReceivePublicKey is used to receive the public key of a sharing member. It can
   255  // be used when the member has delegated authentication, and didn't have a
   256  // password when they accepted the sharing: this route is called when the user
   257  // choose a password a bit later in cozy pass web.
   258  func ReceivePublicKey(c echo.Context) error {
   259  	inst := middlewares.GetInstance(c)
   260  	sharingID := c.Param("sharing-id")
   261  	s, err := sharing.FindSharing(inst, sharingID)
   262  	if err != nil {
   263  		return wrapErrors(err)
   264  	}
   265  	member, err := requestMember(c, s)
   266  	if err != nil {
   267  		return wrapErrors(err)
   268  	}
   269  	var creds sharing.APICredentials
   270  	if _, err = jsonapi.Bind(c.Request().Body, &creds); err != nil || creds.Bitwarden == nil {
   271  		return jsonapi.BadJSON()
   272  	}
   273  	if err := s.SaveBitwarden(inst, member, creds.Bitwarden); err != nil {
   274  		return wrapErrors(err)
   275  	}
   276  	return c.NoContent(http.StatusNoContent)
   277  }
   278  
   279  // ChangeCozyAddress is called when a Cozy has been moved to a new address.
   280  func ChangeCozyAddress(c echo.Context) error {
   281  	inst := middlewares.GetInstance(c)
   282  	sharingID := c.Param("sharing-id")
   283  	s, err := sharing.FindSharing(inst, sharingID)
   284  	if err != nil {
   285  		return wrapErrors(err)
   286  	}
   287  	var moved sharing.APIMoved
   288  	if _, err = jsonapi.Bind(c.Request().Body, &moved); err != nil {
   289  		return jsonapi.BadJSON()
   290  	}
   291  
   292  	member, err := requestMember(c, s)
   293  	if err != nil {
   294  		return wrapErrors(err)
   295  	}
   296  
   297  	if s.Owner {
   298  		err = s.ChangeMemberAddress(inst, member, moved)
   299  	} else {
   300  		err = s.ChangeOwnerAddress(inst, moved)
   301  	}
   302  	if err != nil {
   303  		return wrapErrors(err)
   304  	}
   305  	return c.NoContent(http.StatusNoContent)
   306  }
   307  
   308  func addRecipientsToSharing(inst *instance.Instance, s *sharing.Sharing, rel *jsonapi.Relationship, readOnly bool) error {
   309  	var err error
   310  	if data, ok := rel.Data.([]interface{}); ok {
   311  		var contactIDs, groupIDs []string
   312  		for _, ref := range data {
   313  			if id, ok := ref.(map[string]interface{})["id"].(string); ok {
   314  				if t, _ := ref.(map[string]interface{})["type"].(string); t == consts.Groups {
   315  					groupIDs = append(groupIDs, id)
   316  				} else {
   317  					contactIDs = append(contactIDs, id)
   318  				}
   319  			}
   320  		}
   321  		if s.Owner {
   322  			err = s.AddGroupsAndContacts(inst, groupIDs, contactIDs, readOnly)
   323  		} else {
   324  			err = s.DelegateAddContactsAndGroups(inst, groupIDs, contactIDs, readOnly)
   325  		}
   326  	}
   327  	return err
   328  }
   329  
   330  // AddRecipients is used to add a member to a sharing
   331  func AddRecipients(c echo.Context) error {
   332  	inst := middlewares.GetInstance(c)
   333  	sharingID := c.Param("sharing-id")
   334  	s, err := sharing.FindSharing(inst, sharingID)
   335  	if err != nil {
   336  		return wrapErrors(err)
   337  	}
   338  	if _, err = checkCreatePermissions(c, s); err != nil {
   339  		return wrapErrors(err)
   340  	}
   341  	var body sharing.Sharing
   342  	obj, err := jsonapi.Bind(c.Request().Body, &body)
   343  	if err != nil {
   344  		return jsonapi.BadJSON()
   345  	}
   346  	if rel, ok := obj.GetRelationship("recipients"); ok {
   347  		if err = addRecipientsToSharing(inst, s, rel, false); err != nil {
   348  			return wrapErrors(err)
   349  		}
   350  	}
   351  	if rel, ok := obj.GetRelationship("read_only_recipients"); ok {
   352  		if err = addRecipientsToSharing(inst, s, rel, true); err != nil {
   353  			return wrapErrors(err)
   354  		}
   355  	}
   356  	return jsonapiSharingWithDocs(c, s)
   357  }
   358  
   359  // AddRecipientsDelegated is used to add members and groups to a sharing on the
   360  // owner's cozy when it's the recipient's cozy that sends the mail invitation.
   361  func AddRecipientsDelegated(c echo.Context) error {
   362  	inst := middlewares.GetInstance(c)
   363  	sharingID := c.Param("sharing-id")
   364  	s, err := sharing.FindSharing(inst, sharingID)
   365  	if err != nil {
   366  		return wrapErrors(err)
   367  	}
   368  	if !s.Owner || !s.Open {
   369  		return echo.NewHTTPError(http.StatusForbidden)
   370  	}
   371  	member, err := requestMember(c, s)
   372  	if err != nil {
   373  		return wrapErrors(err)
   374  	}
   375  	memberIndex := -1
   376  	for i, m := range s.Members {
   377  		if m.Instance == member.Instance {
   378  			memberIndex = i
   379  		}
   380  	}
   381  	if memberIndex == -1 {
   382  		return jsonapi.InternalServerError(sharing.ErrInvalidSharing)
   383  	}
   384  
   385  	var body struct {
   386  		Data struct {
   387  			Type          string `json:"type"`
   388  			ID            string `json:"id"`
   389  			Relationships struct {
   390  				Groups struct {
   391  					Data []sharing.Group `json:"data"`
   392  				} `json:"groups"`
   393  				Recipients struct {
   394  					Data []sharing.Member `json:"data"`
   395  				} `json:"recipients"`
   396  			} `json:"relationships"`
   397  		} `json:"data"`
   398  	}
   399  	if err = json.NewDecoder(c.Request().Body).Decode(&body); err != nil {
   400  		return jsonapi.BadJSON()
   401  	}
   402  
   403  	for _, g := range body.Data.Relationships.Groups.Data {
   404  		g.AddedBy = memberIndex
   405  		s.Groups = append(s.Groups, g)
   406  	}
   407  
   408  	states := make(map[string]string)
   409  	for _, m := range body.Data.Relationships.Recipients.Data {
   410  		state, err := s.AddDelegatedContact(inst, m)
   411  		if err != nil {
   412  			if len(m.Groups) > 0 {
   413  				continue
   414  			}
   415  			return wrapErrors(err)
   416  		}
   417  		// If we have an URL for the Cozy, we can create a shortcut as an invitation
   418  		if m.Instance != "" {
   419  			states[m.Instance] = state
   420  			var perms *permission.Permission
   421  			if s.PreviewPath != "" {
   422  				if perms, err = s.CreatePreviewPermissions(inst); err != nil {
   423  					return wrapErrors(err)
   424  				}
   425  			}
   426  			if err = s.SendInvitations(inst, perms); err != nil {
   427  				return wrapErrors(err)
   428  			}
   429  		} else if m.Email != "" {
   430  			states[m.Email] = state
   431  		}
   432  	}
   433  
   434  	if err := couchdb.UpdateDoc(inst, s); err != nil {
   435  		return wrapErrors(err)
   436  	}
   437  	cloned := s.Clone().(*sharing.Sharing)
   438  	go cloned.NotifyRecipients(inst, nil)
   439  	return c.JSON(http.StatusOK, states)
   440  }
   441  
   442  // AddInvitationDelegated is when a member has been added to a sharing via a
   443  // group, but is invited only later (no email or Cozy instance known when they
   444  // was added).
   445  func AddInvitationDelegated(c echo.Context) error {
   446  	inst := middlewares.GetInstance(c)
   447  	sharingID := c.Param("sharing-id")
   448  	s, err := sharing.FindSharing(inst, sharingID)
   449  	if err != nil {
   450  		return wrapErrors(err)
   451  	}
   452  	if !s.Owner || !s.Open {
   453  		return echo.NewHTTPError(http.StatusForbidden)
   454  	}
   455  
   456  	memberIndex, err := strconv.Atoi(c.Param("member-index"))
   457  	if err != nil || memberIndex <= 0 || memberIndex >= len(s.Members) {
   458  		return jsonapi.InvalidParameter("member-index", errors.New("invalid member-index parameter"))
   459  	}
   460  
   461  	var body struct {
   462  		Data struct {
   463  			Type   string         `json:"type"`
   464  			Member sharing.Member `json:"attributes"`
   465  		}
   466  	}
   467  	if err = json.NewDecoder(c.Request().Body).Decode(&body); err != nil {
   468  		return jsonapi.BadJSON()
   469  	}
   470  
   471  	states := make(map[string]string)
   472  	m := s.Members[memberIndex]
   473  	if m.Status == sharing.MemberStatusMailNotSent {
   474  		m.Instance = body.Data.Member.Instance
   475  		m.Email = body.Data.Member.Email
   476  		state64 := crypto.Base64Encode(crypto.GenerateRandomBytes(sharing.StateLen))
   477  		state := string(state64)
   478  		creds := sharing.Credentials{
   479  			State:  state,
   480  			XorKey: sharing.MakeXorKey(),
   481  		}
   482  		s.Credentials[memberIndex-1] = creds
   483  		s.Members[memberIndex] = m
   484  		// If we have an URL for the Cozy, we can create a shortcut as an invitation
   485  		if m.Instance != "" {
   486  			states[m.Instance] = state
   487  			var perms *permission.Permission
   488  			if s.PreviewPath != "" {
   489  				if perms, err = s.CreatePreviewPermissions(inst); err != nil {
   490  					return wrapErrors(err)
   491  				}
   492  			}
   493  			if err = s.SendInvitations(inst, perms); err != nil {
   494  				return wrapErrors(err)
   495  			}
   496  		} else if m.Email != "" {
   497  			states[m.Email] = state
   498  			s.Members[memberIndex].Status = sharing.MemberStatusReady
   499  		}
   500  	}
   501  
   502  	if err := couchdb.UpdateDoc(inst, s); err != nil {
   503  		return wrapErrors(err)
   504  	}
   505  	cloned := s.Clone().(*sharing.Sharing)
   506  	go cloned.NotifyRecipients(inst, nil)
   507  	return c.JSON(http.StatusOK, states)
   508  }
   509  
   510  // RemoveMemberFromGroup is used to remove a member from a group (delegated).
   511  func RemoveMemberFromGroup(c echo.Context) error {
   512  	inst := middlewares.GetInstance(c)
   513  	sharingID := c.Param("sharing-id")
   514  	s, err := sharing.FindSharing(inst, sharingID)
   515  	if err != nil {
   516  		return wrapErrors(err)
   517  	}
   518  	if !s.Owner || !s.Open {
   519  		return echo.NewHTTPError(http.StatusForbidden)
   520  	}
   521  
   522  	member, err := requestMember(c, s)
   523  	if err != nil {
   524  		return wrapErrors(err)
   525  	}
   526  	addedBy := -1
   527  	for i, m := range s.Members {
   528  		if m.Instance == member.Instance {
   529  			addedBy = i
   530  		}
   531  	}
   532  	if addedBy == -1 {
   533  		return jsonapi.InternalServerError(sharing.ErrInvalidSharing)
   534  	}
   535  
   536  	groupIndex, err := strconv.Atoi(c.Param("group-index"))
   537  	if err != nil || groupIndex < 0 || groupIndex >= len(s.Groups) {
   538  		return jsonapi.InvalidParameter("group-index", errors.New("invalid group-index parameter"))
   539  	}
   540  	if s.Groups[groupIndex].AddedBy != addedBy {
   541  		return echo.NewHTTPError(http.StatusForbidden)
   542  	}
   543  
   544  	memberIndex, err := strconv.Atoi(c.Param("member-index"))
   545  	if err != nil || memberIndex <= 0 || memberIndex >= len(s.Members) {
   546  		return jsonapi.InvalidParameter("member-index", errors.New("invalid member-index parameter"))
   547  	}
   548  
   549  	if err := s.DelegatedRemoveMemberFromGroup(inst, groupIndex, memberIndex); err != nil {
   550  		return wrapErrors(err)
   551  	}
   552  	return c.NoContent(http.StatusNoContent)
   553  }
   554  
   555  // PutRecipients is used to update the members list on the recipients cozy
   556  func PutRecipients(c echo.Context) error {
   557  	inst := middlewares.GetInstance(c)
   558  	sharingID := c.Param("sharing-id")
   559  	s, err := sharing.FindSharing(inst, sharingID)
   560  	if err != nil {
   561  		return wrapErrors(err)
   562  	}
   563  
   564  	if s.Active {
   565  		// If the sharing is active, we check the access token for a permission
   566  		// on the sharing
   567  		if err := hasSharingWritePermissions(c); err != nil {
   568  			return err
   569  		}
   570  	} else {
   571  		// If there is no synchronization, it means that we have a shortcut for
   572  		// this sharing, and we can check the sharecode.
   573  		token := middlewares.GetRequestToken(c)
   574  		sharecode, err := s.GetSharecodeFromShortcut(inst)
   575  		if err != nil || token != sharecode {
   576  			return middlewares.ErrForbidden
   577  		}
   578  	}
   579  
   580  	var params sharing.PutRecipientsParams
   581  	if err = json.NewDecoder(c.Request().Body).Decode(&params); err != nil {
   582  		return wrapErrors(err)
   583  	}
   584  	if err = s.UpdateRecipients(inst, params); err != nil {
   585  		return wrapErrors(err)
   586  	}
   587  	return c.NoContent(http.StatusNoContent)
   588  }
   589  
   590  func renderAlreadyAccepted(c echo.Context, inst *instance.Instance, cozyURL string) error {
   591  	return c.Render(http.StatusBadRequest, "error.html", echo.Map{
   592  		"Domain":       inst.ContextualDomain(),
   593  		"ContextName":  inst.ContextName,
   594  		"Locale":       inst.Locale,
   595  		"Title":        inst.TemplateTitle(),
   596  		"Favicon":      middlewares.Favicon(inst),
   597  		"ErrorTitle":   "Error Sharing already accepted Title",
   598  		"Error":        "Error Sharing already accepted",
   599  		"Button":       "Error Sharing already accepted Button",
   600  		"ButtonURL":    cozyURL,
   601  		"SupportEmail": inst.SupportEmailAddress(),
   602  	})
   603  }
   604  
   605  func renderDiscoveryForm(c echo.Context, inst *instance.Instance, code int, sharingID, state, sharecode string, m *sharing.Member) error {
   606  	publicName, _ := settings.PublicName(inst)
   607  	fqdn := strings.TrimPrefix(m.Instance, "https://")
   608  	slug, domain := "", consts.KnownFlatDomains[0]
   609  	if context, ok := inst.SettingsContext(); ok {
   610  		if d, ok := context["sharing_domain"].(string); ok {
   611  			domain = d
   612  		}
   613  	}
   614  	if strings.HasPrefix(m.Instance, "http://") {
   615  		slug, domain = m.Instance, ""
   616  	} else if parts := strings.SplitN(fqdn, ".", 2); len(parts) == 2 {
   617  		slug, domain = parts[0], parts[1]
   618  	}
   619  	return c.Render(code, "sharing_discovery.html", echo.Map{
   620  		"Domain":          inst.ContextualDomain(),
   621  		"ContextName":     inst.ContextName,
   622  		"Locale":          inst.Locale,
   623  		"Title":           inst.TemplateTitle(),
   624  		"Favicon":         middlewares.Favicon(inst),
   625  		"PublicName":      publicName,
   626  		"RecipientSlug":   slug,
   627  		"RecipientDomain": domain,
   628  		"SharingID":       sharingID,
   629  		"State":           state,
   630  		"ShareCode":       sharecode,
   631  		"URLError":        code == http.StatusBadRequest,
   632  		"NotEmailError":   code == http.StatusPreconditionFailed,
   633  	})
   634  }
   635  
   636  // GetDiscovery displays a form where a recipient can give the address of their
   637  // cozy instance
   638  func GetDiscovery(c echo.Context) error {
   639  	inst := middlewares.GetInstance(c)
   640  	sharingID := c.Param("sharing-id")
   641  	state := c.QueryParam("state")
   642  	sharecode := c.FormValue("sharecode")
   643  
   644  	s, err := sharing.FindSharing(inst, sharingID)
   645  	if err != nil {
   646  		return c.Render(http.StatusBadRequest, "error.html", echo.Map{
   647  			"Domain":       inst.ContextualDomain(),
   648  			"ContextName":  inst.ContextName,
   649  			"Locale":       inst.Locale,
   650  			"Title":        inst.TemplateTitle(),
   651  			"Favicon":      middlewares.Favicon(inst),
   652  			"Illustration": "/images/generic-error.svg",
   653  			"Error":        "Error Invalid sharing",
   654  			"SupportEmail": inst.SupportEmailAddress(),
   655  		})
   656  	}
   657  
   658  	m := &sharing.Member{}
   659  	if s.Owner {
   660  		if sharecode != "" {
   661  			m, err = s.FindMemberBySharecode(inst, sharecode)
   662  		} else {
   663  			m, err = s.FindMemberByState(state)
   664  		}
   665  		if err != nil || m.Status == sharing.MemberStatusRevoked {
   666  			return c.Render(http.StatusBadRequest, "error.html", echo.Map{
   667  				"Domain":       inst.ContextualDomain(),
   668  				"ContextName":  inst.ContextName,
   669  				"Locale":       inst.Locale,
   670  				"Title":        inst.TemplateTitle(),
   671  				"Favicon":      middlewares.Favicon(inst),
   672  				"Illustration": "/images/generic-error.svg",
   673  				"Error":        "Error Invalid sharing",
   674  				"SupportEmail": inst.SupportEmailAddress(),
   675  			})
   676  		}
   677  		if m.Status != sharing.MemberStatusMailNotSent &&
   678  			m.Status != sharing.MemberStatusPendingInvitation &&
   679  			m.Status != sharing.MemberStatusSeen {
   680  			return renderAlreadyAccepted(c, inst, m.Instance)
   681  		}
   682  	}
   683  
   684  	if m.Instance != "" {
   685  		if m.Status != sharing.MemberStatusSeen {
   686  			err = s.RegisterCozyURL(inst, m, m.Instance)
   687  		}
   688  		if err == nil {
   689  			redirectURL, err := m.GenerateOAuthURL(s)
   690  			if err == nil {
   691  				return c.Redirect(http.StatusFound, redirectURL)
   692  			}
   693  		}
   694  	}
   695  
   696  	return renderDiscoveryForm(c, inst, http.StatusOK, sharingID, state, sharecode, m)
   697  }
   698  
   699  // PostDiscovery is called when the recipient has given its Cozy URL. Either an
   700  // error is returned or the recipient will be redirected to their cozy.
   701  //
   702  // Note: we don't have an anti-CSRF system, we rely on shareCode being secret.
   703  func PostDiscovery(c echo.Context) error {
   704  	inst := middlewares.GetInstance(c)
   705  	sharingID := c.Param("sharing-id")
   706  	state := c.FormValue("state")
   707  	sharecode := c.FormValue("sharecode")
   708  	cozyURL := c.FormValue("url")
   709  	if cozyURL == "" {
   710  		cozyURL = c.FormValue("slug")
   711  	}
   712  	cozyURL = strings.TrimSuffix(cozyURL, ".")
   713  	if !strings.HasPrefix(cozyURL, "http://") && !strings.HasPrefix(cozyURL, "https://") {
   714  		cozyURL = "https://" + cozyURL
   715  	}
   716  	if domain := c.FormValue("domain"); domain != "" && !strings.Contains(cozyURL, ".") {
   717  		if domain == "mycosy.cloud" {
   718  			domain = "mycozy.cloud"
   719  		}
   720  		cozyURL = cozyURL + "." + domain
   721  	}
   722  	cozyURL = ClearAppInURL(cozyURL)
   723  
   724  	s, err := sharing.FindSharing(inst, sharingID)
   725  	if err != nil {
   726  		return wrapErrors(err)
   727  	}
   728  
   729  	var redirectURL, email string
   730  
   731  	if s.Owner {
   732  		var member *sharing.Member
   733  		if sharecode != "" {
   734  			member, err = s.FindMemberBySharecode(inst, sharecode)
   735  			if err != nil {
   736  				return wrapErrors(err)
   737  			}
   738  		} else {
   739  			member, err = s.FindMemberByState(state)
   740  			if err != nil {
   741  				return wrapErrors(err)
   742  			}
   743  		}
   744  		if strings.Contains(cozyURL, "@") {
   745  			return renderDiscoveryForm(c, inst, http.StatusPreconditionFailed, sharingID, state, sharecode, member)
   746  		}
   747  		email = member.Email
   748  		if err = s.RegisterCozyURL(inst, member, cozyURL); err != nil {
   749  			if c.Request().Header.Get(echo.HeaderAccept) == echo.MIMEApplicationJSON {
   750  				return c.JSON(http.StatusBadRequest, echo.Map{"error": err.Error()})
   751  			}
   752  			if errors.Is(err, sharing.ErrAlreadyAccepted) {
   753  				return renderAlreadyAccepted(c, inst, cozyURL)
   754  			}
   755  			return renderDiscoveryForm(c, inst, http.StatusBadRequest, sharingID, state, sharecode, member)
   756  		}
   757  		redirectURL, err = member.GenerateOAuthURL(s)
   758  		if err != nil {
   759  			return wrapErrors(err)
   760  		}
   761  		sharing.PersistInstanceURL(inst, member.Email, member.Instance)
   762  	} else {
   763  		redirectURL, err = s.DelegateDiscovery(inst, state, cozyURL)
   764  		if err != nil {
   765  			if errors.Is(err, sharing.ErrInvalidURL) {
   766  				if c.Request().Header.Get(echo.HeaderAccept) == echo.MIMEApplicationJSON {
   767  					return c.JSON(http.StatusBadRequest, echo.Map{"error": err.Error()})
   768  				}
   769  				return renderDiscoveryForm(c, inst, http.StatusBadRequest, sharingID, state, sharecode, &sharing.Member{})
   770  			}
   771  			return wrapErrors(err)
   772  		}
   773  	}
   774  
   775  	if c.Request().Header.Get(echo.HeaderAccept) == echo.MIMEApplicationJSON {
   776  		m := echo.Map{"redirect": redirectURL}
   777  		if email != "" {
   778  			m["email"] = email
   779  		}
   780  		return c.JSON(http.StatusOK, m)
   781  	}
   782  	return c.Redirect(http.StatusFound, redirectURL)
   783  }
   784  
   785  // GetPreviewURL returns the preview URL for the member identified by their
   786  // state parameter.
   787  func GetPreviewURL(c echo.Context) error {
   788  	inst := middlewares.GetInstance(c)
   789  	s, err := sharing.FindSharing(inst, c.Param("sharing-id"))
   790  	if err != nil {
   791  		return wrapErrors(err)
   792  	}
   793  	if !s.Owner {
   794  		return wrapErrors(sharing.ErrInvalidSharing)
   795  	}
   796  
   797  	args := struct {
   798  		State string `json:"state"`
   799  	}{}
   800  	if err := c.Bind(&args); err != nil {
   801  		return wrapErrors(err)
   802  	}
   803  	if args.State == "" {
   804  		return jsonapi.BadJSON()
   805  	}
   806  	m, err := s.FindMemberByState(args.State)
   807  	if err != nil {
   808  		return wrapErrors(err)
   809  	}
   810  
   811  	if m.Status != sharing.MemberStatusMailNotSent &&
   812  		m.Status != sharing.MemberStatusPendingInvitation &&
   813  		m.Status != sharing.MemberStatusSeen {
   814  		return wrapErrors(sharing.ErrAlreadyAccepted)
   815  	}
   816  
   817  	perm, err := permission.GetForSharePreview(inst, s.SID)
   818  	if err != nil {
   819  		return wrapErrors(err)
   820  	}
   821  	previewURL := m.InvitationLink(inst, s, args.State, perm)
   822  	return c.JSON(http.StatusOK, map[string]string{"url": previewURL})
   823  }
   824  
   825  // GetAvatar returns the avatar of the given member of the sharing.
   826  func GetAvatar(c echo.Context) error {
   827  	inst := middlewares.GetInstance(c)
   828  	sharingID := c.Param("sharing-id")
   829  	s, err := sharing.FindSharing(inst, sharingID)
   830  	if err != nil {
   831  		return wrapErrors(err)
   832  	}
   833  
   834  	index, err := strconv.Atoi(c.Param("index"))
   835  	if err != nil {
   836  		return jsonapi.InvalidParameter("index", err)
   837  	}
   838  	if index > len(s.Members) {
   839  		return jsonapi.NotFound(errors.New("member not found"))
   840  	}
   841  	m := s.Members[index]
   842  
   843  	// Use the local avatar
   844  	if m.Instance == "" || m.Instance == inst.PageURL("", nil) {
   845  		return localAvatar(c, m)
   846  	}
   847  
   848  	// Use the public avatar from the member's instance
   849  	res, err := safehttp.DefaultClient.Get(m.Instance + "/public/avatar?fallback=404")
   850  	if err != nil {
   851  		return localAvatar(c, m)
   852  	}
   853  	defer res.Body.Close()
   854  	if res.StatusCode == http.StatusNotFound && c.QueryParam("fallback") != "404" {
   855  		return localAvatar(c, m)
   856  	}
   857  	return c.Stream(res.StatusCode, res.Header.Get(echo.HeaderContentType), res.Body)
   858  }
   859  
   860  func localAvatar(c echo.Context, m sharing.Member) error {
   861  	name := m.PublicName
   862  	if name == "" {
   863  		name = strings.Split(m.Email, "@")[0]
   864  	}
   865  	name = strings.ToUpper(name)
   866  	var options []avatar.Options
   867  	if m.Status == sharing.MemberStatusMailNotSent ||
   868  		m.Status == sharing.MemberStatusPendingInvitation {
   869  		options = append(options, avatar.GreyBackground)
   870  	}
   871  	img, mime, err := config.Avatars().GenerateInitials(name, options...)
   872  	if err != nil {
   873  		return wrapErrors(err)
   874  	}
   875  	return c.Blob(http.StatusOK, mime, img)
   876  }
   877  
   878  // Routes sets the routing for the sharing service
   879  func Routes(router *echo.Group) {
   880  	// Create a sharing
   881  	router.POST("/", CreateSharing)        // On the sharer
   882  	router.PUT("/:sharing-id", PutSharing) // On a recipient
   883  	router.GET("/:sharing-id", GetSharing)
   884  	router.POST("/:sharing-id/answer", AnswerSharing)
   885  
   886  	// Managing recipients
   887  	router.POST("/:sharing-id/recipients", AddRecipients)
   888  	router.PUT("/:sharing-id/recipients", PutRecipients)
   889  	router.DELETE("/:sharing-id/recipients", RevokeSharing)          // On the sharer
   890  	router.DELETE("/:sharing-id/recipients/:index", RevokeRecipient) // On the sharer
   891  	router.DELETE("/:sharing-id/groups/:index", RevokeGroup)         // On the sharer
   892  	router.POST("/:sharing-id/recipients/self/moved", ChangeCozyAddress)
   893  	router.POST("/:sharing-id/recipients/:index/readonly", AddReadOnly)                                      // On the sharer
   894  	router.POST("/:sharing-id/recipients/self/readonly", DowngradeToReadOnly, checkSharingWritePermissions)  // On the recipient
   895  	router.DELETE("/:sharing-id/recipients/:index/readonly", RemoveReadOnly)                                 // On the sharer
   896  	router.DELETE("/:sharing-id/recipients/self/readonly", UpgradeToReadWrite, checkSharingWritePermissions) // On the recipient
   897  	router.DELETE("/:sharing-id", RevocationRecipientNotif, checkSharingWritePermissions)                    // On the recipient
   898  	router.DELETE("/:sharing-id/recipients/self", RevokeRecipientBySelf)                                     // On the recipient
   899  	router.DELETE("/:sharing-id/answer", RevocationOwnerNotif, checkSharingWritePermissions)                 // On the sharer
   900  	router.POST("/:sharing-id/public-key", ReceivePublicKey)
   901  
   902  	// Delegated routes for open sharing
   903  	router.POST("/:sharing-id/recipients/delegated", AddRecipientsDelegated, checkSharingWritePermissions)
   904  	router.POST("/:sharing-id/members/:index/invitation", AddInvitationDelegated, checkSharingWritePermissions)
   905  	router.DELETE("/:sharing-id/groups/:group-index/:member-index", RemoveMemberFromGroup, checkSharingWritePermissions)
   906  
   907  	// Misc
   908  	router.GET("/news", CountNewShortcuts)
   909  	router.GET("/doctype/:doctype", GetSharingsInfoByDocType)
   910  	router.GET("/:sharing-id/recipients/:index/avatar", GetAvatar)
   911  
   912  	// Register the URL of their Cozy for recipients
   913  	router.GET("/:sharing-id/discovery", GetDiscovery)
   914  	router.POST("/:sharing-id/discovery", PostDiscovery)
   915  	router.POST("/:sharing-id/preview-url", GetPreviewURL)
   916  
   917  	// Replicator routes
   918  	replicatorRoutes(router)
   919  }
   920  
   921  func extractSlugFromSourceID(sourceID string) (string, error) {
   922  	parts := strings.SplitN(sourceID, "/", 2)
   923  	if len(parts) < 2 {
   924  		return "", jsonapi.BadRequest(errors.New("Invalid request"))
   925  	}
   926  	slug := parts[1]
   927  	return slug, nil
   928  }
   929  
   930  // checkCreatePermissions checks the sharer's token has all the permissions
   931  // matching the ones defined in the sharing document
   932  func checkCreatePermissions(c echo.Context, s *sharing.Sharing) (string, error) {
   933  	requestPerm, err := middlewares.GetPermission(c)
   934  	if err != nil {
   935  		return "", err
   936  	}
   937  	if requestPerm.Type != permission.TypeWebapp &&
   938  		requestPerm.Type != permission.TypeOauth &&
   939  		requestPerm.Type != permission.TypeCLI {
   940  		return "", permission.ErrInvalidAudience
   941  	}
   942  	for _, r := range s.Rules {
   943  		pr := permission.Rule{
   944  			Title:    r.Title,
   945  			Type:     r.DocType,
   946  			Verbs:    permission.ALL,
   947  			Selector: r.Selector,
   948  			Values:   r.Values,
   949  		}
   950  		if !requestPerm.Permissions.RuleInSubset(pr) {
   951  			return "", echo.NewHTTPError(http.StatusForbidden)
   952  		}
   953  	}
   954  	if requestPerm.Type == permission.TypeCLI {
   955  		return "", nil
   956  	}
   957  	if requestPerm.Type == permission.TypeOauth {
   958  		if requestPerm.Client != nil {
   959  			oauthClient := requestPerm.Client.(*oauth.Client)
   960  			if slug := oauth.GetLinkedAppSlug(oauthClient.SoftwareID); slug != "" {
   961  				return slug, nil
   962  			}
   963  		}
   964  		return "", nil
   965  	}
   966  	return extractSlugFromSourceID(requestPerm.SourceID)
   967  }
   968  
   969  // checkGetPermissions checks the requester's token has at least one doctype
   970  // permission declared in the rules of the sharing document
   971  func checkGetPermissions(c echo.Context, s *sharing.Sharing) error {
   972  	requestPerm, err := middlewares.GetPermission(c)
   973  	if err != nil {
   974  		return err
   975  	}
   976  
   977  	if requestPerm.SourceID == consts.Sharings+"/"+s.SID {
   978  		if requestPerm.Type == permission.TypeSharePreview ||
   979  			requestPerm.Type == permission.TypeShareInteract {
   980  			return nil
   981  		}
   982  	}
   983  	if requestPerm.Type != permission.TypeWebapp &&
   984  		requestPerm.Type != permission.TypeOauth &&
   985  		requestPerm.Type != permission.TypeCLI {
   986  		return permission.ErrInvalidAudience
   987  	}
   988  
   989  	for _, r := range s.Rules {
   990  		pr := permission.Rule{
   991  			Title:    r.Title,
   992  			Type:     r.DocType,
   993  			Verbs:    permission.Verbs(permission.GET),
   994  			Selector: r.Selector,
   995  			Values:   r.Values,
   996  		}
   997  		if requestPerm.Permissions.RuleInSubset(pr) {
   998  			return nil
   999  		}
  1000  	}
  1001  	return echo.NewHTTPError(http.StatusForbidden)
  1002  }
  1003  
  1004  // ClearAppInURL will remove the app slug from the URL of a Cozy.
  1005  // Example: https://john-drive.mycozy.cloud/ -> https://john.mycozy.cloud/
  1006  func ClearAppInURL(cozyURL string) string {
  1007  	u, err := url.Parse(cozyURL)
  1008  	if err != nil {
  1009  		return cozyURL
  1010  	}
  1011  	knownDomain := false
  1012  	for _, domain := range consts.KnownFlatDomains {
  1013  		if strings.HasSuffix(u.Host, domain) {
  1014  			knownDomain = true
  1015  			break
  1016  		}
  1017  	}
  1018  	if !knownDomain {
  1019  		return cozyURL
  1020  	}
  1021  	parts := strings.SplitN(u.Host, ".", 2)
  1022  	sub := parts[0]
  1023  	domain := parts[1]
  1024  	parts = strings.SplitN(sub, "-", 2)
  1025  	u.Host = parts[0] + "." + domain
  1026  	return u.String()
  1027  }
  1028  
  1029  // wrapErrors returns a formatted error
  1030  func wrapErrors(err error) error {
  1031  	if merr, ok := err.(*multierror.Error); ok {
  1032  		err = merr.WrappedErrors()[0]
  1033  	}
  1034  	switch err {
  1035  	case contact.ErrNoMailAddress:
  1036  		return jsonapi.InvalidAttribute("recipients", err)
  1037  	case sharing.ErrNoRecipients, sharing.ErrNoRules:
  1038  		return jsonapi.BadRequest(err)
  1039  	case sharing.ErrTooManyMembers:
  1040  		return jsonapi.BadRequest(err)
  1041  	case sharing.ErrInvalidURL:
  1042  		return jsonapi.InvalidParameter("url", err)
  1043  	case sharing.ErrInvalidSharing, sharing.ErrInvalidRule:
  1044  		return jsonapi.BadRequest(err)
  1045  	case sharing.ErrMemberNotFound:
  1046  		return jsonapi.NotFound(err)
  1047  	case sharing.ErrInvitationNotSent:
  1048  		return jsonapi.BadRequest(err)
  1049  	case sharing.ErrRequestFailed:
  1050  		return jsonapi.BadGateway(err)
  1051  	case sharing.ErrNoOAuthClient:
  1052  		return jsonapi.BadRequest(err)
  1053  	case sharing.ErrMissingID, sharing.ErrMissingRev:
  1054  		return jsonapi.BadRequest(err)
  1055  	case sharing.ErrInternalServerError:
  1056  		return jsonapi.InternalServerError(err)
  1057  	case sharing.ErrMissingFileMetadata:
  1058  		return jsonapi.NotFound(err)
  1059  	case sharing.ErrFolderNotFound:
  1060  		return jsonapi.NotFound(err)
  1061  	case sharing.ErrSafety:
  1062  		return jsonapi.BadRequest(err)
  1063  	case sharing.ErrAlreadyAccepted:
  1064  		return jsonapi.Conflict(err)
  1065  	case vfs.ErrInvalidHash:
  1066  		return jsonapi.InvalidParameter("md5sum", err)
  1067  	case vfs.ErrContentLengthMismatch:
  1068  		return jsonapi.PreconditionFailed("Content-Length", err)
  1069  	case vfs.ErrConflict:
  1070  		return jsonapi.Conflict(err)
  1071  	case vfs.ErrFileTooBig, vfs.ErrMaxFileSize:
  1072  		return jsonapi.Errorf(http.StatusRequestEntityTooLarge, "%s", err)
  1073  	case permission.ErrExpiredToken:
  1074  		return jsonapi.BadRequest(err)
  1075  	case sharing.ErrGroupCannotBeAddedTwice, sharing.ErrMemberAlreadyAdded, sharing.ErrMemberAlreadyInGroup:
  1076  		return jsonapi.BadRequest(err)
  1077  	}
  1078  	logger.WithNamespace("sharing").Warnf("Not wrapped error: %s", err)
  1079  	return err
  1080  }