Skip to content

Commit

Permalink
Merge pull request #183 from jfrog/add-tfc-workload-id-token-support
Browse files Browse the repository at this point in the history
Add TFC workload id token support
  • Loading branch information
alexhung authored May 6, 2024
2 parents 1df5984 + 51b2b24 commit d6ab977
Show file tree
Hide file tree
Showing 11 changed files with 230 additions and 72 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 2.6.0 (May 6, 2024). Tested on Artifactory 7.77.11 and Xray 3.94.5

FEATURES:

* provider: Add support for Terraform Cloud Workload Identity Token. PR: [#183](https://github.com/jfrog/terraform-provider-xray/pull/183)

## 2.5.1 (April 30, 2024). Tested on Artifactory 7.77.10 and Xray 3.94.5

* resource/xray_settings: Migrate from SDKv2 to Plugin Framework. PR: [#174](https://github.com/jfrog/terraform-provider-xray/pull/174)
Expand Down
6 changes: 3 additions & 3 deletions docs/data-sources/artifacts_scan.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Read-Only:
- `services` (Attributes) (see [below for nested schema](#nestedatt--results--exposures_issues--categories--services))

<a id="nestedatt--results--exposures_issues--categories--applications"></a>
### Nested Schema for `results.exposures_issues.categories.services`
### Nested Schema for `results.exposures_issues.categories.applications`

Read-Only:

Expand All @@ -96,7 +96,7 @@ Read-Only:


<a id="nestedatt--results--exposures_issues--categories--iac"></a>
### Nested Schema for `results.exposures_issues.categories.services`
### Nested Schema for `results.exposures_issues.categories.iac`

Read-Only:

Expand All @@ -110,7 +110,7 @@ Read-Only:


<a id="nestedatt--results--exposures_issues--categories--secrets"></a>
### Nested Schema for `results.exposures_issues.categories.services`
### Nested Schema for `results.exposures_issues.categories.secrets`

Read-Only:

Expand Down
61 changes: 60 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ The following 3 license types (`jq .type`) do **NOT** support APIs:
- JCR Edition
- OSS

## Terraform CLI version support

Current version support [Terraform Protocol v6](https://developer.hashicorp.com/terraform/plugin/terraform-plugin-protocol#protocol-version-6) which mean Terraform CLI version 1.0 and later.

## Example Usage

```terraform
Expand Down Expand Up @@ -473,7 +477,9 @@ resource "xray_ignore_rule" "ignore-rule-2590577" {

## Authentication

The Xray provider supports one type of authentication using Bearer token.
The Xray provider supports supports two ways of authentication. The following methods are supported:
* Bearer Token
* Terraform Cloud OIDC provider

### Bearer Token
Artifactory access tokens may be used via the Authorization header by providing the `access_token` field to the provider
Expand All @@ -490,11 +496,64 @@ provider "xray" {
}
```

### Terraform Cloud OIDC Provider

If you are using this provider on Terraform Cloud and wish to use dynamic credentials instead of static access token for authentication with JFrog platform, you can leverage Terraform as the OIDC provider.

To setup dynamic credentials, follow these steps:
1. Configure Terraform Cloud as a generic OIDC provider
2. Set environment variable in your Terraform Workspace
3. Setup Terraform Cloud in your configuration

During the provider start up, if it finds env var `TFC_WORKLOAD_IDENTITY_TOKEN` it will use this token with your JFrog instance to exchange for a short-live access token. If that is successful, the provider will the access token for all subsequent API requests with the JFrog instance.

#### Configure Terraform Cloud as generic OIDC provider

Follow [confgure an OIDC integration](https://jfrog.com/help/r/jfrog-platform-administration-documentation/configure-an-oidc-integration). Enter a name for the provider, e.g. `terraform-cloud`. Use `https://app.terraform.io` for "Provider URL". Choose your own value for "Audience", e.g. `jfrog-terraform-cloud`.

Then [configure an identity mapping](https://jfrog.com/help/r/jfrog-platform-administration-documentation/configure-identity-mappings) with an empty "Claims JSON" (`{}`), and select the "Token scope", "User", and "Service" as desired.

#### Set environment variable in your Terraform Workspace

In your workspace, add an environment variable `TFC_WORKLOAD_IDENTITY_AUDIENCE` with audience value (e.g. `jfrog-terraform-cloud`) from JFrog OIDC integration above. See [Manually Generating Workload Identity Tokens](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/dynamic-provider-credentials/manual-generation) for more details.

When a run starts on Terraform Cloud, it will create a workload identity token with the specified audience and assigns it to the environment variable `TFC_WORKLOAD_IDENTITY_TOKEN` for the provider to consume.

#### Setup Terraform Cloud in your configuration

Add `cloud` block to `terraform` block, and add `oidc_provider_name` attribute (from JFrog OIDC integration) to provider block:

```terraform
terraform {
cloud {
organization = "my-org"
workspaces {
name = "my-workspace"
}
}
required_providers {
xray = {
source = "jfrog/xray"
version = "2.5.1"
}
}
}
provider "xray" {
url = "https://myinstance.jfrog.io"
oidc_provider_name = "terraform-cloud"
}
```

**Note:** Ensure `access_token` attribute and `JFROG_ACCESS_TOKEN` env var are not set

<!-- schema generated by tfplugindocs -->
## Schema

### Optional

- `access_token` (String, Sensitive) This is a bearer token that can be given to you by your admin under `Identity and Access`
- `check_license` (Boolean) Toggle for pre-flight checking of Artifactory Pro and Enterprise license. Default to `true`.
- `oidc_provider_name` (String) OIDC provider name. See [Configure an OIDC Integration](https://jfrog.com/help/r/jfrog-platform-administration-documentation/configure-an-oidc-integration) for more details.
- `url` (String) URL of Xray. This can also be sourced from the `XRAY_URL` or `JFROG_URL` environment variable. Default to 'http://localhost:8081' if not set.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ require (
github.com/hashicorp/terraform-plugin-mux v0.15.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0
github.com/hashicorp/terraform-plugin-testing v1.5.1
github.com/jfrog/terraform-provider-shared v1.25.0
github.com/jfrog/terraform-provider-shared v1.25.1
github.com/samber/lo v1.39.0
golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819
)
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jfrog/terraform-provider-shared v1.25.0 h1:FyubiyixS7QnUXb1JKKo957tCFZO2xVKSPLyKzBnvKk=
github.com/jfrog/terraform-provider-shared v1.25.0/go.mod h1:L987Z8XO4cuv7ys4Tw6sP/LESw7z0Dji0U2ysR8FUP4=
github.com/jfrog/terraform-provider-shared v1.25.1 h1:ndbE78OvIUy5QfjnxCs7xq/M0PwBVk2p2FU3Gr4M6G0=
github.com/jfrog/terraform-provider-shared v1.25.1/go.mod h1:L987Z8XO4cuv7ys4Tw6sP/LESw7z0Dji0U2ysR8FUP4=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
Expand Down
92 changes: 53 additions & 39 deletions pkg/xray/provider/framework.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package provider
import (
"context"
"fmt"
"os"

"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
Expand All @@ -26,9 +25,10 @@ type XrayProvider struct{}

// XrayProviderModel describes the provider data model.
type XrayProviderModel struct {
Url types.String `tfsdk:"url"`
AccessToken types.String `tfsdk:"access_token"`
CheckLicense types.Bool `tfsdk:"check_license"`
Url types.String `tfsdk:"url"`
AccessToken types.String `tfsdk:"access_token"`
OIDCProviderName types.String `tfsdk:"oidc_provider_name"`
CheckLicense types.Bool `tfsdk:"check_license"`
}

// Metadata satisfies the provider.Provider interface for ArtifactoryProvider
Expand Down Expand Up @@ -56,6 +56,13 @@ func (p *XrayProvider) Schema(ctx context.Context, req provider.SchemaRequest, r
},
Description: "This is a bearer token that can be given to you by your admin under `Identity and Access`",
},
"oidc_provider_name": schema.StringAttribute{
Optional: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
Description: "OIDC provider name. See [Configure an OIDC Integration](https://jfrog.com/help/r/jfrog-platform-administration-documentation/configure-an-oidc-integration) for more details.",
},
"check_license": schema.BoolAttribute{
Optional: true,
Description: "Toggle for pre-flight checking of Artifactory Pro and Enterprise license. Default to `true`.",
Expand All @@ -66,8 +73,8 @@ func (p *XrayProvider) Schema(ctx context.Context, req provider.SchemaRequest, r

func (p *XrayProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
// Check environment variables, first available OS variable will be assigned to the var
url := CheckEnvVars([]string{"JFROG_URL", "XRAY_URL"}, "")
accessToken := CheckEnvVars([]string{"JFROG_ACCESS_TOKEN", "XRAY_ACCESS_TOKEN"}, "")
url := util.CheckEnvVars([]string{"JFROG_URL", "XRAY_URL"}, "http://localhost:8081")
accessToken := util.CheckEnvVars([]string{"JFROG_ACCESS_TOKEN", "XRAY_ACCESS_TOKEN"}, "")

var config XrayProviderModel

Expand All @@ -77,22 +84,6 @@ func (p *XrayProvider) Configure(ctx context.Context, req provider.ConfigureRequ
return
}

// Check configuration data, which should take precedence over
// environment variable data, if found.
if config.AccessToken.ValueString() != "" {
accessToken = config.AccessToken.ValueString()
}

if accessToken == "" {
resp.Diagnostics.AddError(
"Missing JFrog Access Token",
"While configuring the provider, the Access Token was not found in "+
"the JFROG_ACCESS_TOKEN/XRAY_ACCESS_TOKEN environment variable or provider "+
"configuration block access_token attribute.",
)
return
}

if config.Url.ValueString() != "" {
url = config.Url.ValueString()
}
Expand All @@ -101,21 +92,53 @@ func (p *XrayProvider) Configure(ctx context.Context, req provider.ConfigureRequ
resp.Diagnostics.AddError(
"Missing URL Configuration",
"While configuring the provider, the url was not found in "+
"the JFROG_URL/XRAY_URL environment variable or provider "+
"the JFROG_URL/ARTIFACTORY_URL environment variable or provider "+
"configuration block url attribute.",
)
return
}

restyBase, err := client.Build(url, productId)
restyClient, err := client.Build(url, productId)
if err != nil {
resp.Diagnostics.AddError(
"Error creating Resty client",
err.Error(),
)
return
}

oidcAccessToken, err := util.OIDCTokenExchange(ctx, restyClient, config.OIDCProviderName.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Failed OIDC ID token exchange",
err.Error(),
)
return
}

// use token from OIDC provider, which should take precedence over
// environment variable data, if found.
if oidcAccessToken != "" {
accessToken = oidcAccessToken
}

// Check configuration data, which should take precedence over
// environment variable data, if found.
if config.AccessToken.ValueString() != "" {
accessToken = config.AccessToken.ValueString()
}

if accessToken == "" {
resp.Diagnostics.AddError(
"Missing JFrog Access Token",
"While configuring the provider, the Access Token was not found in "+
"the JFROG_ACCESS_TOKEN/XRAY_ACCESS_TOKEN environment variable, or provider "+
"configuration block access_token attribute, or from Terraform Cloud Workload Identity token.",
)
return
}

restyBase, err = client.AddAuth(restyBase, "", accessToken)
restyClient, err = client.AddAuth(restyClient, "", accessToken)
if err != nil {
resp.Diagnostics.AddError(
"Error adding Auth to Resty client",
Expand All @@ -124,7 +147,7 @@ func (p *XrayProvider) Configure(ctx context.Context, req provider.ConfigureRequ
}

if config.CheckLicense.IsNull() || config.CheckLicense.ValueBool() {
if licenseDs := util.CheckArtifactoryLicense(restyBase, "Enterprise", "Commercial", "Edge"); licenseDs != nil {
if licenseDs := util.CheckArtifactoryLicense(restyClient, "Enterprise", "Commercial", "Edge"); licenseDs != nil {
resp.Diagnostics.AddError(
"Error checking license",
licenseDs.Error(),
Expand All @@ -133,7 +156,7 @@ func (p *XrayProvider) Configure(ctx context.Context, req provider.ConfigureRequ
}
}

version, err := util.GetXrayVersion(restyBase)
version, err := util.GetXrayVersion(restyClient)
if err != nil {
resp.Diagnostics.AddWarning(
"Error getting Xray version",
Expand All @@ -143,16 +166,16 @@ func (p *XrayProvider) Configure(ctx context.Context, req provider.ConfigureRequ
}

featureUsage := fmt.Sprintf("Terraform/%s", req.TerraformVersion)
go util.SendUsage(ctx, restyBase, productId, featureUsage)
go util.SendUsage(ctx, restyClient.R(), productId, featureUsage)

resp.DataSourceData = util.ProviderMetadata{
Client: restyBase,
Client: restyClient,
ProductId: productId,
XrayVersion: version,
}

resp.ResourceData = util.ProviderMetadata{
Client: restyBase,
Client: restyClient,
ProductId: productId,
XrayVersion: version,
}
Expand All @@ -179,12 +202,3 @@ func Framework() func() provider.Provider {
return &XrayProvider{}
}
}

func CheckEnvVars(vars []string, dv string) string {
for _, k := range vars {
if v := os.Getenv(k); v != "" {
return v
}
}
return dv
}
Loading

0 comments on commit d6ab977

Please sign in to comment.