<commit_before>package data

import (
	"encoding/json"
	"net/http"
	"strings"

	"github.com/cozy/cozy-stack/model/account"
	"github.com/cozy/cozy-stack/model/permission"
	"github.com/cozy/cozy-stack/pkg/config/config"
	"github.com/cozy/cozy-stack/pkg/consts"
	"github.com/cozy/cozy-stack/pkg/couchdb"
	"github.com/cozy/cozy-stack/pkg/jsonapi"
	"github.com/cozy/cozy-stack/web/middlewares"
	"github.com/labstack/echo/v4"
)

// XXX: it would be better to have specific routes for managing accounts. The
// overriding of the /data/io.cozy.accounts/* routes is here mainly for
// retro-compatible reasons, but specific routes would improve the API.

func getAccount(c echo.Context) error {
	instance := middlewares.GetInstance(c)
	doctype := consts.Accounts
	docid := c.Get("docid").(string)
	if docid == "" {
		return dbStatus(c)
	}

	var out couchdb.JSONDoc
	var err error
	rev := c.QueryParam("rev")
	if rev != "" {
		err = couchdb.GetDocRev(instance, doctype, docid, rev, &out)
	} else {
		err = couchdb.GetDoc(instance, doctype, docid, &out)
	}
	if err != nil {
		return fixErrorNoDatabaseIsWrongDoctype(err)
	}
	out.Type = doctype

	if err = middlewares.Allow(c, permission.GET, &out); err != nil {
		return err
	}

	if EncryptAccount(out) {
		if err = couchdb.UpdateDoc(instance, &out); err != nil {
			return err
		}
	}

	perm, err := middlewares.GetPermission(c)
	if err != nil {
		return err
	}
	if perm.Type == permission.TypeKonnector ||
		(c.QueryParam("include") == "credentials" && perm.Type == permission.TypeWebapp) {
		// The account decryption is allowed for konnectors or for apps services
		DecryptAccount(out)
	}

	return c.JSON(http.StatusOK, out.ToMapWithType())
}

func updateAccount(c echo.Context) error {
	instance := middlewares.GetInstance(c)

	var doc couchdb.JSONDoc
	if err := json.NewDecoder(c.Request().Body).Decode(&doc); err != nil {
		return jsonapi.Errorf(http.StatusBadRequest, "%s", err)
	}

	doc.Type = consts.Accounts

	if (doc.ID() == "") != (doc.Rev() == "") {
		return jsonapi.NewError(http.StatusBadRequest,
			"You must either provide an _id and _rev in document (update) or neither (create with fixed id).")
	}

	if doc.ID() != "" && doc.ID() != c.Get("docid").(string) {
		return jsonapi.NewError(http.StatusBadRequest, "document _id doesnt match url")
	}

	if doc.ID() == "" {
		doc.SetID(c.Get("docid").(string))
		return createNamedDoc(c, doc)
	}

	errWhole := middlewares.AllowWholeType(c, permission.PUT, doc.DocType())
	if errWhole != nil {
		// we cant apply to whole type, let's fetch old doc and see if it applies there
		var old couchdb.JSONDoc
		errFetch := couchdb.GetDoc(instance, doc.DocType(), doc.ID(), &old)
		if errFetch != nil {
			return errFetch
		}
		old.Type = doc.DocType()
		// check if permissions set allows manipulating old doc
		errOld := middlewares.Allow(c, permission.PUT, &old)
		if errOld != nil {
			return errOld
		}

		// also check if permissions set allows manipulating new doc
		errNew := middlewares.Allow(c, permission.PUT, &doc)
		if errNew != nil {
			return errNew
		}
	}

	EncryptAccount(doc)

	errUpdate := couchdb.UpdateDoc(instance, &doc)
	if errUpdate != nil {
		return fixErrorNoDatabaseIsWrongDoctype(errUpdate)
	}

	perm, err := middlewares.GetPermission(c)
	if err != nil {
		return err
	}
	if perm.Type == permission.TypeKonnector {
		DecryptAccount(doc)
	}

	return c.JSON(http.StatusOK, echo.Map{
		"ok":   true,
		"id":   doc.ID(),
		"rev":  doc.Rev(),
		"type": doc.DocType(),
		"data": doc.ToMapWithType(),
	})
}

func EncryptAccount(doc couchdb.JSONDoc) bool {
	if config.GetVault().CredentialsEncryptorKey() != nil {
		return encryptMap(doc.M)
	}
	return false
}

func DecryptAccount(doc couchdb.JSONDoc) bool {
	if config.GetVault().CredentialsDecryptorKey() != nil {
		return decryptMap(doc.M)
	}
	return false
}

func encryptMap(m map[string]interface{}) (encrypted bool) {
	auth, ok := m["auth"].(map[string]interface{})
	if !ok {
		return
	}
	login, _ := auth["login"].(string)
	cloned := make(map[string]interface{}, len(auth))
	var encKeys []string
	for k, v := range auth {
		var err error
		switch k {
		case "password":
			password, _ := v.(string)
			cloned["credentials_encrypted"], err = account.EncryptCredentials(login, password)
			if err == nil {<commit_msg>docs: Add docstrings to exported functions<commit_after>package data

import (
	"encoding/json"
	"net/http"
	"strings"

	"github.com/cozy/cozy-stack/model/account"
	"github.com/cozy/cozy-stack/model/permission"
	"github.com/cozy/cozy-stack/pkg/config/config"
	"github.com/cozy/cozy-stack/pkg/consts"
	"github.com/cozy/cozy-stack/pkg/couchdb"
	"github.com/cozy/cozy-stack/pkg/jsonapi"
	"github.com/cozy/cozy-stack/web/middlewares"
	"github.com/labstack/echo/v4"
)

// XXX: it would be better to have specific routes for managing accounts. The
// overriding of the /data/io.cozy.accounts/* routes is here mainly for
// retro-compatible reasons, but specific routes would improve the API.

func getAccount(c echo.Context) error {
	instance := middlewares.GetInstance(c)
	doctype := consts.Accounts
	docid := c.Get("docid").(string)
	if docid == "" {
		return dbStatus(c)
	}

	var out couchdb.JSONDoc
	var err error
	rev := c.QueryParam("rev")
	if rev != "" {
		err = couchdb.GetDocRev(instance, doctype, docid, rev, &out)
	} else {
		err = couchdb.GetDoc(instance, doctype, docid, &out)
	}
	if err != nil {
		return fixErrorNoDatabaseIsWrongDoctype(err)
	}
	out.Type = doctype

	if err = middlewares.Allow(c, permission.GET, &out); err != nil {
		return err
	}

	if EncryptAccount(out) {
		if err = couchdb.UpdateDoc(instance, &out); err != nil {
			return err
		}
	}

	perm, err := middlewares.GetPermission(c)
	if err != nil {
		return err
	}
	if perm.Type == permission.TypeKonnector ||
		(c.QueryParam("include") == "credentials" && perm.Type == permission.TypeWebapp) {
		// The account decryption is allowed for konnectors or for apps services
		DecryptAccount(out)
	}

	return c.JSON(http.StatusOK, out.ToMapWithType())
}

func updateAccount(c echo.Context) error {
	instance := middlewares.GetInstance(c)

	var doc couchdb.JSONDoc
	if err := json.NewDecoder(c.Request().Body).Decode(&doc); err != nil {
		return jsonapi.Errorf(http.StatusBadRequest, "%s", err)
	}

	doc.Type = consts.Accounts

	if (doc.ID() == "") != (doc.Rev() == "") {
		return jsonapi.NewError(http.StatusBadRequest,
			"You must either provide an _id and _rev in document (update) or neither (create with fixed id).")
	}

	if doc.ID() != "" && doc.ID() != c.Get("docid").(string) {
		return jsonapi.NewError(http.StatusBadRequest, "document _id doesnt match url")
	}

	if doc.ID() == "" {
		doc.SetID(c.Get("docid").(string))
		return createNamedDoc(c, doc)
	}

	errWhole := middlewares.AllowWholeType(c, permission.PUT, doc.DocType())
	if errWhole != nil {
		// we cant apply to whole type, let's fetch old doc and see if it applies there
		var old couchdb.JSONDoc
		errFetch := couchdb.GetDoc(instance, doc.DocType(), doc.ID(), &old)
		if errFetch != nil {
			return errFetch
		}
		old.Type = doc.DocType()
		// check if permissions set allows manipulating old doc
		errOld := middlewares.Allow(c, permission.PUT, &old)
		if errOld != nil {
			return errOld
		}

		// also check if permissions set allows manipulating new doc
		errNew := middlewares.Allow(c, permission.PUT, &doc)
		if errNew != nil {
			return errNew
		}
	}

	EncryptAccount(doc)

	errUpdate := couchdb.UpdateDoc(instance, &doc)
	if errUpdate != nil {
		return fixErrorNoDatabaseIsWrongDoctype(errUpdate)
	}

	perm, err := middlewares.GetPermission(c)
	if err != nil {
		return err
	}
	if perm.Type == permission.TypeKonnector {
		DecryptAccount(doc)
	}

	return c.JSON(http.StatusOK, echo.Map{
		"ok":   true,
		"id":   doc.ID(),
		"rev":  doc.Rev(),
		"type": doc.DocType(),
		"data": doc.ToMapWithType(),
	})
}

// Encrypts sensitive fields inside the account. The document
// is modified in place.
func EncryptAccount(doc couchdb.JSONDoc) bool {
	if config.GetVault().CredentialsEncryptorKey() != nil {
		return encryptMap(doc.M)
	}
	return false
}

// Decrypts sensitive fields inside the account. The document
// is modified in place.
func DecryptAccount(doc couchdb.JSONDoc) bool {
	if config.GetVault().CredentialsDecryptorKey() != nil {
		return decryptMap(doc.M)
	}
	return false
}

func encryptMap(m map[string]interface{}) (encrypted bool) {
	auth, ok := m["auth"].(map[string]interface{})
	if !ok {
		return
	}
	login, _ := auth["login"].(string)
	cloned := make(map[string]interface{}, len(auth))
	var encKeys []string
	for k, v := range auth {
		var err error
		switch k {
		case "password":
			password, _ := v.(string)
			cloned["credentials_encrypted"], err = account.EncryptCredentials(login, password)
			if err == nil {
{'fraction_non_alphanumeric': 0.14471433458775232, 'fraction_numerical': 0.00022807617744326604, 'mean_word_length': 3.9436302142051862, 'pattern_counts': {'":': 12, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1265413', 'n_tokens_mistral': 3273, 'n_tokens_neox': 3089, 'n_words': 759}