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

     1  package data
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"net/http"
     7  	"strings"
     8  
     9  	"github.com/cozy/cozy-stack/model/account"
    10  	"github.com/cozy/cozy-stack/model/oauth"
    11  	"github.com/cozy/cozy-stack/model/permission"
    12  	"github.com/cozy/cozy-stack/pkg/consts"
    13  	"github.com/cozy/cozy-stack/pkg/couchdb"
    14  	"github.com/cozy/cozy-stack/pkg/jsonapi"
    15  	"github.com/cozy/cozy-stack/pkg/metadata"
    16  	"github.com/cozy/cozy-stack/web/middlewares"
    17  	"github.com/labstack/echo/v4"
    18  )
    19  
    20  // XXX: it would be better to have specific routes for managing accounts. The
    21  // overriding of the /data/io.cozy.accounts/* routes is here mainly for
    22  // retro-compatible reasons, but specific routes would improve the API.
    23  
    24  func getAccount(c echo.Context) error {
    25  	instance := middlewares.GetInstance(c)
    26  	doctype := consts.Accounts
    27  	docid := c.Get("docid").(string)
    28  	if docid == "" {
    29  		return dbStatus(c)
    30  	}
    31  
    32  	var out couchdb.JSONDoc
    33  	var err error
    34  	rev := c.QueryParam("rev")
    35  	if rev != "" {
    36  		err = couchdb.GetDoc(instance, consts.SoftDeletedAccounts, docid, &out)
    37  		if err == nil && out.M["soft_deleted_rev"] != rev {
    38  			err = errors.New("invalid rev")
    39  		}
    40  		if err != nil {
    41  			err = couchdb.GetDocRev(instance, doctype, docid, rev, &out)
    42  		}
    43  	} else {
    44  		err = couchdb.GetDoc(instance, doctype, docid, &out)
    45  	}
    46  	if err != nil {
    47  		return fixErrorNoDatabaseIsWrongDoctype(err)
    48  	}
    49  	out.Type = doctype
    50  
    51  	if err = middlewares.Allow(c, permission.GET, &out); err != nil {
    52  		return err
    53  	}
    54  
    55  	if account.Encrypt(out) {
    56  		if err = couchdb.UpdateDoc(instance, &out); err != nil {
    57  			return err
    58  		}
    59  	}
    60  
    61  	perm, err := middlewares.GetPermission(c)
    62  	if err != nil {
    63  		return err
    64  	}
    65  	if perm.Type == permission.TypeKonnector ||
    66  		(c.QueryParam("include") == "credentials" && perm.Type == permission.TypeWebapp) {
    67  		// The account decryption is allowed for konnectors or for apps services
    68  		account.Decrypt(out)
    69  	}
    70  
    71  	return c.JSON(http.StatusOK, out.ToMapWithType())
    72  }
    73  
    74  func updateAccount(c echo.Context) error {
    75  	instance := middlewares.GetInstance(c)
    76  	docid := c.Get("docid").(string)
    77  
    78  	var doc couchdb.JSONDoc
    79  	if err := json.NewDecoder(c.Request().Body).Decode(&doc); err != nil {
    80  		return jsonapi.Errorf(http.StatusBadRequest, "%s", err)
    81  	}
    82  
    83  	doc.Type = consts.Accounts
    84  
    85  	if (doc.ID() == "") != (doc.Rev() == "") {
    86  		return jsonapi.NewError(http.StatusBadRequest,
    87  			"You must either provide an _id and _rev in document (update) or neither (create with fixed id).")
    88  	}
    89  
    90  	if doc.ID() != "" && doc.ID() != docid {
    91  		return jsonapi.NewError(http.StatusBadRequest, "document _id doesnt match url")
    92  	}
    93  
    94  	if doc.ID() == "" {
    95  		doc.SetID(docid)
    96  		return createNamedDoc(c, doc)
    97  	}
    98  
    99  	errWhole := middlewares.AllowWholeType(c, permission.PUT, doc.DocType())
   100  	if errWhole != nil {
   101  		// we cant apply to whole type, let's fetch old doc and see if it applies there
   102  		var old couchdb.JSONDoc
   103  		errFetch := couchdb.GetDoc(instance, doc.DocType(), doc.ID(), &old)
   104  		if errFetch != nil {
   105  			return errFetch
   106  		}
   107  		old.Type = doc.DocType()
   108  		// check if permissions set allows manipulating old doc
   109  		errOld := middlewares.Allow(c, permission.PUT, &old)
   110  		if errOld != nil {
   111  			return errOld
   112  		}
   113  
   114  		// also check if permissions set allows manipulating new doc
   115  		errNew := middlewares.Allow(c, permission.PUT, &doc)
   116  		if errNew != nil {
   117  			return errNew
   118  		}
   119  	}
   120  
   121  	account.Encrypt(doc)
   122  
   123  	if doc.M["cozyMetadata"] == nil {
   124  		// This is not the expected type for a JSON doc but it should work since it
   125  		// will be marshalled when saved.
   126  		doc.M["cozyMetadata"] = CozyMetadataFromClaims(c)
   127  	}
   128  
   129  	errUpdate := couchdb.UpdateDoc(instance, &doc)
   130  	if errUpdate != nil {
   131  		return fixErrorNoDatabaseIsWrongDoctype(errUpdate)
   132  	}
   133  
   134  	perm, err := middlewares.GetPermission(c)
   135  	if err != nil {
   136  		return err
   137  	}
   138  	if perm.Type == permission.TypeKonnector {
   139  		account.Decrypt(doc)
   140  	}
   141  
   142  	return c.JSON(http.StatusOK, echo.Map{
   143  		"ok":   true,
   144  		"id":   doc.ID(),
   145  		"rev":  doc.Rev(),
   146  		"type": doc.DocType(),
   147  		"data": doc.ToMapWithType(),
   148  	})
   149  }
   150  
   151  func createAccount(c echo.Context) error {
   152  	doctype := consts.Accounts
   153  	instance := middlewares.GetInstance(c)
   154  
   155  	doc := couchdb.JSONDoc{Type: doctype}
   156  	if err := json.NewDecoder(c.Request().Body).Decode(&doc.M); err != nil {
   157  		return jsonapi.Errorf(http.StatusBadRequest, "%s", err)
   158  	}
   159  
   160  	if err := middlewares.Allow(c, permission.POST, &doc); err != nil {
   161  		return err
   162  	}
   163  
   164  	account.Encrypt(doc)
   165  	account.ComputeName(doc)
   166  
   167  	// This is not the expected type for a JSON doc but it should work since it
   168  	// will be marshalled when saved.
   169  	doc.M["cozyMetadata"] = CozyMetadataFromClaims(c)
   170  
   171  	if err := couchdb.CreateDoc(instance, &doc); err != nil {
   172  		return err
   173  	}
   174  
   175  	return c.JSON(http.StatusCreated, echo.Map{
   176  		"ok":   true,
   177  		"id":   doc.ID(),
   178  		"rev":  doc.Rev(),
   179  		"type": doc.DocType(),
   180  		"data": doc.ToMapWithType(),
   181  	})
   182  }
   183  
   184  // CozyMetadataFromClaims returns a CozyMetadata struct, with the app fields
   185  // filled with information from the permission claims.
   186  func CozyMetadataFromClaims(c echo.Context) *metadata.CozyMetadata {
   187  	cm := metadata.New()
   188  
   189  	var slug, version string
   190  	if claims := c.Get("claims"); claims != nil {
   191  		cl := claims.(permission.Claims)
   192  		switch cl.AudienceString() {
   193  		case consts.AppAudience, consts.KonnectorAudience:
   194  			slug = cl.Subject
   195  		case consts.AccessTokenAudience:
   196  			if perms, err := middlewares.GetPermission(c); err == nil {
   197  				if cli, ok := perms.Client.(*oauth.Client); ok {
   198  					slug = oauth.GetLinkedAppSlug(cli.SoftwareID)
   199  					// Special case for cozy-desktop: it is an OAuth app not linked
   200  					// to a web app, so it has no slug, but we still want to keep
   201  					// in cozyMetadata its changes, so we use a fake slug.
   202  					if slug == "" && strings.Contains(cli.SoftwareID, "cozy-desktop") {
   203  						slug = "cozy-desktop"
   204  					}
   205  					version = cli.SoftwareVersion
   206  				}
   207  			}
   208  		}
   209  	}
   210  
   211  	if slug != "" {
   212  		cm.CreatedByApp = slug
   213  		cm.CreatedByAppVersion = version
   214  		cm.UpdatedByApps = []*metadata.UpdatedByAppEntry{
   215  			{
   216  				Slug:    slug,
   217  				Version: version,
   218  				Date:    cm.UpdatedAt,
   219  			},
   220  		}
   221  	}
   222  
   223  	return cm
   224  }