passworddepot_api.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. /*
  2. Copyright © The ESO Authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package passworddepot
  14. import (
  15. "bytes"
  16. "context"
  17. "crypto/tls"
  18. "encoding/json"
  19. "errors"
  20. "fmt"
  21. "net/http"
  22. "strings"
  23. "time"
  24. )
  25. const (
  26. // DoRequestError is the error format string for request errors.
  27. DoRequestError = "error: do request: %w"
  28. )
  29. // HTTPClient is an interface representing the ability to perform HTTP requests.
  30. type HTTPClient interface {
  31. Do(*http.Request) (*http.Response, error)
  32. }
  33. // AccessData represents the access credentials returned by the Password Depot API upon successful login.
  34. type AccessData struct {
  35. ClientID string `json:"client_id"`
  36. AccessToken string `json:"access_token"`
  37. }
  38. // Databases represents the list of Password Depot databases accessible with the current credentials.
  39. type Databases struct {
  40. Databases []struct {
  41. Name string `json:"name"`
  42. Fingerprint string `json:"fingerprint"`
  43. Date time.Time `json:"date"`
  44. Rights string `json:"rights"`
  45. Reasondelete string `json:"reasondelete"`
  46. } `json:"databases"`
  47. Infoclasses string `json:"infoclasses"`
  48. Policyforce string `json:"policyforce"`
  49. Policyminlength string `json:"policyminlength"`
  50. Policyincludeatleast string `json:"policyincludeatleast"`
  51. Policymingroups string `json:"policymingroups"`
  52. Policyselectedgroups string `json:"policyselectedgroups"`
  53. }
  54. // DatabaseEntries represents the entries in a Password Depot database.
  55. type DatabaseEntries struct {
  56. Name string `json:"name"`
  57. Parent string `json:"parent"`
  58. Entries []Entry `json:"entries"`
  59. Infoclasses string `json:"infoclasses"`
  60. Reasondelete string `json:"reasondelete"`
  61. }
  62. // Entry represents a single entry in the Password Depot database.
  63. type Entry struct {
  64. Name string `json:"name"`
  65. Login string `json:"login"`
  66. Password string `json:"pass"`
  67. URL string `json:"url"`
  68. Importance string `json:"importance"`
  69. Date time.Time `json:"date"`
  70. Icon string `json:"icon"`
  71. Secondeye string `json:"secondeye"`
  72. Fingerprint string `json:"fingerprint"`
  73. Rights string `json:"rights"`
  74. Itemclass string `json:"itemclass"`
  75. }
  76. // API represents a client for the Password Depot API.
  77. type API struct {
  78. client HTTPClient
  79. baseURL string
  80. hostPort string
  81. secret *AccessData
  82. password string
  83. username string
  84. }
  85. // SecretEntry represents a secret entry in Password Depot.
  86. type SecretEntry struct {
  87. Name string `json:"name"`
  88. Fingerprint string `json:"fingerprint"`
  89. Itemclass string `json:"itemclass"`
  90. Login string `json:"login"`
  91. Pass string `json:"pass"`
  92. URL string `json:"url"`
  93. Importance string `json:"importance"`
  94. Date time.Time `json:"date"`
  95. Comment string `json:"comment"`
  96. Expirydate string `json:"expirydate"`
  97. Tags string `json:"tags"`
  98. Author string `json:"author"`
  99. Category string `json:"category"`
  100. Icon string `json:"icon"`
  101. Secondeye string `json:"secondeye"`
  102. Secondpass string `json:"secondpass"`
  103. Template string `json:"template"`
  104. Acm string `json:"acm"`
  105. Paramstr string `json:"paramstr"`
  106. Loginid string `json:"loginid"`
  107. Passid string `json:"passid"`
  108. Donotaddon string `json:"donotaddon"`
  109. Markassafe string `json:"markassafe"`
  110. Safemode string `json:"safemode"`
  111. }
  112. var errDBNotFound = errors.New("database not found")
  113. var errSecretNotFound = errors.New("secret not found")
  114. // NewAPI creates a new instance of the PasswordDepot API client and performs login.
  115. func NewAPI(ctx context.Context, baseURL, username, password, hostPort string) (*API, error) {
  116. api := &API{
  117. baseURL: baseURL,
  118. hostPort: hostPort,
  119. username: username,
  120. password: password,
  121. }
  122. tr := &http.Transport{
  123. TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
  124. }
  125. api.client = &http.Client{Transport: tr}
  126. err := api.login(ctx)
  127. if err != nil {
  128. return nil, fmt.Errorf("failed to login: %w", err)
  129. }
  130. return api, nil
  131. }
  132. func (api *API) doAuthenticatedRequest(r *http.Request) (*http.Response, error) {
  133. r.Header.Add("access_token", api.secret.AccessToken)
  134. r.Header.Add("client_id", api.secret.ClientID)
  135. return api.client.Do(r)
  136. }
  137. func (api *API) getDatabaseFingerprint(database string) (string, error) {
  138. databases, err := api.ListDatabases()
  139. if err != nil {
  140. return "", fmt.Errorf("error: getting database list: %w", err)
  141. }
  142. for _, db := range databases.Databases {
  143. if strings.Contains(db.Name, database) {
  144. return db.Fingerprint, nil
  145. }
  146. }
  147. return "", errDBNotFound
  148. }
  149. func (api *API) getSecretFingerprint(databaseFingerprint, secretName, folder string) (string, error) {
  150. secrets, err := api.ListSecrets(databaseFingerprint, folder)
  151. if err != nil {
  152. return "", fmt.Errorf("error: getting secrets list: %w", err)
  153. }
  154. parts := strings.Split(secretName, ".")
  155. searchName := parts[0]
  156. var fingerprint string
  157. for _, entry := range secrets.Entries {
  158. if strings.Contains(entry.Name, searchName) {
  159. fingerprint = entry.Fingerprint
  160. if len(parts) > 1 {
  161. return api.getSecretFingerprint(databaseFingerprint, strings.Join(parts[1:], "."), fingerprint)
  162. }
  163. return fingerprint, nil
  164. }
  165. }
  166. return "", errSecretNotFound
  167. }
  168. func (api *API) getendpointURL(endpoint string) string {
  169. return fmt.Sprintf("https://%s:%s/v1.0/%s", api.baseURL, api.hostPort, endpoint)
  170. }
  171. func (api *API) login(ctx context.Context) error {
  172. loginRequest, err := http.NewRequestWithContext(ctx, "GET", api.getendpointURL("login"), http.NoBody)
  173. if err != nil {
  174. return fmt.Errorf("error creating request: %w", err)
  175. }
  176. loginRequest.Header.Add("user", api.username)
  177. loginRequest.Header.Add("pass", api.password)
  178. resp, err := api.client.Do(loginRequest) //nolint:bodyclose // linters bug
  179. if err != nil {
  180. return fmt.Errorf(DoRequestError, err)
  181. }
  182. accessData := AccessData{}
  183. err = ReadAndUnmarshal(resp, &accessData)
  184. if err != nil {
  185. return fmt.Errorf("error: failed to unmarshal response body: %w", err)
  186. }
  187. api.secret = &accessData
  188. return nil
  189. }
  190. // ListSecrets retrieves the list of secrets from the specified database and folder.
  191. func (api *API) ListSecrets(dbFingerprint, folder string) (DatabaseEntries, error) {
  192. endpointURL := api.getendpointURL(fmt.Sprintf("list?db=%s", dbFingerprint))
  193. if folder != "" {
  194. endpointURL = fmt.Sprintf("%s&folder=%s", endpointURL, folder)
  195. }
  196. listSecrets, err := http.NewRequest("GET", endpointURL, http.NoBody)
  197. if err != nil {
  198. return DatabaseEntries{}, fmt.Errorf("error: creating secrets request: %w", err)
  199. }
  200. respSecretsList, err := api.doAuthenticatedRequest(listSecrets) //nolint:bodyclose // linters bug
  201. if err != nil {
  202. return DatabaseEntries{}, fmt.Errorf(DoRequestError, err)
  203. }
  204. dbEntries := DatabaseEntries{}
  205. err = ReadAndUnmarshal(respSecretsList, &dbEntries)
  206. return dbEntries, err
  207. }
  208. // ReadAndUnmarshal reads the response body and unmarshals it into the target struct.
  209. func ReadAndUnmarshal(resp *http.Response, target any) error {
  210. var buf bytes.Buffer
  211. defer func() {
  212. _ = resp.Body.Close()
  213. }()
  214. if resp.StatusCode < 200 || resp.StatusCode > 299 {
  215. return fmt.Errorf("failed to authenticate with the given credentials: %d %s", resp.StatusCode, buf.String())
  216. }
  217. _, err := buf.ReadFrom(resp.Body)
  218. if err != nil {
  219. return err
  220. }
  221. return json.Unmarshal(buf.Bytes(), target)
  222. }
  223. // ListDatabases retrieves the list of databases accessible with the current credentials.
  224. func (api *API) ListDatabases() (Databases, error) {
  225. listDBRequest, err := http.NewRequest("GET", api.getendpointURL("list"), http.NoBody)
  226. if err != nil {
  227. return Databases{}, fmt.Errorf("error: creating db request: %w", err)
  228. }
  229. respDBList, err := api.doAuthenticatedRequest(listDBRequest) //nolint:bodyclose // linters bug
  230. if err != nil {
  231. return Databases{}, fmt.Errorf(DoRequestError, err)
  232. }
  233. databases := Databases{}
  234. err = ReadAndUnmarshal(respDBList, &databases)
  235. return databases, err
  236. }
  237. // GetSecret retrieves a secret by its name from the specified database.
  238. func (api *API) GetSecret(database, secretName string) (SecretEntry, error) {
  239. dbFingerprint, err := api.getDatabaseFingerprint(database)
  240. if err != nil {
  241. return SecretEntry{}, fmt.Errorf("error: getting DB fingerprint: %w", err)
  242. }
  243. secretFingerprint, err := api.getSecretFingerprint(dbFingerprint, secretName, "")
  244. if err != nil {
  245. return SecretEntry{}, fmt.Errorf("error: getting Secret fingerprint: %w", err)
  246. }
  247. readSecretRequest, err := http.NewRequest("GET", api.getendpointURL(fmt.Sprintf("read?db=%s&entry=%s", dbFingerprint, secretFingerprint)), http.NoBody)
  248. if err != nil {
  249. return SecretEntry{}, fmt.Errorf("error: creating secrets request: %w", err)
  250. }
  251. respSecretRead, err := api.doAuthenticatedRequest(readSecretRequest) //nolint:bodyclose // linters bug
  252. if err != nil {
  253. return SecretEntry{}, fmt.Errorf(DoRequestError, err)
  254. }
  255. secretEntry := SecretEntry{}
  256. err = ReadAndUnmarshal(respSecretRead, &secretEntry)
  257. return secretEntry, err
  258. }
  259. // ToMap converts the SecretEntry struct to a map[string][]byte.
  260. func (s *SecretEntry) ToMap() map[string][]byte {
  261. m := make(map[string][]byte)
  262. m["name"] = []byte(s.Name)
  263. m["fingerprint"] = []byte(s.Fingerprint)
  264. m["itemclass"] = []byte(s.Itemclass)
  265. m["login"] = []byte(s.Login)
  266. m["pass"] = []byte(s.Pass)
  267. m["url"] = []byte(s.URL)
  268. m["importance"] = []byte(s.Importance)
  269. m["comment"] = []byte(s.Comment)
  270. m["expirydate"] = []byte(s.Expirydate)
  271. m["tags"] = []byte(s.Tags)
  272. m["author"] = []byte(s.Author)
  273. m["category"] = []byte(s.Category)
  274. m["icon"] = []byte(s.Icon)
  275. m["secondeye"] = []byte(s.Secondeye)
  276. m["secondpass"] = []byte(s.Secondpass)
  277. m["template"] = []byte(s.Template)
  278. m["acm"] = []byte(s.Acm)
  279. m["paramstr"] = []byte(s.Paramstr)
  280. m["loginid"] = []byte(s.Loginid)
  281. m["passid"] = []byte(s.Passid)
  282. m["donotaddon"] = []byte(s.Donotaddon)
  283. m["markassafe"] = []byte(s.Markassafe)
  284. m["safemode"] = []byte(s.Safemode)
  285. return m
  286. }