diff --git a/internal/helpers/cert.go b/internal/helpers/cert.go index 0f6d26b6..8087ef99 100644 --- a/internal/helpers/cert.go +++ b/internal/helpers/cert.go @@ -12,6 +12,44 @@ import ( pkcs12 "software.sslmate.com/src/go-pkcs12" ) +// ParseCertificateData decodes and parses PKCS#12 data, extracting certificates and a private key. +// +// This function attempts to decode PKCS#12 data using the provided password. It extracts +// the certificate chain (including the end-entity certificate and any CA certificates), +// as well as the private key associated with the end-entity certificate. +// +// The function performs several validations: +// - It checks if any certificates are present in the decoded data. +// - It verifies the presence of a private key. +// - It ensures the private key is of RSA type. +// +// The function logs debug, error, and info messages at various stages of the process. +// +// Parameters: +// - ctx: A context.Context for logging and potential cancellation. +// - certData: A byte slice containing the PKCS#12 data to be parsed. +// - password: A byte slice containing the password to decrypt the PKCS#12 data. +// +// Returns: +// - []*x509.Certificate: A slice of parsed X.509 certificates, with the end-entity +// certificate as the first element, followed by any CA certificates. +// - crypto.PrivateKey: The private key associated with the end-entity certificate. +// - error: An error if any step of the parsing or validation process fails. This will +// be nil if the function executes successfully. +// +// Possible errors: +// - Failure to parse PKCS#12 data +// - No certificates found in the PKCS#12 data +// - No private key found in the PKCS#12 data +// - Private key is not of RSA type +// +// Usage example: +// +// certs, privKey, err := ParseCertificateData(ctx, pkcs12Data, []byte("password")) +// if err != nil { +// // Handle error +// } +// // Use certs and privKey as needed func ParseCertificateData(ctx context.Context, certData []byte, password []byte) ([]*x509.Certificate, crypto.PrivateKey, error) { tflog.Debug(ctx, "Attempting to parse PKCS#12 data") @@ -25,9 +63,16 @@ func ParseCertificateData(ctx context.Context, certData []byte, password []byte) } certs := append([]*x509.Certificate{certificate}, caCerts...) - if len(certs) == 0 { - tflog.Error(ctx, "No certificates found in PKCS#12 data") - return nil, nil, errors.New("no certificates found in PKCS#12 data") + validCerts := []*x509.Certificate{} + for _, cert := range certs { + if cert != nil { + validCerts = append(validCerts, cert) + } + } + + if len(validCerts) == 0 { + tflog.Error(ctx, "No valid certificates found in PKCS#12 data") + return nil, nil, errors.New("no valid certificates found in PKCS#12 data") } if privateKey == nil { @@ -37,15 +82,17 @@ func ParseCertificateData(ctx context.Context, certData []byte, password []byte) rsaKey, ok := privateKey.(*rsa.PrivateKey) if !ok { - tflog.Error(ctx, "Private key is not of RSA type") - return nil, nil, errors.New("private key is not of RSA type") + tflog.Error(ctx, "Private key is not of RSA type", map[string]interface{}{ + "actualType": fmt.Sprintf("%T", privateKey), + }) + return nil, nil, fmt.Errorf("private key is not of RSA type, got %T", privateKey) } tflog.Info(ctx, "PKCS#12 data parsed successfully", map[string]interface{}{ - "certificateCount": len(certs), + "certificateCount": len(validCerts), "privateKeyType": "RSA", "privateKeyBits": rsaKey.N.BitLen(), }) - return certs, privateKey, nil + return validCerts, privateKey, nil } diff --git a/internal/helpers/cert_test.go b/internal/helpers/cert_test.go new file mode 100644 index 00000000..7a621d75 --- /dev/null +++ b/internal/helpers/cert_test.go @@ -0,0 +1,195 @@ +package helpers + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + pkcs12 "software.sslmate.com/src/go-pkcs12" +) + +func TestParseCertificateData(t *testing.T) { + ctx := context.Background() + + t.Run("Valid PFX with password", func(t *testing.T) { + pfxData, password, err := generatePFXWithPassword() + require.NoError(t, err) + + certs, privKey, err := ParseCertificateData(ctx, pfxData, []byte(password)) + assert.NoError(t, err) + assert.Len(t, certs, 1) + assert.NotNil(t, privKey) + _, ok := privKey.(*rsa.PrivateKey) + assert.True(t, ok) + }) + + t.Run("Valid PFX without password", func(t *testing.T) { + pfxData, err := generatePFXWithoutPassword() + require.NoError(t, err) + + certs, privKey, err := ParseCertificateData(ctx, pfxData, []byte("")) + assert.NoError(t, err) + assert.Len(t, certs, 1) + assert.NotNil(t, privKey) + _, ok := privKey.(*rsa.PrivateKey) + assert.True(t, ok) + }) + + t.Run("PFX with non-RSA key", func(t *testing.T) { + pfxData, password, err := generatePFXWithNonRSAKey() + require.NoError(t, err) + + _, _, err = ParseCertificateData(ctx, pfxData, []byte(password)) + assert.Error(t, err) + assert.Contains(t, err.Error(), "private key is not of RSA type") + }) + + t.Run("Invalid PFX data", func(t *testing.T) { + _, _, err := ParseCertificateData(ctx, []byte("invalid data"), []byte("password")) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to parse PKCS#12 data") + }) + + t.Run("Incorrect password", func(t *testing.T) { + pfxData, _, err := generatePFXWithPassword() + require.NoError(t, err) + + _, _, err = ParseCertificateData(ctx, pfxData, []byte("wrongpassword")) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to parse PKCS#12 data") + }) +} + +// generatePFXWithPassword creates a PKCS#12 (PFX) certificate with an RSA private key and password +func generatePFXWithPassword() (pfxData []byte, password string, err error) { + // Generate RSA private key + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, "", err + } + + // Create certificate template + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: "Test Cert", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + // Create certificate + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) + if err != nil { + return nil, "", err + } + + cert, err := x509.ParseCertificate(certDER) + if err != nil { + return nil, "", err + } + + // Encode to PKCS#12 + password = "testpassword" + pfxData, err = pkcs12.Modern.Encode(privateKey, cert, nil, password) + if err != nil { + return nil, "", err + } + + return pfxData, password, nil +} + +// generatePFXWithoutPassword creates a PKCS#12 (PFX) certificate with an RSA private key and no password +func generatePFXWithoutPassword() (pfxData []byte, err error) { + // Generate RSA private key + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + + // Create certificate template + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: "Test Cert No Password", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + // Create certificate + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) + if err != nil { + return nil, err + } + + cert, err := x509.ParseCertificate(certDER) + if err != nil { + return nil, err + } + + // Encode to PKCS#12 without password + pfxData, err = pkcs12.Modern.Encode(privateKey, cert, nil, "") + if err != nil { + return nil, err + } + + return pfxData, nil +} + +// generatePFXWithNonRSAKey creates a PKCS#12 (PFX) certificate with an ECDSA private key (non-RSA) +func generatePFXWithNonRSAKey() (pfxData []byte, password string, err error) { + // Generate ECDSA private key + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, "", err + } + + // Create certificate template + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: "Test Cert ECDSA", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + // Create certificate + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) + if err != nil { + return nil, "", err + } + + cert, err := x509.ParseCertificate(certDER) + if err != nil { + return nil, "", err + } + + // Encode to PKCS#12 + password = "testpassword" + pfxData, err = pkcs12.Modern.Encode(privateKey, cert, nil, password) + if err != nil { + return nil, "", err + } + + return pfxData, password, nil +} diff --git a/internal/helpers/conversion_test.go b/internal/helpers/conversion_test.go new file mode 100644 index 00000000..1ea4b0b5 --- /dev/null +++ b/internal/helpers/conversion_test.go @@ -0,0 +1,33 @@ +package helpers + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStringPtrToString(t *testing.T) { + t.Run("Non-nil string pointer", func(t *testing.T) { + input := "test string" + result := StringPtrToString(&input) + assert.Equal(t, input, result, "Should return the dereferenced string value") + }) + + t.Run("Nil string pointer", func(t *testing.T) { + var input *string + result := StringPtrToString(input) + assert.Equal(t, "", result, "Should return an empty string for nil input") + }) + + t.Run("Empty string pointer", func(t *testing.T) { + input := "" + result := StringPtrToString(&input) + assert.Equal(t, "", result, "Should return an empty string for empty string input") + }) + + t.Run("String pointer with whitespace", func(t *testing.T) { + input := " " + result := StringPtrToString(&input) + assert.Equal(t, " ", result, "Should preserve whitespace") + }) +} diff --git a/internal/helpers/md5_test.go b/internal/helpers/md5_test.go new file mode 100644 index 00000000..a0311ab0 --- /dev/null +++ b/internal/helpers/md5_test.go @@ -0,0 +1,76 @@ +package helpers + +import ( + "crypto/md5" + "crypto/rand" + "encoding/hex" + "io" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCalculateMd5(t *testing.T) { + tempDir := t.TempDir() + + t.Run("Calculate MD5 for non-empty file", func(t *testing.T) { + content := []byte("Hello, World!") + filePath := filepath.Join(tempDir, "test1.txt") + err := os.WriteFile(filePath, content, 0644) + require.NoError(t, err) + + expectedMD5 := md5.Sum(content) + expectedMD5String := hex.EncodeToString(expectedMD5[:]) + + result, err := CalculateMd5(filePath) + assert.NoError(t, err) + assert.Equal(t, expectedMD5String, result) + }) + + t.Run("Calculate MD5 for empty file", func(t *testing.T) { + filePath := filepath.Join(tempDir, "empty.txt") + err := os.WriteFile(filePath, []byte{}, 0644) + require.NoError(t, err) + + expectedMD5 := md5.Sum([]byte{}) + expectedMD5String := hex.EncodeToString(expectedMD5[:]) + + result, err := CalculateMd5(filePath) + assert.NoError(t, err) + assert.Equal(t, expectedMD5String, result) + }) + + t.Run("Calculate MD5 for non-existent file", func(t *testing.T) { + filePath := filepath.Join(tempDir, "non_existent.txt") + + result, err := CalculateMd5(filePath) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to open file") + assert.Empty(t, result) + }) + + t.Run("Calculate MD5 for large file", func(t *testing.T) { + filePath := filepath.Join(tempDir, "large.bin") + f, err := os.Create(filePath) + require.NoError(t, err) + defer f.Close() + + // Write 10MB of random data + data := make([]byte, 1024*1024*10) + _, err = io.ReadFull(rand.Reader, data) + require.NoError(t, err) + + _, err = f.Write(data) + require.NoError(t, err) + + expectedMD5 := md5.Sum(data) + expectedMD5String := hex.EncodeToString(expectedMD5[:]) + + result, err := CalculateMd5(filePath) + assert.NoError(t, err) + assert.Equal(t, expectedMD5String, result) + }) +} diff --git a/internal/provider/cloud_test.go b/internal/provider/cloud_test.go new file mode 100644 index 00000000..8a5cadb2 --- /dev/null +++ b/internal/provider/cloud_test.go @@ -0,0 +1,102 @@ +package provider + +import ( + "testing" + + "github.com/deploymenttheory/terraform-provider-microsoft365/internal/constants" + "github.com/stretchr/testify/assert" +) + +func TestSetCloudConstants(t *testing.T) { + testCases := []struct { + name string + cloud string + expectedAuthority string + expectedScope string + expectedServiceRoot string + expectedBetaServiceRoot string + expectedError string + }{ + { + name: "Public Cloud", + cloud: "public", + expectedAuthority: constants.PUBLIC_OAUTH_AUTHORITY_URL, + expectedScope: constants.PUBLIC_GRAPH_API_SCOPE, + expectedServiceRoot: constants.PUBLIC_GRAPH_API_SERVICE_ROOT, + expectedBetaServiceRoot: constants.PUBLIC_GRAPH_BETA_API_SERVICE_ROOT, + }, + { + name: "DoD Cloud", + cloud: "dod", + expectedAuthority: constants.USDOD_OAUTH_AUTHORITY_URL, + expectedScope: constants.USDOD_GRAPH_API_SCOPE, + expectedServiceRoot: constants.USDOD_GRAPH_API_SERVICE_ROOT, + expectedBetaServiceRoot: constants.USDOD_GRAPH_BETA_API_SERVICE_ROOT, + }, + { + name: "GCC Cloud", + cloud: "gcc", + expectedAuthority: constants.USGOV_OAUTH_AUTHORITY_URL, + expectedScope: constants.USGOV_GRAPH_API_SCOPE, + expectedServiceRoot: constants.USGOV_GRAPH_API_SERVICE_ROOT, + expectedBetaServiceRoot: constants.USGOV_GRAPH_BETA_API_SERVICE_ROOT, + }, + { + name: "GCC High Cloud", + cloud: "gcchigh", + expectedAuthority: constants.USGOVHIGH_OAUTH_AUTHORITY_URL, + expectedScope: constants.USGOVHIGH_GRAPH_API_SCOPE, + expectedServiceRoot: constants.USGOVHIGH_GRAPH_API_SERVICE_ROOT, + expectedBetaServiceRoot: constants.USGOVHIGH_GRAPH_BETA_API_SERVICE_ROOT, + }, + { + name: "China Cloud", + cloud: "china", + expectedAuthority: constants.CHINA_OAUTH_AUTHORITY_URL, + expectedScope: constants.CHINA_GRAPH_API_SCOPE, + expectedServiceRoot: constants.CHINA_GRAPH_API_SERVICE_ROOT, + expectedBetaServiceRoot: constants.CHINA_GRAPH_BETA_API_SERVICE_ROOT, + }, + { + name: "EagleX Cloud", + cloud: "ex", + expectedAuthority: constants.EX_OAUTH_AUTHORITY_URL, + expectedScope: constants.EX_GRAPH_API_SCOPE, + expectedServiceRoot: constants.EX_GRAPH_API_SERVICE_ROOT, + expectedBetaServiceRoot: constants.EX_GRAPH_BETA_API_SERVICE_ROOT, + }, + { + name: "Secure Cloud (RX)", + cloud: "rx", + expectedAuthority: constants.RX_OAUTH_AUTHORITY_URL, + expectedScope: constants.RX_GRAPH_API_SCOPE, + expectedServiceRoot: constants.RX_GRAPH_API_SERVICE_ROOT, + expectedBetaServiceRoot: constants.RX_GRAPH_BETA_API_SERVICE_ROOT, + }, + { + name: "Unsupported Cloud", + cloud: "unsupported", + expectedError: "unsupported microsoft cloud type 'unsupported'", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + authority, scope, serviceRoot, betaServiceRoot, err := setCloudConstants(tc.cloud) + + if tc.expectedError != "" { + assert.EqualError(t, err, tc.expectedError) + assert.Empty(t, authority) + assert.Empty(t, scope) + assert.Empty(t, serviceRoot) + assert.Empty(t, betaServiceRoot) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expectedAuthority, authority) + assert.Equal(t, tc.expectedScope, scope) + assert.Equal(t, tc.expectedServiceRoot, serviceRoot) + assert.Equal(t, tc.expectedBetaServiceRoot, betaServiceRoot) + } + }) + } +} diff --git a/internal/provider/entra.go b/internal/provider/entra.go index 167d9759..5d5c5fb1 100644 --- a/internal/provider/entra.go +++ b/internal/provider/entra.go @@ -153,6 +153,34 @@ func obtainCredential(ctx context.Context, data M365ProviderModel, clientOptions return azidentity.NewUsernamePasswordCredential(data.TenantID.ValueString(), data.ClientID.ValueString(), username, password, &azidentity.UsernamePasswordCredentialOptions{ ClientOptions: clientOptions, }) + case "client_assertion": + tflog.Debug(ctx, "Obtaining Client Assertion Credential", map[string]interface{}{ + "tenant_id": data.TenantID.ValueString(), + "client_id": data.ClientID.ValueString(), + }) + + var assertion string + if data.ClientAssertion.ValueString() != "" { + assertion = data.ClientAssertion.ValueString() + } else if data.ClientAssertionFile.ValueString() != "" { + content, err := os.ReadFile(data.ClientAssertionFile.ValueString()) + if err != nil { + return nil, fmt.Errorf("failed to read client assertion file: %v", err) + } + assertion = string(content) + } else { + return nil, fmt.Errorf("either client_assertion or client_assertion_file must be provided for client assertion authentication") + } + return azidentity.NewClientAssertionCredential( + data.TenantID.ValueString(), + data.ClientID.ValueString(), + func(context.Context) (string, error) { + return assertion, nil + }, + &azidentity.ClientAssertionCredentialOptions{ + ClientOptions: clientOptions, + }) + default: return nil, fmt.Errorf("unsupported authentication method '%s'", data.AuthMethod.ValueString()) } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 118501ec..f3ab17e1 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -37,6 +37,8 @@ type M365ProviderModel struct { ClientCertificatePassword types.String `tfsdk:"client_certificate_password"` Username types.String `tfsdk:"username"` Password types.String `tfsdk:"password"` + ClientAssertion types.String `tfsdk:"client_assertion"` + ClientAssertionFile types.String `tfsdk:"client_assertion_file"` RedirectURL types.String `tfsdk:"redirect_url"` UseProxy types.Bool `tfsdk:"use_proxy"` ProxyURL types.String `tfsdk:"proxy_url"` @@ -58,9 +60,6 @@ func (p *M365Provider) Schema(ctx context.Context, req provider.SchemaRequest, r Description: "The cloud to use for authentication and Graph / Graph Beta API requests." + "Default is `public`. Valid values are `public`, `gcc`, `gcchigh`, `china`, `dod`, `ex`, `rx`." + "Can also be set using the `M365_CLOUD` environment variable.", - MarkdownDescription: "The cloud to use for authentication and Graph / Graph Beta API requests." + - "Default is `public`. Valid values are `public`, `gcc`, `gcchigh`, `china`, `dod`, `ex`, `rx`." + - "Can also be set using the `M365_CLOUD` environment variable.", Required: true, Validators: []validator.String{ stringvalidator.OneOf("public", "gcc", "gcchigh", "china", "dod", "ex", "rx"), @@ -70,9 +69,26 @@ func (p *M365Provider) Schema(ctx context.Context, req provider.SchemaRequest, r Required: true, Description: "The authentication method to use for the Entra ID application to authenticate the provider. " + "Options: 'device_code', 'client_secret', 'client_certificate', 'interactive_browser', " + - "'username_password'. Can also be set using the `M365_AUTH_METHOD` environment variable.", + "'username_password', 'client_assertion'. Each method requires different credentials to be provided. " + + "Can also be set using the `M365_AUTH_METHOD` environment variable.", + MarkdownDescription: "The authentication method to use for the Entra ID application to authenticate the provider. " + + "Options:\n" + + "- `device_code`: Uses a device code flow for authentication.\n" + + "- `client_secret`: Uses a client ID and secret for authentication.\n" + + "- `client_certificate`: Uses a client certificate (.pfx) for authentication.\n" + + "- `interactive_browser`: Opens a browser for interactive login.\n" + + "- `username_password`: Uses username and password for authentication (not recommended for production).\n" + + "- `client_assertion`: Uses a client assertion (OIDC token) for authentication, suitable for CI/CD and server-to-server scenarios.\n\n" + + "Each method requires different credentials to be provided. Can also be set using the `M365_AUTH_METHOD` environment variable.", Validators: []validator.String{ - stringvalidator.OneOf("device_code", "client_secret", "client_certificate", "interactive_browser", "username_password"), + stringvalidator.OneOf( + "device_code", + "client_secret", + "client_certificate", + "interactive_browser", + "username_password", + "client_assertion", + ), }, }, "tenant_id": schema.StringAttribute{ @@ -108,7 +124,7 @@ func (p *M365Provider) Schema(ctx context.Context, req provider.SchemaRequest, r "Can also be set using the `M365_CLIENT_SECRET` environment variable.", }, "client_certificate": schema.StringAttribute{ - MarkdownDescription: "The path to the Client Certificate file associated with the Service " + + Description: "The path to the Client Certificate file associated with the Service " + "Principal for use when authenticating as a Service Principal using a Client Certificate. " + "Supports PKCS#12 (.pfx or .p12) file format. The file should contain the certificate, " + "private key, and optionally a certificate chain. " + @@ -122,7 +138,7 @@ func (p *M365Provider) Schema(ctx context.Context, req provider.SchemaRequest, r Sensitive: true, }, "client_certificate_password": schema.StringAttribute{ - MarkdownDescription: "The password to decrypt the PKCS#12 (.pfx or .p12) file specified in " + + Description: "The password to decrypt the PKCS#12 (.pfx or .p12) file specified in " + "'client_certificate_file_path'. This is required if the PKCS#12 file is password-protected. " + "When the certificate file is created, this password is used to encrypt the private key for " + "security. It's not related to any Microsoft Entra ID (formerly Azure Active Directory) settings," + @@ -143,6 +159,27 @@ func (p *M365Provider) Schema(ctx context.Context, req provider.SchemaRequest, r Description: "The password for username/password authentication. Can also be set using the" + "`M365_PASSWORD` environment variable.", }, + "client_assertion": schema.StringAttribute{ + Optional: true, + Sensitive: true, + Description: "The client assertion string (OIDC token) for authentication. " + + "This is typically a JSON Web Token (JWT) that represents the identity of the client. " + + "It is used in the client credentials flow with client assertion. " + + "This method is more secure than client secret as the assertion is short-lived. " + + "Commonly used in CI/CD pipelines and server-to-server authentication scenarios. " + + "Can also be set using the `M365_CLIENT_ASSERTION` environment variable. " + + "If both this and `client_assertion_file` are specified, this takes precedence.", + }, + "client_assertion_file": schema.StringAttribute{ + Optional: true, + Description: "Path to a file containing the client assertion (OIDC token) for authentication. " + + "This file should contain a JSON Web Token (JWT) that represents the identity of the client. " + + "Useful when the assertion is too long to be specified directly or when it's generated externally. " + + "The provider will read this file to obtain the assertion string. " + + "Ensure the file permissions are set appropriately to protect the token. " + + "Can also be set using the `M365_CLIENT_ASSERTION_FILE` environment variable. " + + "If both this and `client_assertion` are specified, `client_assertion` takes precedence.", + }, "redirect_url": schema.StringAttribute{ Optional: true, Description: "The redirect URL for interactive browser authentication. Can also be set using " + @@ -184,15 +221,11 @@ func (p *M365Provider) Schema(ctx context.Context, req provider.SchemaRequest, r Optional: true, Description: "Flag to indicate whether to opt out of telemetry. Default is `false`. " + "Can also be set using the `M365_TELEMETRY_OPTOUT` environment variable.", - MarkdownDescription: "Flag to indicate whether to opt out of telemetry. Default is `false`. " + - "Can also be set using the `M365_TELEMETRY_OPTOUT` environment variable.", }, "debug_mode": schema.BoolAttribute{ Optional: true, Description: "Flag to enable debug mode for the provider." + "Can also be set using the `M365_DEBUG_MODE` environment variable.", - MarkdownDescription: "Flag to enable debug mode for the provider." + - "Can also be set using the `M365_DEBUG_MODE` environment variable.", }, }, } @@ -236,6 +269,8 @@ func (p *M365Provider) Configure(ctx context.Context, req provider.ConfigureRequ ClientCertificatePassword: types.StringValue(helpers.EnvDefaultFunc("M365_CLIENT_CERTIFICATE_PASSWORD", config.ClientCertificatePassword.ValueString())), Username: types.StringValue(helpers.EnvDefaultFunc("M365_USERNAME", config.Username.ValueString())), Password: types.StringValue(helpers.EnvDefaultFunc("M365_PASSWORD", config.Password.ValueString())), + ClientAssertion: types.StringValue(helpers.EnvDefaultFunc("M365_CLIENT_ASSERTION", config.ClientAssertion.ValueString())), + ClientAssertionFile: types.StringValue(helpers.EnvDefaultFunc("M365_CLIENT_ASSERTION_FILE", config.ClientAssertionFile.ValueString())), RedirectURL: types.StringValue(helpers.EnvDefaultFunc("M365_REDIRECT_URL", config.RedirectURL.ValueString())), UseProxy: types.BoolValue(helpers.EnvDefaultFuncBool("M365_USE_PROXY", config.UseProxy.ValueBool())), ProxyURL: types.StringValue(helpers.EnvDefaultFunc("M365_PROXY_URL", config.ProxyURL.ValueString())), @@ -265,6 +300,10 @@ func (p *M365Provider) Configure(ctx context.Context, req provider.ConfigureRequ ctx = tflog.SetField(ctx, "password", data.Password.ValueString()) ctx = tflog.MaskFieldValuesWithFieldKeys(ctx, "password") + ctx = tflog.SetField(ctx, "client_assertion", data.ClientAssertion.ValueString()) + ctx = tflog.SetField(ctx, "client_assertion_file", data.ClientAssertionFile.ValueString()) + ctx = tflog.MaskAllFieldValuesRegexes(ctx, regexp.MustCompile(`(?i)client_assertion`)) + ctx = tflog.SetField(ctx, "tenant_id", data.TenantID.ValueString()) ctx = tflog.SetField(ctx, "client_id", data.ClientID.ValueString()) ctx = tflog.SetField(ctx, "client_secret", data.ClientSecret.ValueString()) @@ -346,7 +385,11 @@ func (p *M365Provider) Configure(ctx context.Context, req provider.ConfigureRequ } stableAdapter, err := msgraphsdk.NewGraphRequestAdapterWithParseNodeFactoryAndSerializationWriterFactoryAndHttpClient( - authProvider, nil, nil, httpClient) + authProvider, + nil, + nil, + httpClient, + ) if err != nil { resp.Diagnostics.AddError( "Failed to create Microsoft Graph Stable SDK Adapter", @@ -356,7 +399,11 @@ func (p *M365Provider) Configure(ctx context.Context, req provider.ConfigureRequ } betaAdapter, err := msgraphbetasdk.NewGraphRequestAdapterWithParseNodeFactoryAndSerializationWriterFactoryAndHttpClient( - authProvider, nil, nil, httpClient) + authProvider, + nil, + nil, + httpClient, + ) if err != nil { resp.Diagnostics.AddError( "Failed to create Microsoft Graph Beta SDK Adapter",