From 2a31ce632030fd359d6bb718eb533a4e13302cd6 Mon Sep 17 00:00:00 2001 From: Maxime Lagresle Date: Tue, 29 Oct 2024 09:59:26 +0100 Subject: [PATCH 1/2] log http method on errors --- internal/bitwarden/webapi/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/bitwarden/webapi/client.go b/internal/bitwarden/webapi/client.go index 2b59181..7d6f15f 100644 --- a/internal/bitwarden/webapi/client.go +++ b/internal/bitwarden/webapi/client.go @@ -573,7 +573,7 @@ func doRequest[T any](ctx context.Context, httpClient *http.Client, httpReq *htt tflog.Trace(ctx, "Response from Bitwarden server", debugInfo) if resp.StatusCode != 200 { - return nil, fmt.Errorf("bad response status code for '%s': %d!=200, body:%s", httpReq.URL, resp.StatusCode, string(body)) + return nil, fmt.Errorf("bad response status code for '%s %s': %d!=200, body:%s", httpReq.Method, httpReq.URL, resp.StatusCode, string(body)) } var res T From 75ff8f137be58346678f6de34b8e4c51ef1b5ed3 Mon Sep 17 00:00:00 2001 From: Maxime Lagresle Date: Wed, 30 Oct 2024 08:36:04 +0100 Subject: [PATCH 2/2] implement test secrets manager --- go.mod | 1 + go.sum | 2 + .../crypto/keybuilder/encryption_key.go | 10 +- .../bitwarden/embedded/secrets_manager.go | 9 +- .../embedded/secrets_manager_test.go | 16 - .../bitwarden/test/test_secrets_manager.go | 441 ++++++++++++++++++ internal/provider/provider_utils_test.go | 46 +- internal/provider/resource_secret_test.go | 37 +- 8 files changed, 514 insertions(+), 48 deletions(-) delete mode 100644 internal/bitwarden/embedded/secrets_manager_test.go create mode 100644 internal/bitwarden/test/test_secrets_manager.go diff --git a/go.mod b/go.mod index 3a579d9..2fea2b1 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23 require ( github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/uuid v1.6.0 + github.com/gorilla/mux v1.8.1 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/terraform-plugin-docs v0.19.4 github.com/hashicorp/terraform-plugin-log v0.9.0 diff --git a/go.sum b/go.sum index d01abe3..72f5eb1 100644 --- a/go.sum +++ b/go.sum @@ -65,6 +65,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/hashicorp/cli v1.1.6 h1:CMOV+/LJfL1tXCOKrgAX0uRKnzjj/mpmqNXloRSy2K8= github.com/hashicorp/cli v1.1.6/go.mod h1:MPon5QYlgjjo0BSoAiN0ESeT5fRzDjVRp+uioJ0piz4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/internal/bitwarden/crypto/keybuilder/encryption_key.go b/internal/bitwarden/crypto/keybuilder/encryption_key.go index 477b023..1dad30b 100644 --- a/internal/bitwarden/crypto/keybuilder/encryption_key.go +++ b/internal/bitwarden/crypto/keybuilder/encryption_key.go @@ -3,7 +3,6 @@ package keybuilder import ( "crypto/rand" "crypto/sha256" - "encoding/base64" "fmt" "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/crypto" @@ -41,13 +40,8 @@ func EncryptEncryptionKey(key symmetrickey.Key, encryptionKey []byte) (newEncryp return newEncryptionKey, encryptedEncryptionKey, err } -func DeriveFromAccessTokenEncryptionKey(accessToken string) (*symmetrickey.Key, error) { - accessTokenEncryptionKey, err := base64.StdEncoding.DecodeString(accessToken) - if err != nil { - return nil, fmt.Errorf("error base64 decoding access token encryption key: %w", err) - } - - extractedKey := helpers.HMACSum(accessTokenEncryptionKey, []byte("bitwarden-accesstoken"), sha256.New) +func DeriveFromAccessTokenEncryptionKey(accessToken []byte) (*symmetrickey.Key, error) { + extractedKey := helpers.HMACSum(accessToken, []byte("bitwarden-accesstoken"), sha256.New) expandedKey := helpers.HKDFExpand(extractedKey, []byte("sm-access-token"), sha256.New, 64) return symmetrickey.NewFromRawBytesWithEncryptionType(expandedKey, symmetrickey.AesCbc256_HmacSha256_B64) diff --git a/internal/bitwarden/embedded/secrets_manager.go b/internal/bitwarden/embedded/secrets_manager.go index 8bc7e4c..013f42a 100644 --- a/internal/bitwarden/embedded/secrets_manager.go +++ b/internal/bitwarden/embedded/secrets_manager.go @@ -349,7 +349,7 @@ func parseAccessToken(accessToken string) (string, string, *symmetrickey.Key, er } credentialsPart := accessTokenParts1[0] - encryptionPart := accessTokenParts1[1] + base64EncodedEncryptionKey := accessTokenParts1[1] accessTokenParts := strings.Split(credentialsPart, ".") if len(accessTokenParts) != 3 { @@ -363,7 +363,12 @@ func parseAccessToken(accessToken string) (string, string, *symmetrickey.Key, er clientId := accessTokenParts[1] clientSecret := accessTokenParts[2] - userEncryptionKey, err := keybuilder.DeriveFromAccessTokenEncryptionKey(encryptionPart) + accessTokenEncryptionKey, err := base64.StdEncoding.DecodeString(base64EncodedEncryptionKey) + if err != nil { + return "", "", nil, fmt.Errorf("error base64 decoding access token encryption key: %w", err) + } + + userEncryptionKey, err := keybuilder.DeriveFromAccessTokenEncryptionKey(accessTokenEncryptionKey) if err != nil { return "", "", nil, fmt.Errorf("error creating symmetric key: %w", err) } diff --git a/internal/bitwarden/embedded/secrets_manager_test.go b/internal/bitwarden/embedded/secrets_manager_test.go deleted file mode 100644 index fecb5b5..0000000 --- a/internal/bitwarden/embedded/secrets_manager_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package embedded - -import ( - "github.com/jarcoal/httpmock" - "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/embedded/fixtures" - "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/webapi" -) - -func newMockedSecretsManager(client webapi.Client) (secretsManager, func()) { - httpmock.Activate() - - return secretsManager{ - serverURL: fixtures.ServerURL, - client: client, - }, httpmock.DeactivateAndReset -} diff --git a/internal/bitwarden/test/test_secrets_manager.go b/internal/bitwarden/test/test_secrets_manager.go new file mode 100644 index 0000000..80b1812 --- /dev/null +++ b/internal/bitwarden/test/test_secrets_manager.go @@ -0,0 +1,441 @@ +package test + +import ( + "context" + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/gorilla/mux" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/crypto" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/crypto/keybuilder" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/crypto/symmetrickey" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/embedded" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/models" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/webapi" +) + +const ( + jwtSigningSecret = "secret-which-does-not-matter" +) + +func NewTestSecretsManager() *testSecretsManager { + return &testSecretsManager{ + clientSideInformation: ClientSideInformation{ + orgEncryptionKeys: map[string]symmetrickey.Key{}, + }, + issuedJwtTokens: map[string]string{}, + knownClients: map[string]Clients{}, + knownOrganizations: map[string]string{}, + secretsStore: map[string]webapi.Secret{}, + } +} + +type testSecretsManager struct { + clientSideInformation ClientSideInformation + issuedJwtTokens map[string]string + knownClients map[string]Clients + knownOrganizations map[string]string + secretsStore map[string]webapi.Secret +} + +type Clients struct { + ClientID string + ClientSecret string + EncryptedPayload string + OrganizationID string +} + +type ClientSideInformation struct { + orgEncryptionKeys map[string]symmetrickey.Key +} + +type CreateAccessTokenRequest struct { + EncryptedPayload string +} + +type CreateAccessTokenResponse struct { + ClientSecret string + Id string + Object string +} + +func (tsm *testSecretsManager) Run(ctx context.Context, serverPort int) { + handler := mux.NewRouter() + handler.HandleFunc("/api/organizations/{orgId}/secrets", tsm.handlerCreateGetSecret).Methods("POST", "GET") + handler.HandleFunc("/api/secrets/{secretId}", tsm.handlerGetSecret).Methods("GET") + handler.HandleFunc("/api/secrets/{secretId}", tsm.handlerEditSecret).Methods("PUT") + handler.HandleFunc("/api/secrets/delete", tsm.handlerDeleteSecret).Methods("POST") + handler.HandleFunc("/identity/connect/token", tsm.handlerLogin).Methods("POST") + + server := &http.Server{ + Handler: handler, + Addr: fmt.Sprintf(":%d", serverPort), + } + + go func() { + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + fmt.Printf("ListenAndServe(): %v\n", err) + } + }() + + <-ctx.Done() + + server.Shutdown(context.Background()) +} + +func (tsm *testSecretsManager) ClientCreateNewOrganization() (string, string, error) { + encryptionKey, err := generateOrganizationKey() + if err != nil { + return "", "", err + } + + orgId := uuid.New().String() + defaultProjectId := uuid.New().String() + + tsm.clientSideInformation.orgEncryptionKeys[orgId] = *encryptionKey + tsm.knownOrganizations[orgId] = defaultProjectId + return orgId, tsm.knownOrganizations[orgId], nil +} + +func (tsm *testSecretsManager) ClientCreateAccessToken(orgId string) (string, error) { + orgKey, v := tsm.clientSideInformation.orgEncryptionKeys[orgId] + if !v { + return "", fmt.Errorf("organization not found") + } + + accessTokenEncryptionKey, err := generateAccessTokenEncryptionKey() + if err != nil { + return "", err + } + + encryptedPayload, err := encryptPayload(accessTokenEncryptionKey, orgKey) + if err != nil { + return "", fmt.Errorf("error encrypting payload: %w", err) + } + + request := CreateAccessTokenRequest{ + EncryptedPayload: encryptedPayload, + } + + response, err := tsm.createAccessToken(orgId, request) + if err != nil { + return "", fmt.Errorf("error creating access token: %w", err) + } + + return fmt.Sprintf("0.%s.%s:%s", response.Id, response.ClientSecret, base64.StdEncoding.EncodeToString(accessTokenEncryptionKey)), nil +} + +func (tsm *testSecretsManager) createAccessToken(orgId string, request CreateAccessTokenRequest) (*CreateAccessTokenResponse, error) { + clientSecretBytes, err := generateClientSecret() + if err != nil { + return nil, err + } + + client := Clients{ + ClientID: uuid.New().String(), + ClientSecret: base64.StdEncoding.EncodeToString(clientSecretBytes), + OrganizationID: orgId, + EncryptedPayload: request.EncryptedPayload, + } + tsm.knownClients[client.ClientID] = client + + return &CreateAccessTokenResponse{ + Id: client.ClientID, + ClientSecret: client.ClientSecret, + }, nil +} + +func (tsm *testSecretsManager) handlerLogin(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, "Failed to parse form", http.StatusBadRequest) + return + } + + providedClientId := r.FormValue("client_id") + client, clientExists := tsm.knownClients[providedClientId] + if !clientExists { + http.Error(w, "Invalid client id", http.StatusBadRequest) + return + } + + providedClientSecret := r.FormValue("client_secret") + if client.ClientSecret != providedClientSecret { + http.Error(w, "Invalid client secret", http.StatusBadRequest) + return + } + + _, orgExists := tsm.knownOrganizations[client.OrganizationID] + if !orgExists { + http.Error(w, "Invalid organization", http.StatusBadRequest) + return + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, &embedded.MachineAccountClaims{ + Organization: client.OrganizationID, + }) + jwtAccessToken, err := token.SignedString([]byte(jwtSigningSecret)) + if err != nil { + http.Error(w, "error generating jwt token: %w", http.StatusBadRequest) + return + } + + tsm.issuedJwtTokens[jwtAccessToken] = client.ClientID + + response := webapi.MachineTokenResponse{ + AccessToken: jwtAccessToken, + ExpireIn: 3600, + TokenType: "Bearer", + EncryptedPayload: string(client.EncryptedPayload), + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + } +} + +func (tsm *testSecretsManager) handlerCreateGetSecret(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + tsm.handlerGetSecrets(w, r) + } else { + tsm.handlerCreateSecret(w, r) + } +} + +func (tsm *testSecretsManager) handlerCreateSecret(w http.ResponseWriter, r *http.Request) { + orgId := mux.Vars(r)["orgId"] + orgDefaultProjectID, v := tsm.knownOrganizations[orgId] + if !v { + http.Error(w, "Invalid organization", http.StatusBadRequest) + return + } + + err := tsm.checkAuthentication(r.Header.Get("Authorization")) + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + + defer r.Body.Close() + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Failed to read request body", http.StatusBadRequest) + return + } + + var secretCreationRequest webapi.CreateSecretRequest + if err := json.Unmarshal(body, &secretCreationRequest); err != nil { + http.Error(w, "Failed to unmarshal request body", http.StatusBadRequest) + return + } + + secret := webapi.Secret{ + SecretSummary: webapi.SecretSummary{ + ID: uuid.New().String(), + OrganizationID: orgId, + Key: secretCreationRequest.Key, + CreationDate: time.Now(), + RevisionDate: time.Now(), + Projects: []webapi.Project{{ + OrganizationID: orgId, + ID: orgDefaultProjectID, + }}, + Read: true, + Write: true, + }, + Value: secretCreationRequest.Value, + Note: secretCreationRequest.Note, + Object: string(models.ObjectSecret), + } + tsm.secretsStore[secret.ID] = secret + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(secret); err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + } +} + +func (tsm *testSecretsManager) handlerGetSecrets(w http.ResponseWriter, r *http.Request) { + orgId := mux.Vars(r)["orgId"] + + secretList := webapi.SecretsWithProjectsList{} + for _, v := range tsm.secretsStore { + if v.OrganizationID != orgId { + continue + } + + sum := webapi.SecretSummary{ + ID: v.ID, + OrganizationID: v.OrganizationID, + Key: v.Key, + CreationDate: v.CreationDate, + RevisionDate: v.RevisionDate, + Projects: v.Projects, + Read: v.Read, + Write: v.Write, + } + secretList.Secrets = append(secretList.Secrets, sum) + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(secretList); err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + } +} + +func (tsm *testSecretsManager) handlerGetSecret(w http.ResponseWriter, r *http.Request) { + err := tsm.checkAuthentication(r.Header.Get("Authorization")) + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + + secretId := mux.Vars(r)["secretId"] + secret, secretExists := tsm.secretsStore[secretId] + if !secretExists { + http.Error(w, "Secret not found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(secret); err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + } +} + +func (tsm *testSecretsManager) handlerEditSecret(w http.ResponseWriter, r *http.Request) { + err := tsm.checkAuthentication(r.Header.Get("Authorization")) + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + + secretId := mux.Vars(r)["secretId"] + secret, secretExists := tsm.secretsStore[secretId] + if !secretExists { + http.Error(w, "Secret not found", http.StatusNotFound) + return + } + + defer r.Body.Close() + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Failed to read request body", http.StatusBadRequest) + return + } + + var secretCreationRequest webapi.CreateSecretRequest + if err := json.Unmarshal(body, &secretCreationRequest); err != nil { + http.Error(w, "Failed to unmarshal request body", http.StatusBadRequest) + return + } + + secret.RevisionDate = time.Now() + secret.Key = secretCreationRequest.Key + secret.Value = secretCreationRequest.Value + secret.Note = secretCreationRequest.Note + tsm.secretsStore[secretId] = secret + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(secret); err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + } +} + +func (tsm *testSecretsManager) handlerDeleteSecret(w http.ResponseWriter, r *http.Request) { + err := tsm.checkAuthentication(r.Header.Get("Authorization")) + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + + defer r.Body.Close() + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Failed to read request body", http.StatusBadRequest) + return + } + + var IDs []string + if err := json.Unmarshal(body, &IDs); err != nil { + http.Error(w, "Failed to unmarshal request body", http.StatusBadRequest) + return + } + + for _, v := range IDs { + delete(tsm.secretsStore, v) + } + w.WriteHeader(http.StatusOK) +} + +func (tsm *testSecretsManager) checkAuthentication(authorization string) error { + if authorization == "" { + return fmt.Errorf("missing Authorization header") + } + + authorization = authorization[7:] + clientID, jwtTokenKnown := tsm.issuedJwtTokens[authorization] + if !jwtTokenKnown { + return fmt.Errorf("invalid token") + } + + _, clientKnown := tsm.knownClients[clientID] + if !clientKnown { + return fmt.Errorf("client doesn't exist anymore") + } + return nil +} + +func encryptPayload(accessTokenEncryptionKey []byte, orgEncryptionKey symmetrickey.Key) (string, error) { + payload := webapi.MachineTokenEncryptedPayload{ + EncryptionKey: base64.StdEncoding.EncodeToString(orgEncryptionKey.Key), + } + payloadRaw, err := json.Marshal(payload) + if err != nil { + return "", fmt.Errorf("error marshalling payload: %w", err) + } + + accessKeyEncryptionKey, err := keybuilder.DeriveFromAccessTokenEncryptionKey(accessTokenEncryptionKey) + if err != nil { + return "", fmt.Errorf("error deriving access key: %w", err) + } + + encryptedPayloadRaw, err := crypto.Encrypt(payloadRaw, *accessKeyEncryptionKey) + if err != nil { + return "", fmt.Errorf("error encrypting payload: %w", err) + } + return encryptedPayloadRaw.String(), nil +} + +func generateOrganizationKey() (*symmetrickey.Key, error) { + encryptionKey := make([]byte, 64) + _, err := rand.Read(encryptionKey) + if err != nil { + return nil, fmt.Errorf("error generating organization key: %w", err) + } + return symmetrickey.NewFromRawBytes(encryptionKey) +} + +func generateAccessTokenEncryptionKey() ([]byte, error) { + encryptionKey := make([]byte, 64) + _, err := rand.Read(encryptionKey) + if err != nil { + return nil, fmt.Errorf("error generating organization key: %w", err) + } + return encryptionKey, nil +} + +func generateClientSecret() ([]byte, error) { + clientSecretBytes := make([]byte, 64) + _, err := rand.Read(clientSecretBytes) + if err != nil { + return nil, fmt.Errorf("error generating client secret: %w", err) + } + return clientSecretBytes, nil +} diff --git a/internal/provider/provider_utils_test.go b/internal/provider/provider_utils_test.go index 8d02671..7f574c8 100644 --- a/internal/provider/provider_utils_test.go +++ b/internal/provider/provider_utils_test.go @@ -18,6 +18,7 @@ import ( "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/bwcli" "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/embedded" "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/models" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/test" "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/webapi" ) @@ -36,7 +37,6 @@ var testServerURL string var testOrganizationID string var testCollectionID string var testFolderID string -var testProjectId string var testUniqueIdentifier string var useEmbeddedClient bool @@ -255,9 +255,47 @@ func tfConfigPasswordManagerProvider() string { `, testPassword, testServerURL, testEmail, useEmbeddedClientStr) } -func tfConfigSecretsManagerProvider() string { +func testOrRealSecretsManagerProvider(t *testing.T) (string, string, func()) { + tfProvider, testProjectId := tfConfigSecretsManagerProvider() + if len(testProjectId) > 0 { + stop := func() {} + return tfProvider, testProjectId, stop + } else { + return spawnTestSecretsManager(t) + } +} + +func spawnTestSecretsManager(t *testing.T) (string, string, func()) { + testSecretsManager := test.NewTestSecretsManager() + ctx, stop := context.WithCancel(context.Background()) + go testSecretsManager.Run(ctx, 8081) + + orgId, testProjectId, err := testSecretsManager.ClientCreateNewOrganization() + if err != nil { + t.Fatal(err) + } + + accessToken, err := testSecretsManager.ClientCreateAccessToken(orgId) + if err != nil { + t.Fatal(err) + } + + providerConfiguration := fmt.Sprintf(` + provider "bitwarden" { + access_token = "%s" + server = "http://localhost:8081" + + experimental { + embedded_client = true + } + } + `, accessToken) + return providerConfiguration, testProjectId, stop +} + +func tfConfigSecretsManagerProvider() (string, string) { accessToken := os.Getenv("TEST_REAL_BWS_ACCESS_TOKEN") - testProjectId = os.Getenv("TEST_REAL_BWS_PROJECT_ID") + testProjectId := os.Getenv("TEST_REAL_BWS_PROJECT_ID") return fmt.Sprintf(` provider "bitwarden" { access_token = "%s" @@ -266,7 +304,7 @@ func tfConfigSecretsManagerProvider() string { embedded_client = true } } -`, accessToken) +`, accessToken), testProjectId } func getObjectID(n string, objectId *string) resource.TestCheckFunc { diff --git a/internal/provider/resource_secret_test.go b/internal/provider/resource_secret_test.go index 6ca5dc2..246a5f7 100644 --- a/internal/provider/resource_secret_test.go +++ b/internal/provider/resource_secret_test.go @@ -9,15 +9,17 @@ import ( ) func TestResourceSecretSchema(t *testing.T) { + tfProvider, _ := tfConfigSecretsManagerProvider() + resource.Test(t, resource.TestCase{ ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: tfConfigSecretsManagerProvider() + tfConfigDataSecretWithoutAnyInput(), + Config: tfProvider + tfConfigDataSecretWithoutAnyInput(), ExpectError: regexp.MustCompile("Error: Missing required argument"), }, { - Config: tfConfigSecretsManagerProvider() + tfConfigDataSecretTooManyInput(), + Config: tfProvider + tfConfigDataSecretTooManyInput(), ExpectError: regexp.MustCompile(": conflicts"), }, }, @@ -25,50 +27,49 @@ func TestResourceSecretSchema(t *testing.T) { } func TestResourceSecret(t *testing.T) { - tfConfigSecretsManagerProvider() - if len(testProjectId) == 0 { - t.Skip("Skipping test due to missing project_id") - } + tfProvider, testProjectId, stop := testOrRealSecretsManagerProvider(t) + defer stop() + resource.Test(t, resource.TestCase{ ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: tfConfigSecretsManagerProvider() + tfConfigDataSecretWithoutAnyInput(), + Config: tfProvider + tfConfigDataSecretWithoutAnyInput(), ExpectError: regexp.MustCompile("Error: Missing required argument"), }, { - Config: tfConfigSecretsManagerProvider() + tfConfigDataSecretTooManyInput(), + Config: tfProvider + tfConfigDataSecretTooManyInput(), ExpectError: regexp.MustCompile(": conflicts"), }, { - Config: tfConfigSecretsManagerProvider() + tfConfigResourceSecret("foo"), - Check: checkSecret("bitwarden_secret.foo"), + Config: tfProvider + tfConfigResourceSecret("foo", testProjectId), + Check: checkSecret("bitwarden_secret.foo", testProjectId), }, // Test Sourcing Secret by ID { - Config: tfConfigSecretsManagerProvider() + tfConfigResourceSecret("foo") + tfConfigDataSecretByID("bitwarden_secret.foo.id"), - Check: checkSecret("data.bitwarden_secret.foo_data"), + Config: tfProvider + tfConfigResourceSecret("foo", testProjectId) + tfConfigDataSecretByID("bitwarden_secret.foo.id"), + Check: checkSecret("data.bitwarden_secret.foo_data", testProjectId), }, // Test Sourcing Secret by ID with NO MATCH { - Config: tfConfigSecretsManagerProvider() + tfConfigResourceSecret("foo") + tfConfigDataSecretByID("\"27a0007a-a517-4f25-8c2e-baf31ca3b034\""), + Config: tfProvider + tfConfigResourceSecret("foo", testProjectId) + tfConfigDataSecretByID("\"27a0007a-a517-4f25-8c2e-baf31ca3b034\""), ExpectError: regexp.MustCompile("Error: object not found"), }, // Test Sourcing Secret by KEY { - Config: tfConfigSecretsManagerProvider() + tfConfigResourceSecret("foo") + tfConfigDataSecretByKey(), - Check: checkSecret("data.bitwarden_secret.foo_data"), + Config: tfProvider + tfConfigResourceSecret("foo", testProjectId) + tfConfigDataSecretByKey(), + Check: checkSecret("data.bitwarden_secret.foo_data", testProjectId), }, // Test Sourcing Secret with MULTIPLE MATCHES { - Config: tfConfigSecretsManagerProvider() + tfConfigResourceSecret("foo") + tfConfigResourceSecret("foo2") + tfConfigDataSecretByKey(), + Config: tfProvider + tfConfigResourceSecret("foo", testProjectId) + tfConfigResourceSecret("foo2", testProjectId) + tfConfigDataSecretByKey(), ExpectError: regexp.MustCompile("Error: too many objects found"), }, }, }) } -func checkSecret(fullRessourceName string) resource.TestCheckFunc { +func checkSecret(fullRessourceName, testProjectId string) resource.TestCheckFunc { return resource.ComposeTestCheckFunc( resource.TestMatchResourceAttr(fullRessourceName, attributeID, regexp.MustCompile("^([a-z0-9-]+)$")), resource.TestCheckResourceAttr(fullRessourceName, attributeKey, "login-bar"), @@ -116,7 +117,7 @@ data "bitwarden_secret" "foo_data" { ` } -func tfConfigResourceSecret(resourceName string) string { +func tfConfigResourceSecret(resourceName, testProjectId string) string { return fmt.Sprintf(` resource "bitwarden_secret" "%s" { provider = bitwarden