Skip to content

Commit

Permalink
Merge pull request #10 from enthought/add-client-credential-grant
Browse files Browse the repository at this point in the history
Add support for OAuth2 password grant
  • Loading branch information
ddebeau authored Sep 5, 2024
2 parents 06b6b82 + 03ca9d8 commit e938124
Show file tree
Hide file tree
Showing 7 changed files with 400 additions and 53 deletions.
1 change: 0 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ linters:
enable:
- durationcheck
- errcheck
- exportloopref
- forcetypeassert
- godot
- gofmt
Expand Down
8 changes: 8 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ an `OAuth 2 Access Token`. Since there is no mechanism in Quay for creating appl
is recommended to create a separate organization and application for Terraform. Be sure to create the
`OAuth 2 Access Token` using a service account since the token will be tied to that account.

An alternative method of authentication is to use JWT OAuth2 access tokens that are generated by an external Identity
Provider (Quay must be configured to trust this Identity Provider). Only password grant is supported at this time.
Specify the `oauth2_*` set of variables to enable this feature.

## Example Usage

```terraform
Expand All @@ -27,6 +31,10 @@ provider "quay" {

### Optional

- `oauth2_client_id` (String) OAuth2 client ID. Used for generating a JWT OAuth2 access token with password grant. May also be provided via the QUAY_OAUTH2_CLIENT_ID environment variable.
- `oauth2_password` (String, Sensitive) OAuth2 password. Used for generating a JWT OAuth2 access token with password grant. May also be provided via the QUAY_OAUTH2_PASSWORD environment variable.
- `oauth2_token_url` (String) OAuth2 token endpoint URL. Used for generating a JWT OAuth2 access token with password grant. May also be provided via the QUAY_OAUTH2_TOKEN_URL environment variable.
- `oauth2_username` (String) OAuth2 username. Used for generating a JWT OAuth2 access token with password grant. May also be provided via the QUAY_OAUTH2_USERNAME environment variable.
- `token` (String, Sensitive) Quay token. May also be provided via the QUAY_TOKEN environment variable.
- `url` (String) Quay URL. May also be provided via the QUAY_URL environment variable. Example: https://quay.example.com

1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/hashicorp/terraform-plugin-go v0.23.0
github.com/hashicorp/terraform-plugin-log v0.9.0
github.com/hashicorp/terraform-plugin-testing v1.10.0
golang.org/x/oauth2 v0.23.0
)

require (
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down
246 changes: 194 additions & 52 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"golang.org/x/oauth2"

"github.com/enthought/terraform-provider-quay/quay_api"
)
Expand All @@ -26,8 +27,12 @@ func New() func() provider.Provider {
type quayProvider struct{}

type quayProviderModel struct {
Url types.String `tfsdk:"url"`
Token types.String `tfsdk:"token"`
Url types.String `tfsdk:"url"`
Token types.String `tfsdk:"token"`
OAuth2Username types.String `tfsdk:"oauth2_username"`
OAuth2Password types.String `tfsdk:"oauth2_password"`
OAuth2ClientID types.String `tfsdk:"oauth2_client_id"`
OAuth2TokenURL types.String `tfsdk:"oauth2_token_url"`
}

func (p *quayProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
Expand All @@ -41,6 +46,27 @@ func (p *quayProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp
Optional: true,
Sensitive: true,
},
"oauth2_username": schema.StringAttribute{
Description: "OAuth2 username. Used for generating a JWT OAuth2 access token with password grant. " +
"May also be provided via the QUAY_OAUTH2_USERNAME environment variable.",
Optional: true,
},
"oauth2_password": schema.StringAttribute{
Description: "OAuth2 password. Used for generating a JWT OAuth2 access token with password grant. " +
"May also be provided via the QUAY_OAUTH2_PASSWORD environment variable.",
Optional: true,
Sensitive: true,
},
"oauth2_client_id": schema.StringAttribute{
Description: "OAuth2 client ID. Used for generating a JWT OAuth2 access token with password grant. " +
"May also be provided via the QUAY_OAUTH2_CLIENT_ID environment variable.",
Optional: true,
},
"oauth2_token_url": schema.StringAttribute{
Description: "OAuth2 token endpoint URL. Used for generating a JWT OAuth2 access token with password grant. " +
"May also be provided via the QUAY_OAUTH2_TOKEN_URL environment variable.",
Optional: true,
},
}}
}

Expand All @@ -54,94 +80,210 @@ func (p *quayProvider) Configure(ctx context.Context, req provider.ConfigureRequ
return
}

if config.Url.IsUnknown() {
resp.Diagnostics.AddAttributeError(
path.Root("url"),
"Unknown Quay URL",
"The provider cannot create the Quay client as the URL value is unknown. "+
"Either target apply the source of the value first, set the value statically in the configuration, or use the QUAY_URL environment variable",
)
url := os.Getenv("QUAY_URL")
token := os.Getenv("QUAY_TOKEN")
oauth2Username := os.Getenv("QUAY_OAUTH2_USERNAME")
oauth2Password := os.Getenv("QUAY_OAUTH2_PASSWORD")
oauth2ClientID := os.Getenv("QUAY_OAUTH2_CLIENT_ID")
oauth2TokenURL := os.Getenv("QUAY_OAUTH2_TOKEN_URL")

if !config.Url.IsNull() {
url = config.Url.ValueString()
}

if config.Token.IsUnknown() {
resp.Diagnostics.AddAttributeError(
path.Root("token"),
"Unknown Quay token",
"The provider cannot create the Quay client as the token value is unknown. "+
"Either target apply the source of the value first, set the value statically in the configuration, or use the QUAY_TOKEN environment variable",
)
if !config.Token.IsNull() {
token = config.Token.ValueString()
}

if !config.OAuth2Username.IsNull() {
oauth2Username = config.OAuth2Username.ValueString()
}

if !config.OAuth2Password.IsNull() {
oauth2Password = config.OAuth2Password.ValueString()
}

if !config.OAuth2ClientID.IsNull() {
oauth2ClientID = config.OAuth2ClientID.ValueString()
}

if !config.OAuth2TokenURL.IsNull() {
oauth2TokenURL = config.OAuth2TokenURL.ValueString()
}

ctx = tflog.SetField(ctx, "url", url)
ctx = tflog.SetField(ctx, "token", token)
ctx = tflog.SetField(ctx, "oauth2_username", oauth2Username)
ctx = tflog.SetField(ctx, "oauth2_password", oauth2Password)
ctx = tflog.SetField(ctx, "oauth2_client_id", oauth2ClientID)
ctx = tflog.SetField(ctx, "oauth2_token_url", oauth2TokenURL)
ctx = tflog.MaskFieldValuesWithFieldKeys(ctx, "token")
ctx = tflog.MaskFieldValuesWithFieldKeys(ctx, "oauth2_password")

tflog.Debug(ctx, "Creating Quay client")

configuration := &quay_api.Configuration{
DefaultHeader: make(map[string]string),
UserAgent: "OpenAPI-Generator/1.0.0/go",
Debug: false,
Servers: quay_api.ServerConfigurations{
{
URL: url,
Description: "No description provided",
},
},
OperationServers: map[string]quay_api.ServerConfigurations{},
}

if token == "" {
oauth2Config := &oauth2.Config{
ClientID: oauth2ClientID,
Endpoint: oauth2.Endpoint{
TokenURL: oauth2TokenURL,
},
}

oauth2Token, err := oauth2Config.PasswordCredentialsToken(ctx, oauth2Username, oauth2Password)
if err != nil {
resp.Diagnostics.AddError("Error retrieving OAuth2 access token",
"Error retrieving OAuth2 access token: "+err.Error())
return
}

token = oauth2Token.AccessToken
}

configuration.AddDefaultHeader("Authorization", "Bearer "+token)
client := quay_api.NewAPIClient(configuration)

resp.DataSourceData = client
resp.ResourceData = client

tflog.Info(ctx, "Configured Quay client", map[string]any{"success": true})
}

func (p *quayProvider) ValidateConfig(ctx context.Context, req provider.ValidateConfigRequest, resp *provider.ValidateConfigResponse) {
var config quayProviderModel
diags := req.Config.Get(ctx, &config)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

quayURL := os.Getenv("QUAY_URL")
quayToken := os.Getenv("QUAY_TOKEN")
if config.Url.IsUnknown() || config.Token.IsUnknown() || config.OAuth2Username.IsUnknown() || config.OAuth2Password.IsUnknown() ||
config.OAuth2ClientID.IsUnknown() || config.OAuth2TokenURL.IsUnknown() {
resp.Diagnostics.AddError(
"Unknown configuration values",
"The provider cannot create the Quay client if any configuration values are unknown. "+
"Either target apply the source of the unknown value(s) first, set the value(s) statically in the configuration, or set the appropriate environment variable(s).",
)
return
}

url := os.Getenv("QUAY_URL")
token := os.Getenv("QUAY_TOKEN")
oauth2Username := os.Getenv("QUAY_OAUTH2_USERNAME")
oauth2Password := os.Getenv("QUAY_OAUTH2_PASSWORD")
oauth2ClientID := os.Getenv("QUAY_OAUTH2_CLIENT_ID")
oauth2TokenURL := os.Getenv("QUAY_OAUTH2_TOKEN_URL")

if !config.Url.IsNull() {
quayURL = config.Url.ValueString()
url = config.Url.ValueString()
}

if !config.Token.IsNull() {
quayToken = config.Token.ValueString()
token = config.Token.ValueString()
}

if quayURL == "" {
if !config.OAuth2Username.IsNull() {
oauth2Username = config.OAuth2Username.ValueString()
}

if !config.OAuth2Password.IsNull() {
oauth2Password = config.OAuth2Password.ValueString()
}

if !config.OAuth2ClientID.IsNull() {
oauth2ClientID = config.OAuth2ClientID.ValueString()
}

if !config.OAuth2TokenURL.IsNull() {
oauth2TokenURL = config.OAuth2TokenURL.ValueString()
}
if token != "" && (oauth2Username != "" || oauth2Password != "" || oauth2ClientID != "" || oauth2TokenURL != "") {
resp.Diagnostics.AddError(
"Cannot specify token and OAuth2 credentials",
"Token cannot be specified when OAuth2 credentials are also specified. You must pick one authentication method.",
)
return
}

if url == "" {
resp.Diagnostics.AddAttributeError(
path.Root("url"),
"Missing Quay URL",
"The provider cannot create the Quay client as there is a missing or empty value for the Quay URL. "+
"Set the URL in the configuration or use the QUAY_URL environment variable. ",
"Set the URL in the configuration or use the QUAY_URL environment variable.",
)
return
}

if !isValidURL(quayURL) {
if !isValidURL(url) {
resp.Diagnostics.AddAttributeError(
path.Root("url"),
"Quay URL is not a valid URL",
"The provider cannot create the Quay client as the URL provided is not valid.",
)
return
}

if quayToken == "" {
resp.Diagnostics.AddAttributeError(
path.Root("token"),
"Missing Quay token",
"The provider cannot create the Quay client as there is a missing or empty value for the Quay token. "+
"Set the token in the configuration or use the QUAY_TOKEN environment variable. ",
if token == "" && oauth2Username == "" && oauth2Password == "" && oauth2ClientID == "" && oauth2TokenURL == "" {
resp.Diagnostics.AddError(
"Missing Quay token and OAuth2 credentials",
"The provider cannot create the Quay client as both the Quay token and OAuth2 credentials are missing or empty.",
)
return
}

if resp.Diagnostics.HasError() {
if token == "" && oauth2Username == "" {
resp.Diagnostics.AddAttributeError(
path.Root("oauth2_username"),
"Missing OAuth2 username",
"The provider cannot create the Quay client as there is a missing value or empty value for the OAuth2 username."+
"Set the OAuth2 username in the configuration or use the QUAY_OAUTH2_USERNAME environment variable.",
)
return
}

ctx = tflog.SetField(ctx, "quay_url", quayURL)
ctx = tflog.SetField(ctx, "quay_token", quayToken)
ctx = tflog.MaskFieldValuesWithFieldKeys(ctx, "quay_token")

tflog.Debug(ctx, "Creating Quay client")

configuration := &quay_api.Configuration{
DefaultHeader: make(map[string]string),
UserAgent: "OpenAPI-Generator/1.0.0/go",
Debug: false,
Servers: quay_api.ServerConfigurations{
{
URL: quayURL,
Description: "No description provided",
},
},
OperationServers: map[string]quay_api.ServerConfigurations{},
if token == "" && oauth2Password == "" {
resp.Diagnostics.AddAttributeError(
path.Root("oauth2_password"),
"Missing OAuth2 password",
"The provider cannot create the Quay client as there is a missing value or empty value for the OAuth2 password."+
"Set the OAuth2 password in the configuration or use the QUAY_OAUTH2_PASSWORD environment variable.",
)
return
}
configuration.AddDefaultHeader("Authorization", "Bearer "+quayToken)
client := quay_api.NewAPIClient(configuration)

resp.DataSourceData = client
resp.ResourceData = client
if token == "" && oauth2ClientID == "" {
resp.Diagnostics.AddAttributeError(
path.Root("oauth2_client_id"),
"Missing OAuth2 client ID",
"The provider cannot create the Quay client as there is a missing value or empty value for the OAuth2 client ID."+
"Set the OAuth2 client ID in the configuration or use the QUAY_OAUTH2_CLIENT_ID environment variable.",
)
return
}

tflog.Info(ctx, "Configured Quay client", map[string]any{"success": true})
if token == "" && oauth2TokenURL == "" {
resp.Diagnostics.AddAttributeError(
path.Root("oauth2_token_url"),
"Missing OAuth2 token URL",
"The provider cannot create the Quay client as there is a missing value or empty value for the OAuth2 token URL."+
"Set the OAuth2 token URL in the configuration or use the QUAY_OAUTH2_TOKEN_URL environment variable.",
)
return
}
}

func (p *quayProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
Expand Down
Loading

0 comments on commit e938124

Please sign in to comment.