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 support for OAuth2 password grant #10

Merged
merged 7 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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