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

     1  // Package permissions is the HTTP handlers for managing the permissions on a
     2  // Cozy (creating a share by link for example).
     3  package permissions
     4  
     5  import (
     6  	"encoding/json"
     7  	"fmt"
     8  	"net/http"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/cozy/cozy-stack/model/oauth"
    14  	"github.com/cozy/cozy-stack/model/permission"
    15  	"github.com/cozy/cozy-stack/model/sharing"
    16  	"github.com/cozy/cozy-stack/pkg/consts"
    17  	"github.com/cozy/cozy-stack/pkg/couchdb"
    18  	"github.com/cozy/cozy-stack/pkg/crypto"
    19  	"github.com/cozy/cozy-stack/pkg/jsonapi"
    20  	"github.com/cozy/cozy-stack/pkg/metadata"
    21  	"github.com/cozy/cozy-stack/pkg/prefixer"
    22  	"github.com/cozy/cozy-stack/web/middlewares"
    23  	"github.com/justincampbell/bigduration"
    24  	"github.com/labstack/echo/v4"
    25  )
    26  
    27  // ErrPatchCodeOrSet is returned when an attempt is made to patch both
    28  // code & set in one request
    29  var ErrPatchCodeOrSet = echo.NewHTTPError(http.StatusBadRequest,
    30  	"The patch doc should have property 'codes' or 'permissions', not both")
    31  
    32  // ContextPermissionSet is the key used in echo context to store permissions set
    33  const ContextPermissionSet = "permissions_set"
    34  
    35  // ContextClaims is the key used in echo context to store claims
    36  const ContextClaims = "token_claims"
    37  
    38  // APIPermission is the struct that will be used to serialized a permission to
    39  // JSON-API
    40  type APIPermission struct {
    41  	*permission.Permission
    42  	included []jsonapi.Object
    43  }
    44  
    45  // MarshalJSON implements jsonapi.Doc
    46  func (p *APIPermission) MarshalJSON() ([]byte, error) {
    47  	return json.Marshal(p.Permission)
    48  }
    49  
    50  // Relationships implements jsonapi.Doc
    51  func (p *APIPermission) Relationships() jsonapi.RelationshipMap { return nil }
    52  
    53  // Included implements jsonapi.Doc
    54  func (p *APIPermission) Included() []jsonapi.Object { return p.included }
    55  
    56  // Links implements jsonapi.Doc
    57  func (p *APIPermission) Links() *jsonapi.LinksList {
    58  	links := &jsonapi.LinksList{Self: "/permissions/" + p.PID}
    59  	parts := strings.SplitN(p.SourceID, "/", 2)
    60  	if parts[0] == consts.Sharings {
    61  		links.Related = "/sharings/" + parts[1]
    62  	}
    63  	return links
    64  }
    65  
    66  type apiMember struct {
    67  	*sharing.Member
    68  }
    69  
    70  func (m *apiMember) ID() string                             { return "" }
    71  func (m *apiMember) Rev() string                            { return "" }
    72  func (m *apiMember) SetID(id string)                        {}
    73  func (m *apiMember) SetRev(rev string)                      {}
    74  func (m *apiMember) DocType() string                        { return consts.SharingsMembers }
    75  func (m *apiMember) Clone() couchdb.Doc                     { cloned := *m; return &cloned }
    76  func (m *apiMember) Relationships() jsonapi.RelationshipMap { return nil }
    77  func (m *apiMember) Included() []jsonapi.Object             { return nil }
    78  func (m *apiMember) Links() *jsonapi.LinksList              { return nil }
    79  
    80  type getPermsFunc func(db prefixer.Prefixer, id string) (*permission.Permission, error)
    81  
    82  func displayPermissions(c echo.Context) error {
    83  	doc, err := middlewares.GetPermission(c)
    84  	if err != nil {
    85  		return err
    86  	}
    87  
    88  	// Include the sharing member (when relevant)
    89  	var included []jsonapi.Object
    90  	if doc.Type == permission.TypeSharePreview || doc.Type == permission.TypeShareInteract {
    91  		inst := middlewares.GetInstance(c)
    92  		sharingID := strings.TrimPrefix(doc.SourceID, consts.Sharings+"/")
    93  		if s, err := sharing.FindSharing(inst, sharingID); err == nil {
    94  			sharecode := middlewares.GetRequestToken(c)
    95  			if member, err := s.FindMemberByCode(doc, sharecode); err == nil {
    96  				included = []jsonapi.Object{&apiMember{member}}
    97  			}
    98  		}
    99  	}
   100  
   101  	// XXX hides the codes and password hash in the response
   102  	doc.Codes = nil
   103  	doc.ShortCodes = nil
   104  	if doc.Password != nil {
   105  		doc.Password = true
   106  	}
   107  	return jsonapi.Data(c, http.StatusOK, &APIPermission{doc, included}, nil)
   108  }
   109  
   110  func createPermission(c echo.Context) error {
   111  	instance := middlewares.GetInstance(c)
   112  	names := strings.Split(c.QueryParam("codes"), ",")
   113  	ttl := c.QueryParam("ttl")
   114  	tiny, _ := strconv.ParseBool(c.QueryParam("tiny"))
   115  
   116  	parent, err := middlewares.GetPermission(c)
   117  	if err != nil {
   118  		return err
   119  	}
   120  
   121  	var slug string
   122  	sourceID := parent.SourceID
   123  	// Check if the permission is linked to an OAuth Client
   124  	if parent.Client != nil {
   125  		oauthClient := parent.Client.(*oauth.Client)
   126  		if slug = oauth.GetLinkedAppSlug(oauthClient.SoftwareID); slug != "" {
   127  			// Changing the sourceID from the OAuth clientID to the classic
   128  			// io.cozy.apps/slug one
   129  			sourceID = consts.Apps + "/" + slug
   130  		}
   131  	}
   132  
   133  	var subdoc permission.Permission
   134  	if _, err = jsonapi.Bind(c.Request().Body, &subdoc); err != nil {
   135  		return err
   136  	}
   137  
   138  	var expiresAt *time.Time
   139  	if ttl != "" {
   140  		if d, errd := bigduration.ParseDuration(ttl); errd == nil {
   141  			ex := time.Now().Add(d)
   142  			expiresAt = &ex
   143  			if d.Hours() > 1.0 && tiny {
   144  				instance.Logger().Info("Tiny can not be set to true since duration > 1h")
   145  				tiny = false
   146  			}
   147  		}
   148  	}
   149  
   150  	var codes map[string]string
   151  	var shortcodes map[string]string
   152  
   153  	if names != nil {
   154  		codes = make(map[string]string, len(names))
   155  		shortcodes = make(map[string]string, len(names))
   156  		for _, name := range names {
   157  			longcode, err := instance.CreateShareCode(name)
   158  			shortcode := createShortCode(tiny)
   159  
   160  			codes[name] = longcode
   161  			shortcodes[name] = shortcode
   162  			if err != nil {
   163  				return err
   164  			}
   165  		}
   166  	}
   167  
   168  	if parent == nil {
   169  		return echo.NewHTTPError(http.StatusUnauthorized, "no parent")
   170  	}
   171  
   172  	// Getting the slug from the token if it has not been retrieved before
   173  	// with the linkedapp
   174  	if slug == "" {
   175  		claims := c.Get("claims").(permission.Claims)
   176  		slug = claims.Subject
   177  	}
   178  
   179  	// Handles the metadata part
   180  	md, err := metadata.NewWithApp(slug, "", permission.DocTypeVersion)
   181  	if err != nil {
   182  		return err
   183  	}
   184  
   185  	// Adding metadata if it does not exist
   186  	if subdoc.Metadata == nil {
   187  		subdoc.Metadata = md
   188  	} else { // Otherwise, ensure we have all the needed fields
   189  		subdoc.Metadata.EnsureCreatedFields(md)
   190  	}
   191  
   192  	pdoc, err := permission.CreateShareSet(instance, parent, sourceID, codes, shortcodes, subdoc, expiresAt)
   193  	if err != nil {
   194  		return err
   195  	}
   196  
   197  	// Don't send the password hash to the client
   198  	if pdoc.Password != nil {
   199  		pdoc.Password = true
   200  	}
   201  
   202  	return jsonapi.Data(c, http.StatusOK, &APIPermission{pdoc, nil}, nil)
   203  }
   204  
   205  func createShortCode(tiny bool) string {
   206  	if tiny {
   207  		return crypto.GenerateRandomSixDigits()
   208  	}
   209  	return crypto.GenerateRandomString(consts.ShortCodeLen)
   210  }
   211  
   212  const (
   213  	defaultPermissionsByDoctype = 30
   214  	maxPermissionsByDoctype     = 100
   215  )
   216  
   217  func listPermissionsByDoctype(c echo.Context, route, permType string) error {
   218  	ins := middlewares.GetInstance(c)
   219  	doctype := c.Param("doctype")
   220  	if doctype == "" {
   221  		return jsonapi.NewError(http.StatusBadRequest, "Missing doctype")
   222  	}
   223  
   224  	current, err := middlewares.GetPermission(c)
   225  	if err != nil {
   226  		return err
   227  	}
   228  
   229  	if !current.Permissions.AllowWholeType(http.MethodGet, doctype) {
   230  		return jsonapi.NewError(http.StatusForbidden,
   231  			"You need GET permission on whole type to list its permissions")
   232  	}
   233  
   234  	cursor, err := jsonapi.ExtractPaginationCursor(c, defaultPermissionsByDoctype, maxPermissionsByDoctype)
   235  	if err != nil {
   236  		return err
   237  	}
   238  
   239  	perms, err := permission.GetPermissionsByDoctype(ins, permType, doctype, cursor)
   240  	if err != nil {
   241  		return err
   242  	}
   243  
   244  	links := &jsonapi.LinksList{}
   245  	if cursor.HasMore() {
   246  		params, err := jsonapi.PaginationCursorToParams(cursor)
   247  		if err != nil {
   248  			return err
   249  		}
   250  		links.Next = fmt.Sprintf("/permissions/doctype/%s/%s?%s",
   251  			doctype, route, params.Encode())
   252  	}
   253  
   254  	out := make([]jsonapi.Object, len(perms))
   255  	for i := range perms {
   256  		perm := &perms[i]
   257  		if perm.Password != nil {
   258  			perm.Password = true
   259  		}
   260  		out[i] = &APIPermission{perm, nil}
   261  	}
   262  
   263  	return jsonapi.DataList(c, http.StatusOK, out, links)
   264  }
   265  
   266  func listByLinkPermissionsByDoctype(c echo.Context) error {
   267  	return listPermissionsByDoctype(c, "shared-by-link", permission.TypeShareByLink)
   268  }
   269  
   270  type refAndVerb struct {
   271  	ID      string              `json:"id"`
   272  	DocType string              `json:"type"`
   273  	Verbs   *permission.VerbSet `json:"verbs"`
   274  }
   275  
   276  func listPermissions(c echo.Context) error {
   277  	instance := middlewares.GetInstance(c)
   278  
   279  	references, err := jsonapi.BindRelations(c.Request())
   280  	if err != nil {
   281  		return err
   282  	}
   283  	ids := make(map[string][]string)
   284  	for _, ref := range references {
   285  		idSlice, ok := ids[ref.Type]
   286  		if !ok {
   287  			idSlice = []string{}
   288  		}
   289  		ids[ref.Type] = append(idSlice, ref.ID)
   290  	}
   291  
   292  	var out []refAndVerb
   293  	for doctype, idSlice := range ids {
   294  		result, err2 := permission.GetPermissionsForIDs(instance, doctype, idSlice)
   295  		if err2 != nil {
   296  			return err2
   297  		}
   298  		for id, verbs := range result {
   299  			out = append(out, refAndVerb{id, doctype, verbs})
   300  		}
   301  	}
   302  
   303  	data, err := json.Marshal(out)
   304  	if err != nil {
   305  		return err
   306  	}
   307  	doc := jsonapi.Document{
   308  		Data: (*json.RawMessage)(&data),
   309  	}
   310  	resp := c.Response()
   311  	resp.Header().Set(echo.HeaderContentType, jsonapi.ContentType)
   312  	resp.WriteHeader(http.StatusOK)
   313  	return json.NewEncoder(resp).Encode(doc)
   314  }
   315  
   316  func showPermissions(c echo.Context) error {
   317  	inst := middlewares.GetInstance(c)
   318  
   319  	current, err := middlewares.GetPermission(c)
   320  	if err != nil {
   321  		return err
   322  	}
   323  
   324  	doc, err := permission.GetByID(inst, c.Param("permdocid"))
   325  	if err != nil {
   326  		return err
   327  	}
   328  
   329  	if doc.ID() != current.ID() && doc.SourceID != current.SourceID {
   330  		if err := middlewares.AllowMaximal(c); err != nil {
   331  			return middlewares.ErrForbidden
   332  		}
   333  	}
   334  
   335  	// XXX hides the codes and password hash in the response
   336  	doc.Codes = nil
   337  	doc.ShortCodes = nil
   338  	if doc.Password != nil {
   339  		doc.Password = true
   340  	}
   341  	return jsonapi.Data(c, http.StatusOK, &APIPermission{Permission: doc}, nil)
   342  }
   343  
   344  func patchPermission(getPerms getPermsFunc, paramName string) echo.HandlerFunc {
   345  	return func(c echo.Context) error {
   346  		instance := middlewares.GetInstance(c)
   347  		current, err := middlewares.GetPermission(c)
   348  		if err != nil {
   349  			return err
   350  		}
   351  
   352  		var patch permission.Permission
   353  		if _, err = jsonapi.Bind(c.Request().Body, &patch); err != nil {
   354  			return err
   355  		}
   356  
   357  		patchSet := patch.Permissions != nil && len(patch.Permissions) > 0
   358  		patchCodes := len(patch.Codes) > 0
   359  
   360  		if patchCodes == patchSet {
   361  			return ErrPatchCodeOrSet
   362  		}
   363  
   364  		toPatch, err := getPerms(instance, c.Param(paramName))
   365  		if err != nil {
   366  			return err
   367  		}
   368  
   369  		if patchCodes {
   370  			if !current.CanUpdateShareByLink(toPatch) {
   371  				return permission.ErrNotParent
   372  			}
   373  			toPatch.PatchCodes(patch.Codes)
   374  		}
   375  
   376  		if patchSet {
   377  			for _, r := range patch.Permissions {
   378  				if r.Type == "" {
   379  					toPatch.RemoveRule(r)
   380  				} else if err := permission.CheckDoctypeName(r.Type, true); err != nil {
   381  					return err
   382  				} else if current.Permissions.RuleInSubset(r) {
   383  					toPatch.AddRules(r)
   384  				} else {
   385  					return permission.ErrNotSubset
   386  				}
   387  			}
   388  		}
   389  
   390  		// Handle metadata
   391  		// If the metadata has been given in the body request, just apply it to
   392  		// the patch
   393  		if patch.Metadata != nil {
   394  			toPatch.Metadata = patch.Metadata
   395  			patch.Metadata.EnsureCreatedFields(toPatch.Metadata)
   396  		} else if toPatch.Metadata != nil { // No metadata given in the request, but it does exist in the database: update it
   397  			// Using the token Subject for update
   398  			claims := c.Get("claims").(permission.Claims)
   399  			err = toPatch.Metadata.UpdatedByApp(claims.Subject, "")
   400  			if err != nil {
   401  				return err
   402  			}
   403  		}
   404  
   405  		if err = couchdb.UpdateDoc(instance, toPatch); err != nil {
   406  			return err
   407  		}
   408  
   409  		return jsonapi.Data(c, http.StatusOK, &APIPermission{toPatch, nil}, nil)
   410  	}
   411  }
   412  
   413  func revokePermission(c echo.Context) error {
   414  	instance := middlewares.GetInstance(c)
   415  
   416  	current, err := middlewares.GetPermission(c)
   417  	if err != nil {
   418  		return err
   419  	}
   420  
   421  	toRevoke, err := permission.GetByID(instance, c.Param("permdocid"))
   422  	if err != nil {
   423  		return err
   424  	}
   425  
   426  	if !current.CanUpdateShareByLink(toRevoke) {
   427  		return permission.ErrNotParent
   428  	}
   429  
   430  	err = toRevoke.Revoke(instance)
   431  	if err != nil {
   432  		return err
   433  	}
   434  
   435  	return c.NoContent(http.StatusNoContent)
   436  }
   437  
   438  // Routes sets the routing for the permissions service
   439  func Routes(router *echo.Group) {
   440  	// API Routes
   441  	router.POST("", createPermission)
   442  	router.GET("/self", displayPermissions)
   443  	router.POST("/exists", listPermissions)
   444  	router.GET("/:permdocid", showPermissions)
   445  	router.PATCH("/:permdocid", patchPermission(permission.GetByID, "permdocid"))
   446  	router.DELETE("/:permdocid", revokePermission)
   447  
   448  	router.PATCH("/apps/:slug", patchPermission(permission.GetForWebapp, "slug"))
   449  	router.PATCH("/konnectors/:slug", patchPermission(permission.GetForKonnector, "slug"))
   450  
   451  	router.GET("/doctype/:doctype/shared-by-link", listByLinkPermissionsByDoctype)
   452  
   453  	// Legacy routes, kept here for compatibility reasons
   454  	router.GET("/doctype/:doctype/sharedByLink", listByLinkPermissionsByDoctype)
   455  }