Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Auth Options: User-Managed Identity, System-Managed Identity, and AzDO Workload Identity Federation #545

Merged
merged 54 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
9a8cbee
WIP
ianjensenisme Dec 5, 2024
028d1b1
WIP
ianjensenisme Dec 5, 2024
b018309
Break fix
ianjensenisme Dec 6, 2024
b497c05
Exposing the MSI settings
ianjensenisme Dec 6, 2024
b8081e4
Whoops
ianjensenisme Dec 6, 2024
e7bdad2
Fixes, documentation updates
Dec 6, 2024
aa37111
Break fix
Dec 6, 2024
a428da9
Break fix
ianjensenisme Dec 10, 2024
8d12f45
WIP
Dec 11, 2024
5245936
Merge branch 'ianjensenisme/243-Managed-Identity' of https://github.c…
Dec 11, 2024
c5aa21b
Fix merge mistake
Dec 11, 2024
5e40216
Add env var
Dec 16, 2024
56f1ef0
Documentation update, config update
Dec 18, 2024
100fe79
Merge branch 'main' into ianjensenisme/243-Managed-Identity
ianjensenisme Dec 18, 2024
d7991c4
Fixing linter error
Dec 18, 2024
e506eff
Create documentation-20241218.yaml
ianjensenisme Dec 18, 2024
d9d556b
Refactor conditional block to appease the arbitrary linter rule
Dec 18, 2024
91def6f
Newline fix
Dec 18, 2024
d26190f
Remove direct edit to changelog
ianjensenisme Dec 19, 2024
1804ae1
Remove changelog newline
ianjensenisme Dec 19, 2024
74bca64
Documentation update
Dec 19, 2024
3333034
Minor clarification
Dec 23, 2024
0fb380b
Merge branch 'main' into ianjensenisme/243-Managed-Identity
mawasile Jan 6, 2025
2680043
Add basic auth failure test, documentation fix
Jan 6, 2025
5e0a4c4
Documentation fix
Jan 6, 2025
fd73564
Minor edits
Jan 7, 2025
4c20bd3
Merge branch 'main' into ianjensenisme/243-Managed-Identity
mawasile Jan 14, 2025
4f71d03
Update templates/index.md.tmpl
ianjensenisme Jan 27, 2025
da54ef7
Update templates/index.md.tmpl
ianjensenisme Jan 27, 2025
8260158
Update templates/index.md.tmpl
ianjensenisme Jan 27, 2025
4a51d05
Update templates/index.md.tmpl
ianjensenisme Jan 27, 2025
3fbf2b0
Update templates/index.md.tmpl
ianjensenisme Jan 27, 2025
3ef3e3a
Update docs/index.md
ianjensenisme Jan 27, 2025
75cc7c3
Update docs/index.md
ianjensenisme Jan 27, 2025
f702a43
Documentation updates per review notes
Jan 28, 2025
7a00a8f
Merge branch 'ianjensenisme/243-Managed-Identity' of https://github.c…
Jan 28, 2025
9bf766d
Merge branch 'main' into ianjensenisme/243-Managed-Identity
ianjensenisme Jan 28, 2025
02edcf3
Documentation updates per review notes
ianjensenisme Jan 28, 2025
4b233a7
Update docs/index.md
ianjensenisme Jan 28, 2025
1e6a35a
Update docs/index.md
ianjensenisme Jan 28, 2025
e652f54
Update docs/index.md
ianjensenisme Jan 28, 2025
7ceda6a
More documentation fixes
Jan 28, 2025
cadf6ef
Fixes per review comment
Jan 28, 2025
dc87fb4
Adding errors per review feedback
Jan 28, 2025
c5262f3
Adding new auth errors and related tests
Jan 29, 2025
5fc5e44
Changing one of the expected errors to match the switch behavior
Jan 29, 2025
d0f9f83
Updating error testing to match actual (expected) behavior
Feb 3, 2025
0ca7923
Merge branch 'main' into ianjensenisme/243-Managed-Identity
ianjensenisme Feb 3, 2025
b47ee8d
Update internal/api/client_test.go
ianjensenisme Feb 4, 2025
bca3fa4
Update internal/api/client_test.go
ianjensenisme Feb 4, 2025
61f687c
Update internal/api/client_test.go
ianjensenisme Feb 4, 2025
71e5a92
Update internal/api/client_test.go
ianjensenisme Feb 4, 2025
ab50c85
Update internal/api/client_test.go
ianjensenisme Feb 5, 2025
999cbdd
Update internal/api/client_test.go
ianjensenisme Feb 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/unreleased/added-20241218.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: added
body: Added Managed identity and Workload Identity Federation authentication
time: 2024-12-18T08:58:50.826689481Z
custom:
Issue: "243"
73 changes: 61 additions & 12 deletions docs/index.md
ianjensenisme marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ Terraform supports a number of different methods for authenticating to Power Pla
* [Authenticating to Power Platform using the Azure CLI](#authenticating-to-power-platform-using-the-azure-cli)
* [Authenticating to Power Platform using a Service Principal with OIDC](#authenticating-to-power-platform-using-a-service-principal-with-oidc)
* [Authenticating to Power Platform using a Service Principal and a Client Secret](#authenticating-to-power-platform-using-a-service-principal-and-a-client-secret)
* [Authenticating to Power Platform using a Managed Identity](#authenticating-to-power-platform-using-a-managed-identity)
* [Authenticating to Power Platform using Azure DevOps Workload Identity Federation](#authenticating-to-power-platform-using-workload-identity-federation)

We recommend using either a Service Principal when running Terraform non-interactively (such as when running Terraform in a CI server) - and authenticating using the Azure CLI when running Terraform locally.

Expand Down Expand Up @@ -70,18 +72,18 @@ The Power Platform provider can use the [Azure CLI](https://learn.microsoft.com/

### Authenticating to Power Platform using a Service Principal with OIDC

The Power Platform provider can use a Service Principal with OpenID Connect (OIDC) to authenticate to Power Platform services. By using [Microsoft Entra's workload identity federation](https://learn.microsoft.com/entra/workload-id/workload-identity-federation) your CI/CD pipelines in GitHub or Azure DevOps can access Power Platform resources without needing to manage secrets.
The Power Platform provider can use a Service Principal with OpenID Connect (OIDC) to authenticate to Power Platform services. By using [Microsoft Entra's workload identity federation](https://learn.microsoft.com/entra/workload-id/workload-identity-federation), your CI/CD pipelines in GitHub or Azure DevOps can access Power Platform resources without needing to manage secrets.

1. [Create an app registration for the Power Platform Terraform Provider](guides/app_registration.md)
1. [Register your app registration with Power Platform](https://learn.microsoft.com/power-platform/admin/powerplatform-api-create-service-principal#registering-an-admin-management-application)
1. [Create a trust relationship between your CI/CD pipeline and the app registration](https://learn.microsoft.com/entra/workload-id/workload-identity-federation-create-trust?pivots=identity-wif-apps-methods-azp)
1. Configure the provider to use OIDC with the following code:

```terraform
provider "powerplatform" {
use_oidc = true
}
```
```terraform
provider "powerplatform" {
use_oidc = true
}
```

Additional Resources about OIDC:

Expand Down Expand Up @@ -139,9 +141,56 @@ The Power Platform provider can use a Service Principal with Client Secret to au
}
```

#### Using Environment Variables
### Authenticating to Power Platform Using a Managed Identity

The Power Platform provider can use a [Managed Identity](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) (previously called Managed Service Identity, or MSI) to authenticate to Power Platform services for keyless authentication in scenarios where the provider is being executed in select Azure services, such as Microsoft-hosted or self-hosted Azure DevOps pipelines.

#### System-Managed Identity

1. [Enable system-managed identity on an Azure resource](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview)
1. Register the managed identity with the Power Platform using the Application ID from the enterprise application for the system-managed identity resource. This task can be performed using either [the Power Platform Terraform Provider itself](https://registry.terraform.io/providers/microsoft/power-platform/latest/docs/resources/admin_management_application), or [PowerShell]([Register the managed identity with the Power Platform](https://learn.microsoft.com/en-us/power-platform/admin/powershell-create-service-principal).
1. Configure the provider to use the system-managed identity. Note that no Client ID is required as the Client ID is derived from the Azure resource running the provider.

```terraform
provider "powerplatform" {
use_msi = true
}
```

#### User-Managed Identity

We recomend using Environment Variables to pass the credentials to the provider.
1. [Create a User-Managed Identity resource](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview)
1. Register the managed identity with the Power Platform using the Application ID from the enterprise application for the system-managed identity resource. This task can be performed using either [the Power Platform Terraform Provider itself](https://registry.terraform.io/providers/microsoft/power-platform/latest/docs/resources/admin_management_application), or [PowerShell]([Register the managed identity with the Power Platform](https://learn.microsoft.com/en-us/power-platform/admin/powershell-create-service-principal).
1. Configure the provider to use the System-Managed Identity. Note that this example sets the Client ID in the provider configuration, but it could also be set using the POWER_PLATFORM_CLIENT_ID environment variable.

```terraform
provider "powerplatform" {
use_msi = true
client_id = var.client_id # This should be the Client ID from the user-managed identity resource.
}
```

### Authenticating to Power Platform Using Azure DevOps Workload Identity Federation

The Power Platform provider can use [Azure DevOps Workload Identity Federation](https://devblogs.microsoft.com/devops/introduction-to-azure-devops-workload-identity-federation-oidc-with-terraform/) with Azure DeOps pipelines to authenticate to Power Platform services.

*Note: For similar hands-off authentication in GitHub and Azure DevOps, the Power Platform Provider also supports the [OIDC authentication method](#authenticating-to-power-platform-using-a-service-principal-with-oidc).*

1. Create an [App Registration](guides/app_registration.md) or a [User-Managed Identity](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview). This resource will be used to manage the identity federation with Azure DevOps.
1. Register the App Registration or Managed Identity with the Power Platform. This task can be performed using [the provider itself](/resources/admin_management_application.md) or [PowerShell](https://learn.microsoft.com/en-us/power-platform/admin/powershell-create-service-principal).
1. [Complete the service connection configuration in Azure and Azure DevOps](https://learn.microsoft.com/en-us/azure/devops/pipelines/release/configure-workload-identity?view=azure-devops&tabs=managed-identity). Note that Azure DevOps may automatically generate the federated credential in Azure, depending on your permissions and Azure Subscription configuration.
1. Configure the provider to use Azure DevOps Workload Identity Federation. This authentication option also requires values to be set in the ARM_OIDC_REQUEST_TOKEN and POWER_PLATFORM_AZDO_SERVICE_CONNECTION_ID environment variables, which should be configured in the AzDO pipeline itself. Note that this example sets some of the required properties in the provider configuration, but the whole configuration could also be performed using just environment variables.

```terraform
provider "powerplatform" {
tenant_id = var.tenant_id
client_id = var.client_id # The client ID for the Azure resource containing the federated credentials for Azure DevOps. Should be an App Registration or a Managed Identity.
}
```

### Using Environment Variables

We recommend using Environment Variables to pass the credentials to the provider.

| Name | Description | Default Value |
|------|-------------|---------------|
Expand All @@ -151,9 +200,10 @@ We recomend using Environment Variables to pass the credentials to the provider.
| `POWER_PLATFORM_CLOUD` | override for the cloud used (default is `public`) | |
| `POWER_PLATFORM_USE_OIDC` | if set to `true` then OIDC authentication will be used | |
| `POWER_PLATFORM_USE_CLI` | if set to `true` then Azure CLI authentication will be used | |
| `POWER_PLATFORM_CLIENT_CERTIFICATE` | The Base64 format of your certificate that will be used to certificate based authentication | |
| `POWER_PLATFORM_CLIENT_CERTIFICATE_FILE_PATH` | The path to the certificate that will be used to certificate based authentication | |
| `POWER_PLATFORM_CLIENT_CERTIFICATE_PASSWORD` | Password for the provider certificate | |
| `POWER_PLATFORM_USE_MSI` | if set to `true` then Managed Identity authentication will be used | |
| `POWER_PLATFORM_CLIENT_CERTIFICATE` | The Base64 format of your certificate that will be used for certificate-based authentication | |
| `POWER_PLATFORM_CLIENT_CERTIFICATE_FILE_PATH` | The path to the certificate that will be used for certificate-based authentication | |
| `POWER_PLATFORM_AZDO_SERVICE_CONNECTION_ID` | The GUID of the Azure DevOps service connection to be used for Azure DevOps Workload Identity Federation | |

-> Variables passed into the provider will override the environment variables.

Expand Down Expand Up @@ -192,7 +242,6 @@ More detailed examples can be found in the [Power Platform Terraform Quickstarts

A full list of released versions of the Power Platform Terraform Provider can be found [here](https://github.com/microsoft/terraform-provider-power-platform/releases). Starting from v3.0.0, a summary of the changes to the provider in each release are documented the [CHANGELOG.md file in the GitHub repository](https://github.com/microsoft/terraform-provider-power-platform/blob/main/CHANGELOG.md). This provider follows Semantic Versioning for releases. The provider version is incremented based on the type of changes included in the release.


## Contributing

Contributions to this provider are always welcome! Please see the [Contribution Guidelines](https://github.com/microsoft/terraform-provider-power-platform/)
63 changes: 63 additions & 0 deletions internal/api/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,63 @@ func (client *Auth) AuthenticateOIDC(ctx context.Context, scopes []string) (stri
return accessToken.Token, accessToken.ExpiresOn, nil
}

func (client *Auth) AuthenticateUserManagedIdentity(ctx context.Context, scopes []string) (string, time.Time, error) {
userManagedIdentityCredential, err := azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{
ID: azidentity.ClientID(client.config.ClientId),
ClientOptions: azcore.ClientOptions{
Cloud: client.config.Cloud,
},
})
if err != nil {
return "", time.Time{}, err
}

accessToken, err := userManagedIdentityCredential.GetToken(context.Background(), policy.TokenRequestOptions{Scopes: scopes})
if err != nil {
return "", time.Time{}, err
}

return accessToken.Token, accessToken.ExpiresOn, nil
}

func (client *Auth) AuthenticateSystemManagedIdentity(ctx context.Context, scopes []string) (string, time.Time, error) {
systemManagedIdentityCredential, err := azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{
ClientOptions: azcore.ClientOptions{
Cloud: client.config.Cloud,
},
})
if err != nil {
return "", time.Time{}, err
}

accessToken, err := systemManagedIdentityCredential.GetToken(context.Background(), policy.TokenRequestOptions{Scopes: scopes})
if err != nil {
return "", time.Time{}, err
}

return accessToken.Token, accessToken.ExpiresOn, nil
}

func (client *Auth) AuthenticateAzDOWorkloadIdentityFederation(ctx context.Context, scopes []string) (string, time.Time, error) {
azdoWorkloadIdentityCredential, err := azidentity.NewAzurePipelinesCredential(
client.config.TenantId,
client.config.ClientId,
client.config.AzDOServiceConnectionID,
client.config.OidcRequestToken,
&azidentity.AzurePipelinesCredentialOptions{}, // Auxiliary tenants could be defined here
ianjensenisme marked this conversation as resolved.
Show resolved Hide resolved
)
if err != nil {
return "", time.Time{}, err
}

accessToken, err := azdoWorkloadIdentityCredential.GetToken(context.Background(), policy.TokenRequestOptions{Scopes: scopes})
if err != nil {
return "", time.Time{}, err
}

return accessToken.Token, accessToken.ExpiresOn, nil
}

func (w *OidcCredential) getAssertion(ctx context.Context) (string, error) {
if w.token != "" {
return w.token, nil
Expand Down Expand Up @@ -281,10 +338,16 @@ func (client *Auth) GetTokenForScopes(ctx context.Context, scopes []string) (*st
token, tokenExpiry, err = client.AuthenticateClientSecret(ctx, scopes)
case client.config.IsCliProvided():
token, tokenExpiry, err = client.AuthenticateUsingCli(ctx, scopes)
case client.config.IsAzDOWorkloadIdentityFederationProvided():
ianjensenisme marked this conversation as resolved.
Show resolved Hide resolved
token, tokenExpiry, err = client.AuthenticateAzDOWorkloadIdentityFederation(ctx, scopes)
case client.config.IsOidcProvided():
token, tokenExpiry, err = client.AuthenticateOIDC(ctx, scopes)
case client.config.IsClientCertificateCredentialsProvided():
token, tokenExpiry, err = client.AuthenticateClientCertificate(ctx, scopes)
case client.config.IsUserManagedIdentityProvided():
token, tokenExpiry, err = client.AuthenticateUserManagedIdentity(ctx, scopes)
case client.config.IsSystemManagedIdentityProvided():
token, tokenExpiry, err = client.AuthenticateSystemManagedIdentity(ctx, scopes)
default:
return nil, errors.New("no credentials provided")
}
Expand Down
43 changes: 42 additions & 1 deletion internal/api/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"net/http"
"reflect"
"strings"
"testing"
"time"

Expand All @@ -23,6 +24,7 @@ func TestUnitApiClient_GetConfig(t *testing.T) {
cfg := config.ProviderConfig{
UseCli: false,
UseOidc: false,
UseMsi: false,
TenantId: uuid.NewString(),
ClientId: uuid.NewString(),
ClientSecret: uuid.NewString(),
Expand All @@ -32,7 +34,7 @@ func TestUnitApiClient_GetConfig(t *testing.T) {
x := api.NewApiClientBase(&cfg, api.NewAuthBase(&cfg))
_, err := x.Execute(ctx, []string{"test"}, "GET", "/relativeurl", http.Header{}, nil, []int{http.StatusOK}, nil)
if err == nil {
t.Error("Expected an error for relatvieurl but got nil error")
t.Error("Expected an error for relativeurl but got nil error")
}

switch err.(type) {
Expand Down Expand Up @@ -76,3 +78,42 @@ func TestUnitSleepWithContext_HappyPath(t *testing.T) {

cancel()
}

func TestUnitApiClient_SystemManagedIdentity(t *testing.T) {
expectedError := "ManagedIdentityCredential: failed to authenticate a system assigned identity."

ctx := context.Background()
cfg := config.ProviderConfig{
UseMsi: true,
}

x := api.NewApiClientBase(&cfg, api.NewAuthBase(&cfg))
_, err := x.Execute(ctx, []string{"test"}, "GET", "https://api.bap.microsoft.com", http.Header{}, nil, []int{http.StatusOK}, nil)
if err == nil {
t.Error("Expected an authentication error but got nil error")
}

if !strings.HasPrefix(err.Error(), expectedError) {
t.Errorf("Expected error message '%s' but got '%s'", expectedError, err.Error())
}
}

func TestUnitApiClient_UserManagedIdentity(t *testing.T) {
expectedError := "ManagedIdentityCredential authentication failed. the requested identity isn't assigned to this resource"

ctx := context.Background()
cfg := config.ProviderConfig{
UseMsi: true,
ClientId: uuid.NewString(),
}

x := api.NewApiClientBase(&cfg, api.NewAuthBase(&cfg))
_, err := x.Execute(ctx, []string{"test"}, "GET", "https://api.bap.microsoft.com", http.Header{}, nil, []int{http.StatusOK}, nil)
if err == nil {
t.Error("Expected an authentication error but got nil error")
}

if !strings.HasPrefix(err.Error(), expectedError) {
t.Errorf("Expected error message '%s' but got '%s'", expectedError, err.Error())
}
}
18 changes: 18 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
type ProviderConfig struct {
UseCli bool
UseOidc bool
UseMsi bool

TenantId string
ClientId string
Expand All @@ -24,6 +25,8 @@ type ProviderConfig struct {
OidcToken string
OidcTokenFilePath string

AzDOServiceConnectionID string

// internal runtime configuration values
TestMode bool
Urls ProviderConfigUrls
Expand All @@ -41,6 +44,18 @@ type ProviderConfigUrls struct {
LicensingUrl string
}

func (model *ProviderConfig) IsUserManagedIdentityProvided() bool {
return model.UseMsi && model.ClientId != ""
}

func (model *ProviderConfig) IsSystemManagedIdentityProvided() bool {
return model.UseMsi && model.ClientId == "" // The switch that consumes this could be structured to avoid the second check, but we don't have a guarantee of what's consuming this.
}

func (model *ProviderConfig) IsAzDOWorkloadIdentityFederationProvided() bool {
return model.UseOidc && model.AzDOServiceConnectionID != ""
}

func (model *ProviderConfig) IsClientSecretCredentialsProvided() bool {
return model.ClientId != "" && model.ClientSecret != "" && model.TenantId != ""
}
Expand All @@ -61,6 +76,7 @@ func (model *ProviderConfig) IsOidcProvided() bool {
type ProviderConfigModel struct {
UseCli types.Bool `tfsdk:"use_cli"`
UseOidc types.Bool `tfsdk:"use_oidc"`
UseMsi types.Bool `tfsdk:"use_msi"`

Cloud types.String `tfsdk:"cloud"`
TelemetryOptout types.Bool `tfsdk:"telemetry_optout"`
Expand All @@ -77,4 +93,6 @@ type ProviderConfigModel struct {
OidcRequestUrl types.String `tfsdk:"oidc_request_url"`
OidcToken types.String `tfsdk:"oidc_token"`
OidcTokenFilePath types.String `tfsdk:"oidc_token_file_path"`

AzDOServiceConnectionID types.String `tfsdk:"azdo_service_connection_id"`
}
2 changes: 2 additions & 0 deletions internal/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,12 @@ const (
ENV_VAR_POWER_PLATFORM_CLIENT_SECRET = "POWER_PLATFORM_CLIENT_SECRET"
ENV_VAR_POWER_PLATFORM_USE_OIDC = "POWER_PLATFORM_USE_OIDC"
ENV_VAR_POWER_PLATFORM_USE_CLI = "POWER_PLATFORM_USE_CLI"
ENV_VAR_POWER_PLATFORM_USE_MSI = "POWER_PLATFORM_USE_MSI"
ENV_VAR_POWER_PLATFORM_CLIENT_CERTIFICATE = "POWER_PLATFORM_CLIENT_CERTIFICATE"
ENV_VAR_POWER_PLATFORM_CLIENT_CERTIFICATE_FILE_PATH = "POWER_PLATFORM_CLIENT_CERTIFICATE_FILE_PATH"
ENV_VAR_POWER_PLATFORM_CLIENT_CERTIFICATE_PASSWORD = "POWER_PLATFORM_CLIENT_CERTIFICATE_PASSWORD"
ENV_VAR_POWER_PLATFORM_TELEMETRY_OPTOUT = "POWER_PLATFORM_TELEMETRY_OPTOUT"
ENV_VAR_POWER_PLATFORM_AZDO_SERVICE_CONNECTION_ID = "POWER_PLATFORM_AZDO_SERVICE_CONNECTION_ID"

ENV_VAR_ARM_OIDC_REQUEST_URL = "ARM_OIDC_REQUEST_URL"
ENV_VAR_ACTIONS_ID_TOKEN_REQUEST_URL = "ACTIONS_ID_TOKEN_REQUEST_URL"
Expand Down
Loading
Loading