Skip to content

Commit

Permalink
Merge pull request #11 from deploymenttheory/feature-scaffolding
Browse files Browse the repository at this point in the history
added certificate validators and consts for various ms cloud types
  • Loading branch information
ShocOne authored Jul 22, 2024
2 parents f4783b9 + a039330 commit 654be6c
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 104 deletions.
40 changes: 40 additions & 0 deletions internal/constants/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package constants

const (
// Public Cloud
PUBLIC_OAUTH_AUTHORITY_URL = "https://login.microsoftonline.com/"
PUBLIC_GRAPH_API_SCOPE = "https://graph.microsoft.com/.default"
PUBLIC_GRAPH_API_DOMAIN = "graph.microsoft.com"

// US Department of Defense (DoD) Cloud
USDOD_OAUTH_AUTHORITY_URL = "https://login.microsoftonline.us/"
USDOD_GRAPH_API_SCOPE = "https://graph.microsoft.us/.default"
USDOD_GRAPH_API_DOMAIN = "graph.microsoft.us"

// US Government Cloud
USGOV_OAUTH_AUTHORITY_URL = "https://login.microsoftonline.com/"
USGOV_GRAPH_API_SCOPE = "https://graph.microsoft.us/.default"
USGOV_GRAPH_API_DOMAIN = "graph.microsoft.us"

// US Government High Cloud
USGOVHIGH_OAUTH_AUTHORITY_URL = "https://login.microsoftonline.us/"
USGOVHIGH_GRAPH_API_SCOPE = "https://graph.microsoft.us/.default"
USGOVHIGH_GRAPH_API_DOMAIN = "graph.microsoft.us"

// China Cloud - https://learn.microsoft.com/en-us/previous-versions/office/office-365-api/api/o365-china-endpoints
CHINA_OAUTH_AUTHORITY_URL = "https://login.chinacloudapi.cn/"
CHINA_GRAPH_API_SCOPE = "https://microsoftgraph.chinacloudapi.cn/.default"
CHINA_GRAPH_API_DOMAIN = "microsoftgraph.chinacloudapi.cn"

// EagleX Cloud
EX_OAUTH_AUTHORITY_URL = "https://login.microsoftonline.eaglex.ic.gov/"
EX_GRAPH_API_SCOPE = "https://graph.eaglex.ic.gov/.default"
EX_GRAPH_API_DOMAIN = "graph.eaglex.ic.gov"
EX_AUTHORITY_HOST = "https://login.microsoftonline.eaglex.ic.gov/"

// Secure Cloud (RX)
RX_OAUTH_AUTHORITY_URL = "https://login.microsoftonline.microsoft.scloud/"
RX_GRAPH_API_SCOPE = "https://graph.microsoft.scloud/.default"
RX_GRAPH_API_DOMAIN = "graph.microsoft.scloud"
RX_AUTHORITY_HOST = "https://login.microsoftonline.microsoft.scloud/"
)
20 changes: 10 additions & 10 deletions internal/helpers/cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,34 @@ import (
pkcs12 "software.sslmate.com/src/go-pkcs12"
)

// GetRawCertificateFromCertOrFilePath takes either a DER-encoded certificate
// or a file path to a DER-encoded PKCS#12 file, decodes it, and returns the raw certificate.
func GetRawCertificateFromCertOrFilePath(certOrFilePath string, password string) (*x509.Certificate, error) {
// GetCertificatesAndKeyFromCertOrFilePath takes either a base64-encoded certificate or a file path to a PKCS#12 file,
// decodes it, and returns the certificates and private key.
func GetCertificatesAndKeyFromCertOrFilePath(certOrFilePath string, password string) ([]*x509.Certificate, interface{}, error) {
certData, err := base64.StdEncoding.DecodeString(certOrFilePath)
if err == nil {
cert, err := x509.ParseCertificate(certData)
key, cert, err := pkcs12.Decode(certData, password)
if err == nil {
return cert, nil
return []*x509.Certificate{cert}, key, nil
}
}

file, err := os.Open(certOrFilePath)
if err != nil {
return nil, errors.New("could not open file or decode base64 input")
return nil, nil, errors.New("could not open file or decode base64 input")
}
defer file.Close()

pfxData, err := io.ReadAll(file)
if err != nil {
return nil, errors.New("could not read file content")
return nil, nil, errors.New("could not read file content")
}

_, cert, err := pkcs12.Decode(pfxData, password)
key, cert, err := pkcs12.Decode(pfxData, password)
if err != nil {
return nil, err
return nil, nil, err
}

return cert, nil
return []*x509.Certificate{cert}, key, nil
}

// ConvertBase64ToCert takes a base64 encoded PKCS#12 file, decodes it, and returns the certificate.
Expand Down
102 changes: 102 additions & 0 deletions internal/provider/authentication.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package provider

import (
"context"
"crypto/x509"
"fmt"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
azidentity "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/deploymenttheory/terraform-provider-microsoft365/internal/helpers"
"github.com/hashicorp/terraform-plugin-log/tflog"
)

// createCredential creates an Azure credential based on the provider configuration.
func createCredential(ctx context.Context, data M365ProviderModel, clientOptions policy.ClientOptions) (azcore.TokenCredential, error) {
switch data.AuthMethod.ValueString() {
case "device_code":
tflog.Debug(ctx, "Creating DeviceCodeCredential", map[string]interface{}{
"tenant_id": data.TenantID.ValueString(),
"client_id": data.ClientID.ValueString(),
})
return azidentity.NewDeviceCodeCredential(&azidentity.DeviceCodeCredentialOptions{
TenantID: data.TenantID.ValueString(),
ClientID: data.ClientID.ValueString(),
UserPrompt: func(ctx context.Context, message azidentity.DeviceCodeMessage) error {
tflog.Info(ctx, message.Message)
return nil
},
ClientOptions: clientOptions,
})
case "client_secret":
tflog.Debug(ctx, "Creating ClientSecretCredential", map[string]interface{}{
"tenant_id": data.TenantID.ValueString(),
"client_id": data.ClientID.ValueString(),
})
return azidentity.NewClientSecretCredential(data.TenantID.ValueString(), data.ClientID.ValueString(), data.ClientSecret.ValueString(), &azidentity.ClientSecretCredentialOptions{
ClientOptions: clientOptions,
})
case "client_certificate":
tflog.Debug(ctx, "Creating ClientCertificateCredential", map[string]interface{}{
"tenant_id": data.TenantID.ValueString(),
"client_id": data.ClientID.ValueString(),
})

var certs []*x509.Certificate
var key interface{}
var err error

if !data.ClientCertificate.IsNull() {
tflog.Debug(ctx, "Using base64 encoded client certificate")
certs, key, err = helpers.GetCertificatesAndKeyFromCertOrFilePath(data.ClientCertificate.ValueString(), data.ClientCertificatePassword.ValueString())
} else if !data.ClientCertificateFilePath.IsNull() {
tflog.Debug(ctx, "Using client certificate file path")
certs, key, err = helpers.GetCertificatesAndKeyFromCertOrFilePath(data.ClientCertificateFilePath.ValueString(), data.ClientCertificatePassword.ValueString())
} else {
return nil, fmt.Errorf("either 'client_certificate' or 'client_certificate_file_path' must be provided for client_certificate authentication")
}

if err != nil {
return nil, fmt.Errorf("failed to get certificates and key: %s", err.Error())
}

return azidentity.NewClientCertificateCredential(data.TenantID.ValueString(), data.ClientID.ValueString(), certs, key, &azidentity.ClientCertificateCredentialOptions{
ClientOptions: clientOptions,
})
case "on_behalf_of":
tflog.Debug(ctx, "Creating OnBehalfOfCredentialWithSecret", map[string]interface{}{
"tenant_id": data.TenantID.ValueString(),
"client_id": data.ClientID.ValueString(),
})
userAssertion := data.UserAssertion.ValueString()
return azidentity.NewOnBehalfOfCredentialWithSecret(data.TenantID.ValueString(), data.ClientID.ValueString(), userAssertion, data.ClientSecret.ValueString(), &azidentity.OnBehalfOfCredentialOptions{
ClientOptions: clientOptions,
})
case "interactive_browser":
tflog.Debug(ctx, "Creating InteractiveBrowserCredential", map[string]interface{}{
"tenant_id": data.TenantID.ValueString(),
"client_id": data.ClientID.ValueString(),
"redirect_url": data.RedirectURL.ValueString(),
})
redirectURL := data.RedirectURL.ValueString()
return azidentity.NewInteractiveBrowserCredential(&azidentity.InteractiveBrowserCredentialOptions{
TenantID: data.TenantID.ValueString(),
ClientID: data.ClientID.ValueString(),
RedirectURL: redirectURL,
ClientOptions: clientOptions,
})
case "username_password":
tflog.Debug(ctx, "Creating UsernamePasswordCredential", map[string]interface{}{
"tenant_id": data.TenantID.ValueString(),
"client_id": data.ClientID.ValueString(),
})
username := data.Username.ValueString()
password := data.Password.ValueString()
return azidentity.NewUsernamePasswordCredential(data.TenantID.ValueString(), data.ClientID.ValueString(), username, password, &azidentity.UsernamePasswordCredentialOptions{
ClientOptions: clientOptions,
})
default:
return nil, fmt.Errorf("unsupported authentication method '%s'", data.AuthMethod.ValueString())
}
}
97 changes: 6 additions & 91 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
azidentity "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/deploymenttheory/terraform-provider-microsoft365/internal/helpers"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
Expand Down Expand Up @@ -118,6 +117,7 @@ func (p *M365Provider) Schema(ctx context.Context, req provider.SchemaRequest, r
"client_certificate_file_path": schema.StringAttribute{
MarkdownDescription: "The path to the Client Certificate associated with the Service Principal for use when authenticating as a Service Principal using a Client Certificate.",
Optional: true,
Sensitive: true,
},
"client_certificate_password": schema.StringAttribute{
MarkdownDescription: "The password associated with the Client Certificate. For use when authenticating as a Service Principal using a Client Certificate.",
Expand Down Expand Up @@ -181,6 +181,9 @@ 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`",
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`",
Optional: true,
Validators: []validator.String{
validateCloud(),
},
},
"national_cloud_deployment": schema.BoolAttribute{
Optional: true,
Expand Down Expand Up @@ -365,101 +368,13 @@ func (p *M365Provider) Configure(ctx context.Context, req provider.ConfigureRequ
clientOptions.Cloud.ActiveDirectoryAuthorityHost = nationalCloudDeploymentTokenEndpoint
}

switch authMethod {
case "device_code":
cred, err = azidentity.NewDeviceCodeCredential(&azidentity.DeviceCodeCredentialOptions{
TenantID: tenantID,
ClientID: clientID,
UserPrompt: func(ctx context.Context, message azidentity.DeviceCodeMessage) error {
fmt.Println(message.Message)
return nil
},
ClientOptions: clientOptions,
})
case "client_secret":
cred, err = azidentity.NewClientSecretCredential(tenantID, clientID, clientSecret, &azidentity.ClientSecretCredentialOptions{
ClientOptions: clientOptions,
})
case "client_certificate":
certificatePath := data.ClientCertificateFilePath.ValueString()
certFile, err := os.Open(certificatePath)
if err != nil {
resp.Diagnostics.AddError(
"Error Opening Certificate File",
fmt.Sprintf("Failed to open the certificate file at path '%s': %s. "+
"Ensure the file path is correct and the file is accessible.", certificatePath, err.Error()),
)
return
}
defer certFile.Close()

info, err := certFile.Stat()
if err != nil {
resp.Diagnostics.AddError(
"Error Accessing Certificate File",
fmt.Sprintf("Failed to retrieve file information: %s. "+
"Ensure the file exists and is accessible.", err.Error()),
)
return
}

certBytes := make([]byte, info.Size())
_, err = certFile.Read(certBytes)
if err != nil {
resp.Diagnostics.AddError(
"Error Reading Certificate File",
fmt.Sprintf("Failed to read the certificate file: %s. "+
"Ensure the file is accessible and not corrupted.", err.Error()),
)
return
}

certs, key, err := azidentity.ParseCertificates(certBytes, nil)
if err != nil {
resp.Diagnostics.AddError(
"Error Parsing Certificates",
fmt.Sprintf("Failed to parse certificates from the provided file: %s. "+
"Ensure the file contains valid certificate data and is correctly formatted.", err.Error()),
)
return
}

cred, err = azidentity.NewClientCertificateCredential(tenantID, clientID, certs, key, &azidentity.ClientCertificateCredentialOptions{
ClientOptions: clientOptions,
})
case "on_behalf_of":
userAssertion := data.UserAssertion.ValueString()
cred, err = azidentity.NewOnBehalfOfCredentialWithSecret(tenantID, clientID, userAssertion, clientSecret, &azidentity.OnBehalfOfCredentialOptions{
ClientOptions: clientOptions,
})
case "interactive_browser":
redirectURL := data.RedirectURL.ValueString()
cred, err = azidentity.NewInteractiveBrowserCredential(&azidentity.InteractiveBrowserCredentialOptions{
TenantID: tenantID,
ClientID: clientID,
RedirectURL: redirectURL,
ClientOptions: clientOptions,
})
case "username_password":
username := data.Username.ValueString()
password := data.Password.ValueString()
cred, err = azidentity.NewUsernamePasswordCredential(tenantID, clientID, username, password, &azidentity.UsernamePasswordCredentialOptions{
ClientOptions: clientOptions,
})
default:
resp.Diagnostics.AddError(
"Unsupported authentication method",
fmt.Sprintf("The authentication method '%s' is not supported.", authMethod),
)
return
}

cred, err = createCredential(ctx, data, clientOptions)
if err != nil {
resp.Diagnostics.AddError(
"Unable to create credentials",
fmt.Sprintf("An error occurred while attempting to create the credentials using the provided authentication method '%s'. "+
"This may be due to incorrect or missing credentials, misconfigured client options, or issues with the underlying authentication library. "+
"Please verify the authentication method and credentials configuration. Detailed error: %s", authMethod, err.Error()),
"Please verify the authentication method and credentials configuration. Detailed error: %s", data.AuthMethod.ValueString(), err.Error()),
)
return
}
Expand Down
Loading

0 comments on commit 654be6c

Please sign in to comment.