api.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  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 impliec.
  9. See the License for the specific language governing permissions and
  10. limitations under the License.
  11. */
  12. package api
  13. import (
  14. "bytes"
  15. "encoding/json"
  16. "errors"
  17. "fmt"
  18. "io"
  19. "net/http"
  20. "net/url"
  21. "strconv"
  22. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  23. "github.com/external-secrets/external-secrets/pkg/metrics"
  24. "github.com/external-secrets/external-secrets/pkg/provider/infisical/constants"
  25. )
  26. type InfisicalClient struct {
  27. BaseURL *url.URL
  28. client *http.Client
  29. token string
  30. }
  31. type InfisicalApis interface {
  32. MachineIdentityLoginViaUniversalAuth(data MachineIdentityUniversalAuthLoginRequest) (*MachineIdentityDetailsResponse, error)
  33. GetSecretsV3(data GetSecretsV3Request) (map[string]string, error)
  34. GetSecretByKeyV3(data GetSecretByKeyV3Request) (string, error)
  35. RevokeAccessToken() error
  36. }
  37. const (
  38. machineIdentityLoginViaUniversalAuth = "MachineIdentityLoginViaUniversalAuth"
  39. getSecretsV3 = "GetSecretsV3"
  40. getSecretByKeyV3 = "GetSecretByKeyV3"
  41. revokeAccessToken = "RevokeAccessToken"
  42. )
  43. const UserAgentName = "k8-external-secrets-operator"
  44. var errJSONUnmarshal = errors.New("unable to unmarshal API response")
  45. var errNoAccessToken = errors.New("unexpected error: no access token available to revoke")
  46. var errAccessTokenAlreadyRetrieved = errors.New("unexpected error: access token was already retrieved")
  47. type InfisicalAPIError struct {
  48. StatusCode int
  49. Err any
  50. Message any
  51. Details any
  52. }
  53. func (e *InfisicalAPIError) Error() string {
  54. if e.Details != nil {
  55. detailsJSON, _ := json.Marshal(e.Details)
  56. return fmt.Sprintf("API error (%d): error=%v message=%v, details=%s", e.StatusCode, e.Err, e.Message, string(detailsJSON))
  57. } else {
  58. return fmt.Sprintf("API error (%d): error=%v message=%v", e.StatusCode, e.Err, e.Message)
  59. }
  60. }
  61. // checkError checks for an error on the http response and generates an appropriate error if one is
  62. // found.
  63. func checkError(resp *http.Response) error {
  64. if resp.StatusCode >= 200 && resp.StatusCode < 400 {
  65. return nil
  66. }
  67. var buf bytes.Buffer
  68. _, err := buf.ReadFrom(resp.Body)
  69. if err != nil {
  70. return fmt.Errorf("API error (%d) and failed to read response body: %w", resp.StatusCode, err)
  71. }
  72. // Attempt to unmarshal the response body into an InfisicalAPIErrorResponse.
  73. var errRes InfisicalAPIErrorResponse
  74. err = json.Unmarshal(buf.Bytes(), &errRes)
  75. // Non-200 errors that cannot be unmarshaled must be handled, as errors could come from outside of
  76. // Infisical.
  77. if err != nil {
  78. return fmt.Errorf("API error (%d), could not unmarshal error response: %w", resp.StatusCode, err)
  79. } else if errRes.StatusCode == 0 {
  80. // When the InfisicalResponse has a zero-value status code, then the
  81. // response was either malformed or not from Infisical. Instead, just return
  82. // the error string from the response.
  83. return fmt.Errorf("API error (%d): %s", resp.StatusCode, buf.String())
  84. } else {
  85. return &InfisicalAPIError{
  86. StatusCode: resp.StatusCode,
  87. Message: errRes.Message,
  88. Err: errRes.Error,
  89. Details: errRes.Details,
  90. }
  91. }
  92. }
  93. func NewAPIClient(baseURL string, client *http.Client) (*InfisicalClient, error) {
  94. baseParsedURL, err := url.Parse(baseURL)
  95. if err != nil {
  96. return nil, err
  97. }
  98. api := &InfisicalClient{
  99. BaseURL: baseParsedURL,
  100. client: client,
  101. }
  102. return api, nil
  103. }
  104. func (a *InfisicalClient) SetTokenViaMachineIdentity(clientID, clientSecret string) error {
  105. if a.token != "" {
  106. return errAccessTokenAlreadyRetrieved
  107. }
  108. var loginResponse MachineIdentityDetailsResponse
  109. err := a.do(
  110. "api/v1/auth/universal-auth/login",
  111. http.MethodPost,
  112. map[string]string{},
  113. MachineIdentityUniversalAuthLoginRequest{
  114. ClientID: clientID,
  115. ClientSecret: clientSecret,
  116. },
  117. &loginResponse,
  118. )
  119. metrics.ObserveAPICall(constants.ProviderName, machineIdentityLoginViaUniversalAuth, err)
  120. if err != nil {
  121. return err
  122. }
  123. a.token = loginResponse.AccessToken
  124. return nil
  125. }
  126. func (a *InfisicalClient) RevokeAccessToken() error {
  127. if a.token == "" {
  128. return errNoAccessToken
  129. }
  130. var revokeResponse RevokeMachineIdentityAccessTokenResponse
  131. err := a.do(
  132. "api/v1/auth/token/revoke",
  133. http.MethodPost,
  134. map[string]string{},
  135. RevokeMachineIdentityAccessTokenRequest{AccessToken: a.token},
  136. &revokeResponse,
  137. )
  138. metrics.ObserveAPICall(constants.ProviderName, revokeAccessToken, err)
  139. if err != nil {
  140. return err
  141. }
  142. a.token = ""
  143. return nil
  144. }
  145. func (a *InfisicalClient) resolveEndpoint(path string) string {
  146. return a.BaseURL.ResolveReference(&url.URL{Path: path}).String()
  147. }
  148. func (a *InfisicalClient) addHeaders(r *http.Request) {
  149. if a.token != "" {
  150. r.Header.Add("Authorization", "Bearer "+a.token)
  151. }
  152. r.Header.Add("User-Agent", UserAgentName)
  153. r.Header.Add("Content-Type", "application/json")
  154. }
  155. // do is a generic function that makes an API call to the Infisical API, and handle the response
  156. // (including if an API error is returned).
  157. func (a *InfisicalClient) do(endpoint, method string, params map[string]string, body, response any) error {
  158. endpointURL := a.resolveEndpoint(endpoint)
  159. bodyReader, err := MarshalReqBody(body)
  160. if err != nil {
  161. return err
  162. }
  163. r, err := http.NewRequest(method, endpointURL, bodyReader)
  164. if err != nil {
  165. return err
  166. }
  167. a.addHeaders(r)
  168. q := r.URL.Query()
  169. for key, value := range params {
  170. q.Add(key, value)
  171. }
  172. r.URL.RawQuery = q.Encode()
  173. resp, err := a.client.Do(r)
  174. if err != nil {
  175. return err
  176. }
  177. defer func() {
  178. _ = resp.Body.Close()
  179. }()
  180. if err := checkError(resp); err != nil {
  181. return err
  182. }
  183. bodyBytes, err := io.ReadAll(resp.Body)
  184. if err != nil {
  185. return err
  186. }
  187. err = json.Unmarshal(bodyBytes, response)
  188. if err != nil {
  189. // Importantly, we do not include the response in the actual error to avoid
  190. // leaking anything sensitive.
  191. return errJSONUnmarshal
  192. }
  193. return nil
  194. }
  195. func (a *InfisicalClient) GetSecretsV3(data GetSecretsV3Request) (map[string]string, error) {
  196. params := map[string]string{
  197. "workspaceSlug": data.ProjectSlug,
  198. "environment": data.EnvironmentSlug,
  199. "secretPath": data.SecretPath,
  200. "include_imports": "true",
  201. "expandSecretReferences": strconv.FormatBool(data.ExpandSecretReferences),
  202. "recursive": strconv.FormatBool(data.Recursive),
  203. }
  204. res := GetSecretsV3Response{}
  205. err := a.do(
  206. "api/v3/secrets/raw",
  207. http.MethodGet,
  208. params,
  209. http.NoBody,
  210. &res,
  211. )
  212. metrics.ObserveAPICall(constants.ProviderName, getSecretsV3, err)
  213. if err != nil {
  214. return nil, err
  215. }
  216. secrets := make(map[string]string)
  217. for _, s := range res.ImportedSecrets {
  218. for _, el := range s.Secrets {
  219. secrets[el.SecretKey] = el.SecretValue
  220. }
  221. }
  222. for _, el := range res.Secrets {
  223. secrets[el.SecretKey] = el.SecretValue
  224. }
  225. return secrets, nil
  226. }
  227. func (a *InfisicalClient) GetSecretByKeyV3(data GetSecretByKeyV3Request) (string, error) {
  228. params := map[string]string{
  229. "workspaceSlug": data.ProjectSlug,
  230. "environment": data.EnvironmentSlug,
  231. "secretPath": data.SecretPath,
  232. "include_imports": "true",
  233. "expandSecretReferences": strconv.FormatBool(data.ExpandSecretReferences),
  234. }
  235. endpointURL := fmt.Sprintf("api/v3/secrets/raw/%s", data.SecretKey)
  236. res := GetSecretByKeyV3Response{}
  237. err := a.do(
  238. endpointURL,
  239. http.MethodGet,
  240. params,
  241. http.NoBody,
  242. &res,
  243. )
  244. metrics.ObserveAPICall(constants.ProviderName, getSecretByKeyV3, err)
  245. if err != nil {
  246. var apiErr *InfisicalAPIError
  247. if errors.As(err, &apiErr) && apiErr.StatusCode == 404 {
  248. return "", esv1.NoSecretError{}
  249. }
  250. return "", err
  251. }
  252. return res.Secret.SecretValue, nil
  253. }
  254. func MarshalReqBody(data any) (*bytes.Reader, error) {
  255. body, err := json.Marshal(data)
  256. if err != nil {
  257. return nil, err
  258. }
  259. return bytes.NewReader(body), nil
  260. }