client.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. /*
  2. Licensed under the Apache License, Version 2.0 (the "License");
  3. you may not use this file except in compliance with the License.
  4. You may obtain a copy of the License at
  5. http://www.apache.org/licenses/LICENSE-2.0
  6. Unless required by applicable law or agreed to in writing, software
  7. distributed under the License is distributed on an "AS IS" BASIS,
  8. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. See the License for the specific language governing permissions and
  10. limitations under the License.
  11. */
  12. package client
  13. import (
  14. "bytes"
  15. "context"
  16. "crypto/tls"
  17. "encoding/json"
  18. "fmt"
  19. "io"
  20. "net/http"
  21. "net/url"
  22. "strings"
  23. "time"
  24. aesdecrypt "github.com/Onboardbase/go-cryptojs-aes-decrypt/decrypt"
  25. )
  26. const HTTPTimeoutDuration = 20 * time.Second
  27. const ObbSecretsEndpointPath = "/secrets"
  28. const errUnableToDecrtypt = "unable to decrypt secret payload"
  29. type OnboardbaseClient struct {
  30. baseURL *url.URL
  31. OnboardbaseAPIKey string
  32. VerifyTLS bool
  33. UserAgent string
  34. OnboardbasePassCode string
  35. httpClient *http.Client
  36. }
  37. type queryParams map[string]string
  38. type headers map[string]string
  39. type DeleteSecretsRequest struct {
  40. SecretID string `json:"secretId,omitempty"`
  41. }
  42. type httpRequestBody []byte
  43. type Secrets map[string]string
  44. type RawSecret struct {
  45. Key string `json:"key,omitempty"`
  46. Value string `json:"value,omitempty"`
  47. }
  48. type RawSecrets []RawSecret
  49. type APIError struct {
  50. Err error
  51. Message string
  52. Data string
  53. }
  54. type apiResponse struct {
  55. HTTPResponse *http.Response
  56. Body []byte
  57. }
  58. type apiErrorResponse struct {
  59. Messages []string
  60. Success bool
  61. }
  62. type SecretRequest struct {
  63. Environment string
  64. Project string
  65. Name string
  66. }
  67. type SecretsRequest struct {
  68. Environment string
  69. Project string
  70. }
  71. type secretResponseBodyObject struct {
  72. Title string `json:"title,omitempty"`
  73. ID string `json:"id,omitempty"`
  74. }
  75. type secretResponseSecrets struct {
  76. ID string `json:"id"`
  77. Key string `json:"key"`
  78. Value string `json:"value"`
  79. }
  80. type secretResponseBodyData struct {
  81. Project secretResponseBodyObject `json:"project,omitempty"`
  82. Environment secretResponseBodyObject `json:"environment,omitempty"`
  83. Team secretResponseBodyObject `json:"team,omitempty"`
  84. Secrets []secretResponseSecrets `json:"secrets,omitempty"`
  85. Status string `json:"status"`
  86. Message string `json:"string"`
  87. }
  88. type secretResponseBody struct {
  89. Data secretResponseBodyData `json:"data,omitempty"`
  90. Message string `json:"message,omitempty"`
  91. Status string `json:"status,omitempty"`
  92. }
  93. type SecretResponse struct {
  94. Name string
  95. Value string
  96. }
  97. type SecretsResponse struct {
  98. Secrets Secrets
  99. Body []byte
  100. }
  101. func NewOnboardbaseClient(onboardbaseAPIKey, onboardbasePasscode string) (*OnboardbaseClient, error) {
  102. tlsConfig := &tls.Config{
  103. MinVersion: tls.VersionTLS12,
  104. }
  105. httpTransport := &http.Transport{
  106. DisableKeepAlives: true,
  107. TLSClientConfig: tlsConfig,
  108. }
  109. client := &OnboardbaseClient{
  110. OnboardbaseAPIKey: onboardbaseAPIKey,
  111. OnboardbasePassCode: onboardbasePasscode,
  112. VerifyTLS: true,
  113. UserAgent: "onboardbase-external-secrets",
  114. httpClient: &http.Client{
  115. Timeout: HTTPTimeoutDuration,
  116. Transport: httpTransport,
  117. },
  118. }
  119. if err := client.SetBaseURL("https://public.onboardbase.com/api/v1/"); err != nil {
  120. return nil, &APIError{Err: err, Message: "setting base URL failed"}
  121. }
  122. return client, nil
  123. }
  124. func (c *OnboardbaseClient) BaseURL() *url.URL {
  125. u := *c.baseURL
  126. return &u
  127. }
  128. func (c *OnboardbaseClient) SetBaseURL(urlStr string) error {
  129. baseURL, err := url.Parse(strings.TrimSuffix(urlStr, "/"))
  130. if err != nil {
  131. return err
  132. }
  133. c.baseURL = baseURL
  134. return nil
  135. }
  136. func (c *OnboardbaseClient) Authenticate() error {
  137. _, err := c.performRequest(
  138. &performRequestConfig{
  139. path: "/team/members",
  140. method: "GET",
  141. headers: headers{},
  142. params: queryParams{},
  143. body: httpRequestBody{},
  144. })
  145. if err != nil {
  146. return err
  147. }
  148. return nil
  149. }
  150. func (c *OnboardbaseClient) getSecretsFromPayload(data secretResponseBodyData) (map[string]string, error) {
  151. kv := make(map[string]string)
  152. for _, secret := range data.Secrets {
  153. passphrase := c.OnboardbasePassCode
  154. key, err := aesdecrypt.Run(secret.Key, passphrase)
  155. if err != nil {
  156. return nil, &APIError{Err: err, Message: errUnableToDecrtypt, Data: secret.Key}
  157. }
  158. value, err := aesdecrypt.Run(secret.Value, passphrase)
  159. if err != nil {
  160. return nil, &APIError{Err: err, Message: errUnableToDecrtypt, Data: secret.Value}
  161. }
  162. kv[key] = value
  163. }
  164. return kv, nil
  165. }
  166. func (c *OnboardbaseClient) mapSecretsByPlainKey(data secretResponseBodyData) (map[string]secretResponseSecrets, error) {
  167. kv := make(map[string]secretResponseSecrets)
  168. for _, secret := range data.Secrets {
  169. passphrase := c.OnboardbasePassCode
  170. key, err := aesdecrypt.Run(secret.Key, passphrase)
  171. if err != nil {
  172. return nil, &APIError{Err: err, Message: errUnableToDecrtypt, Data: secret.Key}
  173. }
  174. kv[key] = secret
  175. }
  176. return kv, nil
  177. }
  178. func (c *OnboardbaseClient) GetSecret(request SecretRequest) (*SecretResponse, error) {
  179. response, err := c.performRequest(
  180. &performRequestConfig{
  181. path: ObbSecretsEndpointPath,
  182. method: "GET",
  183. headers: headers{},
  184. params: request.buildQueryParams(),
  185. body: httpRequestBody{},
  186. })
  187. if err != nil {
  188. return nil, err
  189. }
  190. var data secretResponseBody
  191. if err := json.Unmarshal(response.Body, &data); err != nil {
  192. return nil, &APIError{Err: err, Message: "unable to unmarshal secret payload", Data: string(response.Body)}
  193. }
  194. secrets, _ := c.getSecretsFromPayload(data.Data)
  195. secret := secrets[request.Name]
  196. if secret == "" {
  197. return nil, &APIError{Message: fmt.Sprintf("secret %s for project '%s' and environment '%s' not found", request.Name, request.Project, request.Environment)}
  198. }
  199. return &SecretResponse{Name: request.Name, Value: secrets[request.Name]}, nil
  200. }
  201. func (c *OnboardbaseClient) DeleteSecret(request SecretRequest) error {
  202. secretsrequest := SecretsRequest{
  203. Project: request.Project,
  204. Environment: request.Environment,
  205. }
  206. secretsData, _, err := c.makeGetSecretsRequest(secretsrequest)
  207. if err != nil {
  208. return err
  209. }
  210. secrets, err := c.mapSecretsByPlainKey(secretsData.Data)
  211. if err != nil {
  212. return err
  213. }
  214. secret, ok := secrets[request.Name]
  215. if !ok || secret.ID == "" {
  216. return nil
  217. }
  218. params := request.buildQueryParams()
  219. deleteSecretDto := &DeleteSecretsRequest{
  220. SecretID: secret.ID,
  221. }
  222. body, jsonErr := json.Marshal(deleteSecretDto)
  223. if jsonErr != nil {
  224. return &APIError{Err: jsonErr, Message: "unable to unmarshal delete secrets payload"}
  225. }
  226. _, err = c.performRequest(&performRequestConfig{
  227. path: ObbSecretsEndpointPath,
  228. method: "DELETE",
  229. headers: headers{},
  230. params: params,
  231. body: body,
  232. })
  233. if err != nil {
  234. return err
  235. }
  236. return nil
  237. }
  238. func (c *OnboardbaseClient) makeGetSecretsRequest(request SecretsRequest) (*secretResponseBody, *apiResponse, error) {
  239. response, apiErr := c.performRequest(&performRequestConfig{
  240. path: ObbSecretsEndpointPath,
  241. method: "GET",
  242. headers: headers{},
  243. params: request.buildQueryParams(),
  244. body: httpRequestBody{},
  245. })
  246. if apiErr != nil {
  247. return nil, nil, apiErr
  248. }
  249. var data *secretResponseBody
  250. if err := json.Unmarshal(response.Body, &data); err != nil {
  251. return nil, nil, &APIError{Err: err, Message: "unable to unmarshal secret payload", Data: string(response.Body)}
  252. }
  253. return data, response, nil
  254. }
  255. func (c *OnboardbaseClient) GetSecrets(request SecretsRequest) (*SecretsResponse, error) {
  256. data, response, err := c.makeGetSecretsRequest(request)
  257. if err != nil {
  258. return nil, err
  259. }
  260. secrets, _ := c.getSecretsFromPayload(data.Data)
  261. return &SecretsResponse{Secrets: secrets, Body: response.Body}, nil
  262. }
  263. func (r *SecretsRequest) buildQueryParams() queryParams {
  264. params := queryParams{}
  265. if r.Project != "" {
  266. params["project"] = r.Project
  267. }
  268. if r.Environment != "" {
  269. params["environment"] = r.Environment
  270. }
  271. return params
  272. }
  273. func (r *SecretRequest) buildQueryParams() queryParams {
  274. params := queryParams{}
  275. if r.Project != "" {
  276. params["project"] = r.Project
  277. }
  278. if r.Environment != "" {
  279. params["environment"] = r.Environment
  280. }
  281. return params
  282. }
  283. type performRequestConfig struct {
  284. path string
  285. method string
  286. headers headers
  287. params queryParams
  288. body httpRequestBody
  289. }
  290. func (c *OnboardbaseClient) performRequest(config *performRequestConfig) (*apiResponse, error) {
  291. urlStr := c.BaseURL().String() + config.path
  292. reqURL, err := url.Parse(urlStr)
  293. if err != nil {
  294. return nil, &APIError{Err: err, Message: fmt.Sprintf("invalid API URL: %s", urlStr)}
  295. }
  296. var bodyReader io.Reader
  297. if config.body != nil {
  298. bodyReader = bytes.NewReader(config.body)
  299. } else {
  300. bodyReader = http.NoBody
  301. }
  302. // timeout this request after 20 seconds
  303. ctx, cancel := context.WithTimeout(context.Background(), HTTPTimeoutDuration)
  304. defer cancel()
  305. req, err := http.NewRequestWithContext(ctx, config.method, reqURL.String(), bodyReader)
  306. if err != nil {
  307. return nil, &APIError{Err: err, Message: "unable to form HTTP request"}
  308. }
  309. req.Header.Set("content-type", "application/json")
  310. req.Header.Set("user-agent", c.UserAgent)
  311. req.Header.Set("api_key", c.OnboardbaseAPIKey)
  312. for key, value := range config.headers {
  313. req.Header.Set(key, value)
  314. }
  315. query := req.URL.Query()
  316. for key, value := range config.params {
  317. query.Add(key, value)
  318. }
  319. req.URL.RawQuery = query.Encode()
  320. r, err := c.httpClient.Do(req)
  321. if err != nil {
  322. return nil, &APIError{Err: err, Message: "unable to load response"}
  323. }
  324. defer func() {
  325. _ = r.Body.Close()
  326. }()
  327. bodyResponse, err := io.ReadAll(r.Body)
  328. if err != nil {
  329. return nil, &APIError{Err: err, Message: "unable to read entire response body"}
  330. }
  331. response := &apiResponse{HTTPResponse: r, Body: bodyResponse}
  332. success := isSuccess(r.StatusCode)
  333. if !success {
  334. return handlePerformRequestFailure(response)
  335. }
  336. if success && err != nil {
  337. return nil, &APIError{Err: err, Message: "unable to load data from successful response"}
  338. }
  339. return response, nil
  340. }
  341. func handlePerformRequestFailure(response *apiResponse) (*apiResponse, *APIError) {
  342. if contentType := response.HTTPResponse.Header.Get("content-type"); strings.HasPrefix(contentType, "application/json") {
  343. var errResponse apiErrorResponse
  344. err := json.Unmarshal(response.Body, &errResponse)
  345. if err != nil {
  346. return response, &APIError{Err: err, Message: "unable to unmarshal error JSON payload"}
  347. }
  348. return response, &APIError{Err: nil, Message: strings.Join(errResponse.Messages, "\n")}
  349. }
  350. return nil, &APIError{Err: fmt.Errorf("%d status code; %d bytes", response.HTTPResponse.StatusCode, len(response.Body)), Message: "unable to load response"}
  351. }
  352. func isSuccess(statusCode int) bool {
  353. return (statusCode >= 200 && statusCode <= 299) || (statusCode >= 300 && statusCode <= 399)
  354. }
  355. func (e *APIError) Error() string {
  356. message := fmt.Sprintf("Onboardbase API Client Error: %s", e.Message)
  357. if underlyingError := e.Err; underlyingError != nil {
  358. message = fmt.Sprintf("%s\n%s", message, underlyingError.Error())
  359. }
  360. if e.Data != "" {
  361. message = fmt.Sprintf("%s\nData: %s", message, e.Data)
  362. }
  363. return message
  364. }