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

     1  package bitwarden
     2  
     3  import (
     4  	"net/http"
     5  
     6  	"github.com/cozy/cozy-stack/model/bitwarden"
     7  	"github.com/cozy/cozy-stack/model/bitwarden/settings"
     8  	"github.com/cozy/cozy-stack/model/instance"
     9  	"github.com/cozy/cozy-stack/model/permission"
    10  	"github.com/cozy/cozy-stack/pkg/consts"
    11  	"github.com/cozy/cozy-stack/pkg/couchdb"
    12  	"github.com/cozy/cozy-stack/web/middlewares"
    13  	"github.com/labstack/echo/v4"
    14  )
    15  
    16  // https://github.com/bitwarden/jslib/blob/master/common/src/models/response/profileResponse.ts
    17  type profileResponse struct {
    18  	ID            string                  `json:"Id"`
    19  	Name          string                  `json:"Name"`
    20  	Email         string                  `json:"Email"`
    21  	EmailVerified bool                    `json:"EmailVerified"`
    22  	Premium       bool                    `json:"Premium"`
    23  	Hint          interface{}             `json:"MasterPasswordHint"`
    24  	Culture       string                  `json:"Culture"`
    25  	TwoFactor     bool                    `json:"TwoFactorEnabled"`
    26  	Key           string                  `json:"Key"`
    27  	PrivateKey    interface{}             `json:"PrivateKey"`
    28  	SStamp        string                  `json:"SecurityStamp"`
    29  	Organizations []*organizationResponse `json:"Organizations"`
    30  	Object        string                  `json:"Object"`
    31  }
    32  
    33  func newProfileResponse(inst *instance.Instance, setting *settings.Settings) (*profileResponse, error) {
    34  	doc, err := inst.SettingsDocument()
    35  	if err != nil {
    36  		return nil, err
    37  	}
    38  	name, _ := doc.M["public_name"].(string)
    39  	salt := inst.PassphraseSalt()
    40  	orgs, err := bitwarden.FindAllOrganizations(inst, setting)
    41  	if err != nil {
    42  		return nil, err
    43  	}
    44  	organizations := make([]*organizationResponse, len(orgs))
    45  	for i, org := range orgs {
    46  		organizations[i] = newOrganizationResponse(inst, org)
    47  	}
    48  	p := &profileResponse{
    49  		ID:            inst.ID(),
    50  		Name:          name,
    51  		Email:         string(salt),
    52  		EmailVerified: false,
    53  		Premium:       true,
    54  		Hint:          nil,
    55  		Culture:       inst.Locale,
    56  		TwoFactor:     false,
    57  		Key:           setting.Key,
    58  		SStamp:        setting.SecurityStamp,
    59  		Organizations: organizations,
    60  		Object:        "profile",
    61  	}
    62  	if setting.PrivateKey != "" {
    63  		p.PrivateKey = setting.PrivateKey
    64  	}
    65  	if setting.PassphraseHint != "" {
    66  		p.Hint = setting.PassphraseHint
    67  	}
    68  	return p, nil
    69  }
    70  
    71  // https://github.com/bitwarden/jslib/blob/master/common/src/models/response/syncResponse.ts
    72  type syncResponse struct {
    73  	Profile     *profileResponse      `json:"Profile"`
    74  	Folders     []*folderResponse     `json:"Folders"`
    75  	Ciphers     []*cipherResponse     `json:"Ciphers"`
    76  	Collections []*collectionResponse `json:"Collections"`
    77  	Domains     *domainsResponse      `json:"Domains"`
    78  	Object      string                `json:"Object"`
    79  }
    80  
    81  func newSyncResponse(
    82  	inst *instance.Instance,
    83  	setting *settings.Settings,
    84  	profile *profileResponse,
    85  	ciphers []*bitwarden.Cipher,
    86  	folders []*bitwarden.Folder,
    87  	organizations []*bitwarden.Organization,
    88  	domains *domainsResponse,
    89  ) *syncResponse {
    90  	foldersResponse := make([]*folderResponse, len(folders))
    91  	for i, f := range folders {
    92  		foldersResponse[i] = newFolderResponse(f)
    93  	}
    94  	ciphersResponse := make([]*cipherResponse, len(ciphers))
    95  	for i, c := range ciphers {
    96  		ciphersResponse[i] = newCipherResponse(c, setting)
    97  	}
    98  	collectionsResponse := make([]*collectionResponse, len(organizations))
    99  	for i, o := range organizations {
   100  		collectionsResponse[i] = newCollectionResponse(inst, o, &o.Collection)
   101  	}
   102  	return &syncResponse{
   103  		Profile:     profile,
   104  		Folders:     foldersResponse,
   105  		Ciphers:     ciphersResponse,
   106  		Collections: collectionsResponse,
   107  		Domains:     domains,
   108  		Object:      "sync",
   109  	}
   110  }
   111  
   112  // Sync is the handler for the main endpoint of the bitwarden API. It is used
   113  // by the client as a one-way sync: it fetches all objects from the server to
   114  // update its local database.
   115  func Sync(c echo.Context) error {
   116  	inst := middlewares.GetInstance(c)
   117  	if err := middlewares.AllowWholeType(c, permission.GET, consts.BitwardenCiphers); err != nil {
   118  		return c.JSON(http.StatusUnauthorized, echo.Map{
   119  			"error": "invalid token",
   120  		})
   121  	}
   122  	setting, err := settings.Get(inst)
   123  	if err != nil {
   124  		return err
   125  	}
   126  
   127  	profile, err := newProfileResponse(inst, setting)
   128  	if err != nil {
   129  		return c.JSON(http.StatusInternalServerError, echo.Map{
   130  			"error": err.Error(),
   131  		})
   132  	}
   133  
   134  	var ciphers []*bitwarden.Cipher
   135  	req := &couchdb.AllDocsRequest{}
   136  	if err := couchdb.GetAllDocs(inst, consts.BitwardenCiphers, req, &ciphers); err != nil {
   137  		return c.JSON(http.StatusInternalServerError, echo.Map{
   138  			"error": err.Error(),
   139  		})
   140  	}
   141  
   142  	var folders []*bitwarden.Folder
   143  	req = &couchdb.AllDocsRequest{}
   144  	if err := couchdb.GetAllDocs(inst, consts.BitwardenFolders, req, &folders); err != nil {
   145  		if couchdb.IsNoDatabaseError(err) {
   146  			_ = couchdb.CreateDB(inst, consts.BitwardenFolders)
   147  		} else {
   148  			return c.JSON(http.StatusInternalServerError, echo.Map{
   149  				"error": err.Error(),
   150  			})
   151  		}
   152  	}
   153  
   154  	organizations, err := bitwarden.FindAllOrganizations(inst, setting)
   155  	if err != nil {
   156  		return c.JSON(http.StatusInternalServerError, echo.Map{
   157  			"error": err.Error(),
   158  		})
   159  	}
   160  
   161  	var domains *domainsResponse
   162  	if c.QueryParam("excludeDomains") == "" {
   163  		domains = newDomainsResponse(setting)
   164  	}
   165  
   166  	res := newSyncResponse(inst, setting, profile, ciphers, folders, organizations, domains)
   167  	return c.JSON(http.StatusOK, res)
   168  }