Skip to content

Commit

Permalink
Merge pull request #139 from gravufo/implement-more-auth-methods
Browse files Browse the repository at this point in the history
Add support for ManagedIdentity and OIDC authentication methods
  • Loading branch information
turkenf authored Sep 6, 2024
2 parents 0f15ad9 + a3e8240 commit 19c4cbd
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 45 deletions.
35 changes: 33 additions & 2 deletions apis/v1beta1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,41 @@ import (
type ProviderConfigSpec struct {
// Credentials required to authenticate to this provider.
Credentials ProviderCredentials `json:"credentials"`

// ClientID is the user-assigned managed identity's ID
// when Credentials.Source is `InjectedIdentity`. If unset and
// Credentials.Source is `InjectedIdentity`, then a system-assigned
// managed identity is used.
// Required if Credentials.Source is OIDCTokenFile.
// +kubebuilder:validation:Optional
ClientID *string `json:"clientID,omitempty"`

// TenantID is the Azure AD tenant ID to be used.
// If unset, tenant ID from Credentials will be used.
// Required if Credentials.Source is InjectedIdentity, OIDCTokenFile.
// +kubebuilder:validation:Optional
TenantID *string `json:"tenantID,omitempty"`

// MSIEndpoint is the optional path to a custom endpoint for
// Managed Service Identity.
// +kubebuilder:validation:Optional
MSIEndpoint *string `json:"msiEndpoint,omitempty"`

// The Cloud Environment which should be used. Possible values are "public",
// "usgovernment", "german", and "china". Defaults to "public".
// +kubebuilder:validation:Optional
Environment *string `json:"environment,omitempty"`

// OIDCTokenFilePath is the optional path to a token file
// that allows to access a managed identity.
// +kubebuilder:validation:Optional
OidcTokenFilePath *string `json:"oidcTokenFilePath,omitempty"`
}

// ProviderCredentials required to authenticate.
type ProviderCredentials struct {
// Source of the provider credentials.
// +kubebuilder:validation:Enum=None;Secret;InjectedIdentity;Environment;Filesystem
// +kubebuilder:validation:Enum=None;Secret;InjectedIdentity;Environment;Filesystem;UserAssignedManagedIdentity;SystemAssignedManagedIdentity;OIDCTokenFile
Source xpv1.CredentialsSource `json:"source"`

xpv1.CommonCredentialSelectors `json:",inline"`
Expand All @@ -32,12 +61,13 @@ type ProviderConfigStatus struct {

// +kubebuilder:object:root=true

// A ProviderConfig configures a Azuread provider.
// A ProviderConfig configures the AzureAD provider.
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp"
// +kubebuilder:printcolumn:name="SECRET-NAME",type="string",JSONPath=".spec.credentials.secretRef.name",priority=1
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:resource:scope=Cluster,categories={crossplane,provider,azuread}
// +kubebuilder:storageversion
type ProviderConfig struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Expand All @@ -63,6 +93,7 @@ type ProviderConfigList struct {
// +kubebuilder:printcolumn:name="RESOURCE-KIND",type="string",JSONPath=".resourceRef.kind"
// +kubebuilder:printcolumn:name="RESOURCE-NAME",type="string",JSONPath=".resourceRef.name"
// +kubebuilder:resource:scope=Cluster,categories={crossplane,provider,azuread}
// +kubebuilder:storageversion
type ProviderConfigUsage struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Expand Down
25 changes: 25 additions & 0 deletions apis/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

172 changes: 130 additions & 42 deletions internal/clients/azuread.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ package clients
import (
"context"
"encoding/json"
"strings"

"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/upjet/pkg/terraform"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
tfsdk "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"

xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
xpresource "github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/upjet/pkg/terraform"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand All @@ -21,23 +24,47 @@ import (

const (
// error messages
errNoProviderConfig = "no providerConfigRef provided"
errGetProviderConfig = "cannot get referenced ProviderConfig"
errTrackUsage = "cannot track ProviderConfig usage"
errExtractCredentials = "cannot extract credentials"
errUnmarshalCredentials = "cannot unmarshal azuread credentials as JSON"
keyClientID = "clientId"
keyClientSecret = "clientSecret"
keyTenantID = "tenantId"
keyTerraformClientID = "client_id"
keyTerraformClientSecret = "client_secret"
keyTerraformTenantID = "tenant_id"
errNoProviderConfig = "no providerConfigRef provided"
errGetProviderConfig = "cannot get referenced ProviderConfig"
errTrackUsage = "cannot track ProviderConfig usage"
errExtractCredentials = "cannot extract credentials"
errUnmarshalCredentials = "cannot unmarshal Azure credentials as JSON"
errTenantIDNotSet = "tenant ID must be set in ProviderConfig when credential source is InjectedIdentity, OIDCTokenFile"
errClientIDNotSet = "client ID must be set in ProviderConfig when credential source is OIDCTokenFile"
// Azure service principal credentials file JSON keys
keyAzureClientID = "clientId"
keyAzureClientSecret = "clientSecret"
keyAzureClientCert = "clientCertificate"
keyAzureClientCertPass = "clientCertificatePassword"
keyAzureTenantID = "tenantId"
// Terraform Provider configuration block keys
keyTerraformFeatures = "features"
keySkipProviderRegistration = "skip_provider_registration"
keyUseMSI = "use_msi"
keyClientID = "client_id"
keyTenantID = "tenant_id"
keyMSIEndpoint = "msi_endpoint"
keyClientSecret = "client_secret"
keyClientCert = "client_certificate"
keyClientCertPassword = "client_certificate_password"
keyEnvironment = "environment"
keyOidcTokenFilePath = "oidc_token_file_path"
keyUseOIDC = "use_oidc"
// Default OidcTokenFilePath
defaultOidcTokenFilePath = "/var/run/secrets/azure/tokens/azure-identity-token"
)

var (
credentialsSourceUserAssignedManagedIdentity xpv1.CredentialsSource = "UserAssignedManagedIdentity"
credentialsSourceSystemAssignedManagedIdentity xpv1.CredentialsSource = "SystemAssignedManagedIdentity"
credentialsSourceOIDCTokenFile xpv1.CredentialsSource = "OIDCTokenFile"
)

// TerraformSetupBuilder builds Terraform a terraform.SetupFn function which
// returns Terraform provider setup configuration
func TerraformSetupBuilder(tfProvider *schema.Provider) terraform.SetupFn {
return func(ctx context.Context, client client.Client, mg resource.Managed) (terraform.Setup, error) {
// TerraformSetupBuilder returns Terraform setup with provider specific
// configuration like provider credentials used to connect to cloud APIs in the
// expected form of a Terraform provider.
func TerraformSetupBuilder(tfProvider *schema.Provider) terraform.SetupFn { //nolint:gocyclo
return func(ctx context.Context, client client.Client, mg xpresource.Managed) (terraform.Setup, error) {
ps := terraform.Setup{}

configRef := mg.GetProviderConfigReference()
Expand All @@ -49,41 +76,33 @@ func TerraformSetupBuilder(tfProvider *schema.Provider) terraform.SetupFn {
return ps, errors.Wrap(err, errGetProviderConfig)
}

t := resource.NewProviderConfigUsageTracker(client, &v1beta1.ProviderConfigUsage{})
t := xpresource.NewProviderConfigUsageTracker(client, &v1beta1.ProviderConfigUsage{})
if err := t.Track(ctx, mg); err != nil {
return ps, errors.Wrap(err, errTrackUsage)
}

data, err := resource.CommonCredentialExtractor(ctx, pc.Spec.Credentials.Source, client, pc.Spec.Credentials.CommonCredentialSelectors)
if err != nil {
return ps, errors.Wrap(err, errExtractCredentials)
}
creds := map[string]string{}
if err := json.Unmarshal(data, &creds); err != nil {
return ps, errors.Wrap(err, errUnmarshalCredentials)
ps.Configuration = map[string]interface{}{
keyTerraformFeatures: map[string]interface{}{},
}
// set provider configuration
ps.Configuration = map[string]any{}
if v, ok := creds[keyClientID]; ok {
ps.Configuration[keyTerraformClientID] = v
}
if v, ok := creds[keyClientSecret]; ok {
ps.Configuration[keyTerraformClientSecret] = v

var err error
switch pc.Spec.Credentials.Source { //nolint:exhaustive
case credentialsSourceSystemAssignedManagedIdentity, credentialsSourceUserAssignedManagedIdentity:
err = msiAuth(pc, &ps)
case credentialsSourceOIDCTokenFile:
err = oidcAuth(pc, &ps)
default:
err = spAuth(ctx, pc, &ps, client)
}
if v, ok := creds[keyTenantID]; ok {
ps.Configuration[keyTerraformTenantID] = v
if err != nil {
return terraform.Setup{}, errors.Wrap(err, "failed to prepare terraform.Setup")
}
return ps, errors.Wrap(configureTFProviderMeta(ctx, &ps, *tfProvider), "failed to configure the Terraform AzureAD provider meta")

return ps, errors.Wrap(configureNoForkAzureClient(ctx, &ps, *tfProvider), "failed to configure the no-fork Azure client")
}
}

func configureTFProviderMeta(ctx context.Context, ps *terraform.Setup, p schema.Provider) error {
// Please be aware that this implementation relies on the schema.Provider
// parameter `p` being a non-pointer. This is because normally
// the Terraform plugin SDK normally configures the provider
// only once and using a pointer argument here will cause
// race conditions between resources referring to different
// ProviderConfigs.
func configureNoForkAzureClient(ctx context.Context, ps *terraform.Setup, p schema.Provider) error {
diag := p.Configure(context.WithoutCancel(ctx), &tfsdk.ResourceConfig{
Config: ps.Configuration,
})
Expand All @@ -93,3 +112,72 @@ func configureTFProviderMeta(ctx context.Context, ps *terraform.Setup, p schema.
ps.Meta = p.Meta()
return nil
}

func spAuth(ctx context.Context, pc *v1beta1.ProviderConfig, ps *terraform.Setup, client client.Client) error {
data, err := xpresource.CommonCredentialExtractor(ctx, pc.Spec.Credentials.Source, client, pc.Spec.Credentials.CommonCredentialSelectors)
if err != nil {
return errors.Wrap(err, errExtractCredentials)
}
data = []byte(strings.TrimSpace(string(data)))
azureCreds := map[string]string{}
if err := json.Unmarshal(data, &azureCreds); err != nil {
return errors.Wrap(err, errUnmarshalCredentials)
}
// set credentials configuration
ps.Configuration[keyTenantID] = azureCreds[keyAzureTenantID]
ps.Configuration[keyClientID] = azureCreds[keyAzureClientID]
ps.Configuration[keyClientSecret] = azureCreds[keyAzureClientSecret]
if clientCert, ok := azureCreds[keyAzureClientCert]; ok {
ps.Configuration[keyClientCert] = clientCert
if clientCertPass, passwordOk := azureCreds[keyAzureClientCertPass]; passwordOk {
ps.Configuration[keyClientCertPassword] = clientCertPass
}
}
if pc.Spec.TenantID != nil {
ps.Configuration[keyTenantID] = *pc.Spec.TenantID
}
if pc.Spec.ClientID != nil {
ps.Configuration[keyClientID] = *pc.Spec.ClientID
}
if pc.Spec.Environment != nil {
ps.Configuration[keyEnvironment] = *pc.Spec.Environment
}
return nil
}

func msiAuth(pc *v1beta1.ProviderConfig, ps *terraform.Setup) error {
if pc.Spec.TenantID == nil || len(*pc.Spec.TenantID) == 0 {
return errors.New(errTenantIDNotSet)
}
ps.Configuration[keyTenantID] = *pc.Spec.TenantID
ps.Configuration[keyUseMSI] = "true"
if pc.Spec.MSIEndpoint != nil {
ps.Configuration[keyMSIEndpoint] = *pc.Spec.MSIEndpoint
}
if pc.Spec.ClientID != nil {
ps.Configuration[keyClientID] = *pc.Spec.ClientID
}
if pc.Spec.Environment != nil {
ps.Configuration[keyEnvironment] = *pc.Spec.Environment
}
return nil
}

func oidcAuth(pc *v1beta1.ProviderConfig, ps *terraform.Setup) error {
if pc.Spec.TenantID == nil || len(*pc.Spec.TenantID) == 0 {
return errors.New(errTenantIDNotSet)
}
if pc.Spec.ClientID == nil || len(*pc.Spec.ClientID) == 0 {
return errors.New(errClientIDNotSet)
}
// OIDC Token File Path defaults to a projected-volume path mounted in the pod running in the AKS cluster, when workload identity is enabled on the pod.
ps.Configuration[keyOidcTokenFilePath] = defaultOidcTokenFilePath
if pc.Spec.OidcTokenFilePath != nil {
ps.Configuration[keyOidcTokenFilePath] = *pc.Spec.OidcTokenFilePath
}
ps.Configuration[keyTenantID] = *pc.Spec.TenantID
ps.Configuration[keyClientID] = *pc.Spec.ClientID
ps.Configuration[keyUseOIDC] = "true"
return nil

}
34 changes: 33 additions & 1 deletion package/crds/azuread.upbound.io_providerconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ spec:
name: v1beta1
schema:
openAPIV3Schema:
description: A ProviderConfig configures a Azuread provider.
description: A ProviderConfig configures the AzureAD provider.
properties:
apiVersion:
description: |-
Expand All @@ -51,6 +51,14 @@ spec:
spec:
description: A ProviderConfigSpec defines the desired state of a ProviderConfig.
properties:
clientID:
description: |-
ClientID is the user-assigned managed identity's ID
when Credentials.Source is `InjectedIdentity`. If unset and
Credentials.Source is `InjectedIdentity`, then a system-assigned
managed identity is used.
Required if Credentials.Source is OIDCTokenFile.
type: string
credentials:
description: Credentials required to authenticate to this provider.
properties:
Expand Down Expand Up @@ -103,10 +111,34 @@ spec:
- InjectedIdentity
- Environment
- Filesystem
- UserAssignedManagedIdentity
- SystemAssignedManagedIdentity
- OIDCTokenFile
type: string
required:
- source
type: object
environment:
description: |-
The Cloud Environment which should be used. Possible values are "public",
"usgovernment", "german", and "china". Defaults to "public".
type: string
msiEndpoint:
description: |-
MSIEndpoint is the optional path to a custom endpoint for
Managed Service Identity.
type: string
oidcTokenFilePath:
description: |-
OIDCTokenFilePath is the optional path to a token file
that allows to access a managed identity.
type: string
tenantID:
description: |-
TenantID is the Azure AD tenant ID to be used.
If unset, tenant ID from Credentials will be used.
Required if Credentials.Source is InjectedIdentity, OIDCTokenFile.
type: string
required:
- credentials
type: object
Expand Down

0 comments on commit 19c4cbd

Please sign in to comment.