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 }