| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 |
- /*
- Copyright © The ESO Authors
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- https://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- // Package bitwarden implements a secret manager provider for Bitwarden.
- package bitwarden
- import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "strings"
- "sigs.k8s.io/controller-runtime/pkg/client"
- esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
- )
- // Defined Header Keys.
- const (
- WardenHeaderAccessToken = "Warden-Access-Token"
- WardenHeaderAPIURL = "Warden-Api-Url"
- WardenHeaderIdentityURL = "Warden-Identity-Url"
- restAPIURL = "/rest/api/1/secret"
- )
- // SecretResponse represents a response from the Bitwarden API containing secret details.
- type SecretResponse struct {
- CreationDate string `json:"creationDate"`
- ID string `json:"id"`
- Key string `json:"key"`
- Note string `json:"note"`
- OrganizationID string `json:"organizationId"`
- ProjectID *string `json:"projectId,omitempty"`
- RevisionDate string `json:"revisionDate"`
- Value string `json:"value"`
- // fix ProjectIDS -> ProjectIDs
- ProjectIDs []string `json:"projectIds,omitempty"`
- }
- // SecretsDeleteResponse represents the response when deleting multiple secrets.
- type SecretsDeleteResponse struct {
- Data []SecretDeleteResponse `json:"data"`
- }
- // SecretDeleteResponse represents the response for a single secret deletion.
- type SecretDeleteResponse struct {
- Error *string `json:"error,omitempty"`
- ID string `json:"id"`
- }
- // SecretIdentifiersResponse represents the response when listing secret identifiers.
- type SecretIdentifiersResponse struct {
- Data []SecretIdentifierResponse `json:"data"`
- }
- // SecretIdentifierResponse represents a single secret identifier in a list response.
- type SecretIdentifierResponse struct {
- ID string `json:"id"`
- Key string `json:"key"`
- OrganizationID string `json:"organizationId"`
- }
- // SecretCreateRequest represents the request to create a new secret.
- type SecretCreateRequest struct {
- Key string `json:"key"`
- Note string `json:"note"`
- // Organization where the secret will be created
- OrganizationID string `json:"organizationId"`
- // IDs of the projects that this secret will belong to
- ProjectIDs []string `json:"projectIds,omitempty"` // Changed from ProjectIDS
- Value string `json:"value"`
- }
- // SecretPutRequest represents the request to update an existing secret.
- type SecretPutRequest struct {
- ID string `json:"id"`
- Key string `json:"key"`
- Note string `json:"note"`
- // Organization where the secret will be created
- OrganizationID string `json:"organizationId"`
- // IDs of the projects that this secret will belong to
- ProjectIDs []string `json:"projectIds,omitempty"` // Changed from ProjectIDS
- Value string `json:"value"`
- }
- // Client for the bitwarden SDK.
- type Client interface {
- GetSecret(ctx context.Context, id string) (*SecretResponse, error)
- DeleteSecret(ctx context.Context, ids []string) (*SecretsDeleteResponse, error)
- CreateSecret(ctx context.Context, secret SecretCreateRequest) (*SecretResponse, error)
- UpdateSecret(ctx context.Context, secret SecretPutRequest) (*SecretResponse, error)
- ListSecrets(ctx context.Context, organizationID string) (*SecretIdentifiersResponse, error)
- }
- // SdkClient creates a client to talk to the bitwarden SDK server.
- type SdkClient struct {
- apiURL string
- identityURL string
- token string
- bitwardenSdkServerURL string
- client *http.Client
- }
- // NewSdkClient creates a new Bitwarden SDK client instance.
- func NewSdkClient(ctx context.Context, c client.Client, storeKind, namespace string, provider *esv1.BitwardenSecretsManagerProvider, token string) (*SdkClient, error) {
- httpsClient, err := newHTTPSClient(ctx, c, storeKind, namespace, provider)
- if err != nil {
- return nil, fmt.Errorf("error creating https client: %w", err)
- }
- return &SdkClient{
- apiURL: strings.TrimSuffix(provider.APIURL, "/"),
- identityURL: strings.TrimSuffix(provider.IdentityURL, "/"),
- bitwardenSdkServerURL: provider.BitwardenServerSDKURL,
- token: token,
- client: httpsClient,
- }, nil
- }
- // GetSecret retrieves a secret from Bitwarden by its ID.
- func (s *SdkClient) GetSecret(ctx context.Context, id string) (*SecretResponse, error) {
- body := struct {
- ID string `json:"id"`
- }{
- ID: id,
- }
- secretResp := &SecretResponse{}
- if err := s.performHTTPRequestOperation(ctx, params{
- method: http.MethodGet,
- url: s.bitwardenSdkServerURL + restAPIURL,
- body: body,
- result: &secretResp,
- }); err != nil {
- return nil, fmt.Errorf("failed to get secret: %w", err)
- }
- return secretResp, nil
- }
- // DeleteSecret deletes secrets from Bitwarden by their IDs.
- func (s *SdkClient) DeleteSecret(ctx context.Context, ids []string) (*SecretsDeleteResponse, error) {
- body := struct {
- IDs []string `json:"ids"`
- }{
- IDs: ids,
- }
- secretResp := &SecretsDeleteResponse{}
- if err := s.performHTTPRequestOperation(ctx, params{
- method: http.MethodDelete,
- url: s.bitwardenSdkServerURL + restAPIURL,
- body: body,
- result: &secretResp,
- }); err != nil {
- return nil, fmt.Errorf("failed to delete secret: %w", err)
- }
- return secretResp, nil
- }
- // CreateSecret creates a new secret in Bitwarden.
- func (s *SdkClient) CreateSecret(ctx context.Context, createReq SecretCreateRequest) (*SecretResponse, error) {
- secretResp := &SecretResponse{}
- if err := s.performHTTPRequestOperation(ctx, params{
- method: http.MethodPost,
- url: s.bitwardenSdkServerURL + restAPIURL,
- body: createReq,
- result: &secretResp,
- }); err != nil {
- return nil, fmt.Errorf("failed to create secret: %w", err)
- }
- return secretResp, nil
- }
- // UpdateSecret updates an existing secret in Bitwarden.
- func (s *SdkClient) UpdateSecret(ctx context.Context, putReq SecretPutRequest) (*SecretResponse, error) {
- secretResp := &SecretResponse{}
- if err := s.performHTTPRequestOperation(ctx, params{
- method: http.MethodPut,
- url: s.bitwardenSdkServerURL + restAPIURL,
- body: putReq,
- result: &secretResp,
- }); err != nil {
- return nil, fmt.Errorf("failed to update secret: %w", err)
- }
- return secretResp, nil
- }
- // ListSecrets retrieves all secrets from a Bitwarden organization.
- func (s *SdkClient) ListSecrets(ctx context.Context, organizationID string) (*SecretIdentifiersResponse, error) {
- body := struct {
- ID string `json:"organizationID"`
- }{
- ID: organizationID,
- }
- secretResp := &SecretIdentifiersResponse{}
- if err := s.performHTTPRequestOperation(ctx, params{
- method: http.MethodGet,
- url: s.bitwardenSdkServerURL + "/rest/api/1/secrets",
- body: body,
- result: &secretResp,
- }); err != nil {
- return nil, fmt.Errorf("failed to list secrets: %w", err)
- }
- return secretResp, nil
- }
- func (s *SdkClient) constructSdkRequest(ctx context.Context, method, url string, body []byte) (*http.Request, error) {
- req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewBuffer(body))
- if err != nil {
- return nil, fmt.Errorf("failed to construct request: %w", err)
- }
- req.Header.Set(WardenHeaderAccessToken, s.token)
- req.Header.Set(WardenHeaderAPIURL, s.apiURL)
- req.Header.Set(WardenHeaderIdentityURL, s.identityURL)
- return req, nil
- }
- type params struct {
- method string
- url string
- body any
- result any
- }
- func (s *SdkClient) performHTTPRequestOperation(ctx context.Context, params params) error {
- data, err := json.Marshal(params.body)
- if err != nil {
- return fmt.Errorf("failed to marshal body: %w", err)
- }
- req, err := s.constructSdkRequest(ctx, params.method, params.url, data)
- if err != nil {
- return fmt.Errorf("failed to construct request: %w", err)
- }
- resp, err := s.client.Do(req)
- if err != nil {
- return fmt.Errorf("failed to do request: %w", err)
- }
- defer func() {
- _ = resp.Body.Close()
- }()
- if resp.StatusCode != http.StatusOK {
- content, _ := io.ReadAll(resp.Body)
- return fmt.Errorf("failed to perform http request, got response: %s with status code %d", string(content), resp.StatusCode)
- }
- decoder := json.NewDecoder(resp.Body)
- if err := decoder.Decode(¶ms.result); err != nil {
- return fmt.Errorf("failed to decode response: %w", err)
- }
- return nil
- }
|