From 4611408834d99fb2cd89cdf6d58f7193327ae028 Mon Sep 17 00:00:00 2001 From: Rafael Perez Date: Tue, 12 Nov 2024 17:22:15 +0100 Subject: [PATCH 1/3] Allow to use a refresh token in the provider to get the access token --- go.sum | 3 ++ internal/provider/provider.go | 45 ++++++++++++++++++++++++++++-- internal/provider/provider_test.go | 4 +-- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/go.sum b/go.sum index 74c212a..55958e0 100644 --- a/go.sum +++ b/go.sum @@ -28,12 +28,14 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -49,6 +51,7 @@ github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6 github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4= github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 167f6e0..a36f6d7 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -2,10 +2,13 @@ package provider import ( "context" + "encoding/json" + "fmt" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/shurcooL/graphql" "golang.org/x/oauth2" + "net/http" "net/url" ) @@ -23,7 +26,13 @@ func New(version string) func() *schema.Provider { Type: schema.TypeString, Optional: true, DefaultFunc: schema.EnvDefaultFunc("PRISMATIC_TOKEN", ""), - Description: "An [access token to use for headless authentication](https://prismatic.io/docs/cli/cli-usage/#headless-prism-usage-for-cicd-pipelines) of Prismatic API calls.", + Description: "An [access token to use for headless authentication](https://prismatic.io/docs/cli/cli-usage/#headless-prism-usage-for-cicd-pipelines) of Prismatic API calls. Refresh token parameter is not going to be used if token is provided.", + }, + "refresh_token": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("PRISM_REFRESH_TOKEN", ""), + Description: "A [refresh token to use for headless authentication](https://prismatic.io/docs/cli/cli-usage/#headless-prism-usage-for-cicd-pipelines), of Prismatic API calls. Token parameter is not going to be used if refresh token is provided, a new access token will be requested using the refresh token provided.", }, }, ResourcesMap: map[string]*schema.Resource{ @@ -49,6 +58,7 @@ func configure(version string, p *schema.Provider) func(context.Context, *schema return func(ctx context.Context, d *schema.ResourceData) (any, diag.Diagnostics) { baseUrl := d.Get("url").(string) token := d.Get("token").(string) + refreshToken := d.Get("refresh_token").(string) var diags diag.Diagnostics @@ -59,11 +69,11 @@ func configure(version string, p *schema.Provider) func(context.Context, *schema Detail: "Unable to create a Prismatic client without a url.", }) } - if token == "" { + if token == "" && refreshToken == "" { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, Summary: "Unable to create a Prismatic client", - Detail: "Unable to create a Prismatic client without an authorization token. Please either pass in an authorization token to the Prismatic provider, or set an environment variable, PRISMATIC_TOKEN", + Detail: "Unable to create a Prismatic client without an authorization token or a refresh token. Please either pass in an authorization token or a refresh_token to the Prismatic provider. Optionally, you can set a environment variable, PRISMATIC_TOKEN or PRISM_REFRESH_TOKEN", }) } @@ -79,6 +89,14 @@ func configure(version string, p *schema.Provider) func(context.Context, *schema u.Path = "api" apiUrl := u.String() + if refreshToken != "" { + accessToken, err := refreshAccessToken(apiUrl, refreshToken) + if err != nil { + return nil, diag.FromErr(err) + } + token = *accessToken + } + src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) httpClient := oauth2.NewClient(context.Background(), src) @@ -86,3 +104,24 @@ func configure(version string, p *schema.Provider) func(context.Context, *schema return client, diags } } + +func refreshAccessToken(apiUrl string, refreshToken string) (*string, error) { + resp, err := http.PostForm(apiUrl, url.Values{"refresh_token": {refreshToken}}) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to refresh access token: %s", resp.Status) + } + + var result struct { + AccessToken string `json:"access_token"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + + return &result.AccessToken, nil +} diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index e133e20..7a7a423 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -23,8 +23,8 @@ func testAccPreCheck(t *testing.T) { t.Fatal("PRISMATIC_URL must be set for acceptance tests") } - if v := os.Getenv("PRISMATIC_TOKEN"); v == "" { - t.Fatal("PRISMATIC_TOKEN must be set for acceptance tests") + if os.Getenv("PRISMATIC_TOKEN") == "" && os.Getenv("PRISMATIC_REFRESH_TOKEN") == "" { + t.Fatal("Either PRISMATIC_TOKEN or PRISMATIC_REFRESH_TOKEN must be set for acceptance tests") } err := testAccProvider.Configure(context.Background(), terraform.NewResourceConfigRaw(nil)) From ff4faca5536c29ada4968fb3d4a828bf2b2cdfc7 Mon Sep 17 00:00:00 2001 From: Rafael Perez Date: Tue, 12 Nov 2024 17:28:29 +0100 Subject: [PATCH 2/3] Align the refresh token env variable naming --- internal/provider/provider.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/provider/provider.go b/internal/provider/provider.go index a36f6d7..ddee7d8 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -31,7 +31,7 @@ func New(version string) func() *schema.Provider { "refresh_token": { Type: schema.TypeString, Optional: true, - DefaultFunc: schema.EnvDefaultFunc("PRISM_REFRESH_TOKEN", ""), + DefaultFunc: schema.EnvDefaultFunc("PRISMATIC_REFRESH_TOKEN", ""), Description: "A [refresh token to use for headless authentication](https://prismatic.io/docs/cli/cli-usage/#headless-prism-usage-for-cicd-pipelines), of Prismatic API calls. Token parameter is not going to be used if refresh token is provided, a new access token will be requested using the refresh token provided.", }, }, @@ -73,7 +73,7 @@ func configure(version string, p *schema.Provider) func(context.Context, *schema diags = append(diags, diag.Diagnostic{ Severity: diag.Error, Summary: "Unable to create a Prismatic client", - Detail: "Unable to create a Prismatic client without an authorization token or a refresh token. Please either pass in an authorization token or a refresh_token to the Prismatic provider. Optionally, you can set a environment variable, PRISMATIC_TOKEN or PRISM_REFRESH_TOKEN", + Detail: "Unable to create a Prismatic client without an authorization token or a refresh token. Please either pass in an authorization token or a refresh_token to the Prismatic provider. Optionally, you can set a environment variable, PRISMATIC_TOKEN or PRISMATIC_REFRESH_TOKEN", }) } From b9ed6a3f019ea019a5dd296f9928794b68fc2e56 Mon Sep 17 00:00:00 2001 From: Rafael Perez Date: Tue, 12 Nov 2024 18:18:09 +0100 Subject: [PATCH 3/3] Update documentation and fix refresh token mechanism --- examples/provider/provider_refresh.tf | 4 ++++ internal/provider/provider.go | 30 ++++++++++++++++++++------- templates/index.md.tmpl | 8 ++++++- 3 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 examples/provider/provider_refresh.tf diff --git a/examples/provider/provider_refresh.tf b/examples/provider/provider_refresh.tf new file mode 100644 index 0000000..3b95fb6 --- /dev/null +++ b/examples/provider/provider_refresh.tf @@ -0,0 +1,4 @@ +provider "prismatic" { + url = "" + refresh_token = "" +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index ddee7d8..e12e719 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1,15 +1,17 @@ package provider import ( + "bytes" "context" "encoding/json" "fmt" + "net/http" + "net/url" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/shurcooL/graphql" "golang.org/x/oauth2" - "net/http" - "net/url" ) func New(version string) func() *schema.Provider { @@ -54,6 +56,10 @@ func New(version string) func() *schema.Provider { } } +type RefreshTokenRequest struct { + RefreshToken string `json:"refresh_token"` +} + func configure(version string, p *schema.Provider) func(context.Context, *schema.ResourceData) (any, diag.Diagnostics) { return func(ctx context.Context, d *schema.ResourceData) (any, diag.Diagnostics) { baseUrl := d.Get("url").(string) @@ -86,17 +92,17 @@ func configure(version string, p *schema.Provider) func(context.Context, *schema return nil, diag.FromErr(err) } - u.Path = "api" - apiUrl := u.String() - if refreshToken != "" { - accessToken, err := refreshAccessToken(apiUrl, refreshToken) + accessToken, err := refreshAccessToken(u, RefreshTokenRequest{RefreshToken: refreshToken}) if err != nil { return nil, diag.FromErr(err) } token = *accessToken } + u.Path = "api" + apiUrl := u.String() + src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) httpClient := oauth2.NewClient(context.Background(), src) @@ -105,8 +111,16 @@ func configure(version string, p *schema.Provider) func(context.Context, *schema } } -func refreshAccessToken(apiUrl string, refreshToken string) (*string, error) { - resp, err := http.PostForm(apiUrl, url.Values{"refresh_token": {refreshToken}}) +func refreshAccessToken(baseUrl *url.URL, refreshToken RefreshTokenRequest) (*string, error) { + baseUrl.Path = "/auth/refresh" + apiUrl := baseUrl.String() + + body, err := json.Marshal(refreshToken) + if err != nil { + return nil, err + } + + resp, err := http.Post(apiUrl, "application/json", bytes.NewBuffer(body)) if err != nil { return nil, err } diff --git a/templates/index.md.tmpl b/templates/index.md.tmpl index 94175a8..f3a8396 100644 --- a/templates/index.md.tmpl +++ b/templates/index.md.tmpl @@ -16,6 +16,12 @@ Use the navigation to the left to read about the available resources and data so {{ tffile "examples/provider/provider.tf" }} +In case you need to use a refresh token to get automatically a headless access token you can use: + +{{ tffile "examples/provider/provider_with_refresh_token.tf" }} + +In this case the token provided is not going to be used. + ~> Hard-coding credentials into any Terraform configuration is not recommended and risks secret leakage should this file ever be committed to a public version control system. See [Environment Variables](#environment-variables) for a better alternative. @@ -24,7 +30,7 @@ better alternative. ## Environment Variables -You can provide your credentials using the `PRISMATIC_URL` and `PRISMATIC_TOKEN` environment variables. +You can provide your credentials using the `PRISMATIC_URL` and `PRISMATIC_TOKEN` environment variables. Or if you need to use a refresh token you can use `PRISMATIC_URL` and `PRISMATIC_REFRESH_TOKEN` environment variables, remember the `PRISMATIC_TOKEN` is going to be ignored in that case. {{ tffile "examples/provider/provider_with_env_vars.tf" }}