feat(postgres): add iam roles anywhere auth profile (#3604)

Signed-off-by: Samantha Coyle <sam@diagrid.io>
Co-authored-by: Yaron Schneider <schneider.yaron@live.com>
This commit is contained in:
Sam 2024-12-03 15:17:13 -06:00 committed by GitHub
parent 1e095ed25a
commit 72c92fb1fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 462 additions and 196 deletions

View File

@ -38,7 +38,7 @@ func ParseBuiltinAuthenticationProfile(bi BuiltinAuthenticationProfile, componen
metadataPtr[j] = &profile.Metadata[j] metadataPtr[j] = &profile.Metadata[j]
} }
if componentTitle == "Apache Kafka" { if componentTitle == "Apache Kafka" || strings.ToLower(componentTitle) == "postgresql" {
removeRequiredOnSomeAWSFields(&metadataPtr) removeRequiredOnSomeAWSFields(&metadataPtr)
} }
@ -55,17 +55,17 @@ func ParseBuiltinAuthenticationProfile(bi BuiltinAuthenticationProfile, componen
// Note: We must apply the removal of deprecated fields after the merge!! // Note: We must apply the removal of deprecated fields after the merge!!
// Here, we remove some deprecated fields as we support the transition to a new auth profile // Here, we remove some deprecated fields as we support the transition to a new auth profile
if profile.Title == "AWS: Assume specific IAM Role" && componentTitle == "Apache Kafka" { if profile.Title == "AWS: Assume IAM Role" && componentTitle == "Apache Kafka" || profile.Title == "AWS: Assume IAM Role" && strings.ToLower(componentTitle) == "postgresql" {
merged = removeSomeDeprecatedFieldsOnUnrelatedAuthProfiles(merged) merged = removeSomeDeprecatedFieldsOnUnrelatedAuthProfiles(merged)
} }
// Here, there are no metadata fields that need deprecating // Here, there are no metadata fields that need deprecating
if profile.Title == "AWS: Credentials from Environment Variables" && componentTitle == "Apache Kafka" { if profile.Title == "AWS: Credentials from Environment Variables" && componentTitle == "Apache Kafka" || profile.Title == "AWS: Credentials from Environment Variables" && strings.ToLower(componentTitle) == "postgresql" {
merged = removeAllDeprecatedFieldsOnUnrelatedAuthProfiles(merged) merged = removeAllDeprecatedFieldsOnUnrelatedAuthProfiles(merged)
} }
// Here, this is a new auth profile, so rm all deprecating fields as unrelated. // Here, this is a new auth profile, so rm all deprecating fields as unrelated.
if profile.Title == "AWS: IAM Roles Anywhere" && componentTitle == "Apache Kafka" { if profile.Title == "AWS: IAM Roles Anywhere" && componentTitle == "Apache Kafka" || profile.Title == "AWS: IAM Roles Anywhere" && strings.ToLower(componentTitle) == "postgresql" {
merged = removeAllDeprecatedFieldsOnUnrelatedAuthProfiles(merged) merged = removeAllDeprecatedFieldsOnUnrelatedAuthProfiles(merged)
} }
@ -125,7 +125,7 @@ func removeSomeDeprecatedFieldsOnUnrelatedAuthProfiles(metadata []Metadata) []Me
filteredMetadata := []Metadata{} filteredMetadata := []Metadata{}
for _, field := range metadata { for _, field := range metadata {
if field.Name == "awsAccessKey" || field.Name == "awsSecretKey" || field.Name == "awsSessionToken" { if field.Name == "awsAccessKey" || field.Name == "awsSecretKey" || field.Name == "awsSessionToken" || field.Name == "awsRegion" {
continue continue
} else { } else {
filteredMetadata = append(filteredMetadata, field) filteredMetadata = append(filteredMetadata, field)

View File

@ -35,7 +35,7 @@ builtinAuthenticationProfiles:
description: | description: |
This maintains backwards compatibility with existing fields. This maintains backwards compatibility with existing fields.
It will be deprecated as of Dapr 1.17. Use 'region' instead. It will be deprecated as of Dapr 1.17. Use 'region' instead.
The AWS Region where the AWS Relational Database Service is deployed to. The AWS Region where the AWS service is deployed to.
example: '"us-east-1"' example: '"us-east-1"'
- name: awsAccessKey - name: awsAccessKey
type: string type: string
@ -82,7 +82,7 @@ builtinAuthenticationProfiles:
If both fields are set, then 'sessionName' value will be used. If both fields are set, then 'sessionName' value will be used.
Represents the session name for assuming a role. Represents the session name for assuming a role.
example: '"MyAppSession"' example: '"MyAppSession"'
default: '"MSKSASLDefaultSession"' default: '"DaprDefaultSession"'
authenticationProfiles: authenticationProfiles:
- title: "OIDC Authentication" - title: "OIDC Authentication"
description: | description: |

View File

@ -28,7 +28,7 @@ const (
type psqlMetadata struct { type psqlMetadata struct {
pgauth.PostgresAuthMetadata `mapstructure:",squash"` pgauth.PostgresAuthMetadata `mapstructure:",squash"`
aws.AWSIAM `mapstructure:",squash"` aws.DeprecatedPostgresIAM `mapstructure:",squash"`
Timeout time.Duration `mapstructure:"timeout" mapstructurealiases:"timeoutInSeconds"` Timeout time.Duration `mapstructure:"timeout" mapstructurealiases:"timeoutInSeconds"`
} }

View File

@ -56,25 +56,31 @@ builtinAuthenticationProfiles:
example: | example: |
"host=mydb.postgres.database.aws.com user=myapplication port=5432 dbname=dapr_test sslmode=require" "host=mydb.postgres.database.aws.com user=myapplication port=5432 dbname=dapr_test sslmode=require"
type: string type: string
- name: awsRegion
type: string
required: true
description: |
The AWS Region where the AWS Relational Database Service is deployed to.
example: '"us-east-1"'
- name: awsAccessKey - name: awsAccessKey
type: string type: string
required: true required: false
description: | description: |
Deprecated as of Dapr 1.17. Use 'accessKey' instead if using AWS IAM.
If both fields are set, then 'accessKey' value will be used.
AWS access key associated with an IAM account. AWS access key associated with an IAM account.
example: '"AKIAIOSFODNN7EXAMPLE"' example: '"AKIAIOSFODNN7EXAMPLE"'
- name: awsSecretKey - name: awsSecretKey
type: string type: string
required: true required: false
sensitive: true sensitive: true
description: | description: |
Deprecated as of Dapr 1.17. Use 'secretKey' instead if using AWS IAM.
If both fields are set, then 'secretKey' value will be used.
The secret key associated with the access key. The secret key associated with the access key.
example: '"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"' example: '"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"'
- name: awsRegion
type: string
required: false
description: |
This maintains backwards compatibility with existing fields.
It will be deprecated as of Dapr 1.17. Use 'region' instead.
The AWS Region where the AWS service is deployed to.
example: '"us-east-1"'
authenticationProfiles: authenticationProfiles:
- title: "Connection string" - title: "Connection string"
description: "Authenticate using a Connection String" description: "Authenticate using a Connection String"

View File

@ -26,6 +26,8 @@ import (
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
awsAuth "github.com/dapr/components-contrib/common/authentication/aws"
pgauth "github.com/dapr/components-contrib/common/authentication/postgresql"
"github.com/dapr/components-contrib/metadata" "github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
@ -45,6 +47,11 @@ type Postgres struct {
logger logger.Logger logger logger.Logger
db *pgxpool.Pool db *pgxpool.Pool
closed atomic.Bool closed atomic.Bool
enableAzureAD bool
enableAWSIAM bool
awsAuthProvider awsAuth.Provider
} }
// NewPostgres returns a new PostgreSQL output binding. // NewPostgres returns a new PostgreSQL output binding.
@ -59,16 +66,34 @@ func (p *Postgres) Init(ctx context.Context, meta bindings.Metadata) error {
if p.closed.Load() { if p.closed.Load() {
return errors.New("cannot initialize a previously-closed component") return errors.New("cannot initialize a previously-closed component")
} }
opts := pgauth.InitWithMetadataOpts{
AzureADEnabled: p.enableAzureAD,
AWSIAMEnabled: p.enableAWSIAM,
}
m := psqlMetadata{} m := psqlMetadata{}
err := m.InitWithMetadata(meta.Properties) if err := m.InitWithMetadata(meta.Properties); err != nil {
return err
}
var err error
poolConfig, err := m.GetPgxPoolConfig()
if err != nil { if err != nil {
return err return err
} }
poolConfig, err := m.GetPgxPoolConfig() if opts.AWSIAMEnabled && m.UseAWSIAM {
if err != nil { opts, validateErr := m.BuildAwsIamOptions(p.logger, meta.Properties)
return err if validateErr != nil {
return fmt.Errorf("failed to validate AWS IAM authentication fields: %w", validateErr)
}
var provider awsAuth.Provider
provider, err = awsAuth.NewProvider(ctx, *opts, awsAuth.GetConfig(*opts))
if err != nil {
return err
}
p.awsAuthProvider = provider
p.awsAuthProvider.UpdatePostgres(ctx, poolConfig)
} }
// This context doesn't control the lifetime of the connection pool, and is // This context doesn't control the lifetime of the connection pool, and is
@ -186,7 +211,11 @@ func (p *Postgres) Close() error {
} }
p.db = nil p.db = nil
return nil errs := make([]error, 1)
if p.awsAuthProvider != nil {
errs[0] = p.awsAuthProvider.Close()
}
return errors.Join(errs...)
} }
func (p *Postgres) query(ctx context.Context, sql string, args ...any) (result []byte, err error) { func (p *Postgres) query(ctx context.Context, sql string, args ...any) (result []byte, err error) {

View File

@ -15,16 +15,8 @@ package aws
import ( import (
"context" "context"
"errors"
"fmt"
"strconv"
"time"
"github.com/aws/aws-sdk-go-v2/config"
v2creds "github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/feature/rds/auth"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
@ -34,16 +26,6 @@ type EnvironmentSettings struct {
Metadata map[string]string Metadata map[string]string
} }
type AWSIAM struct {
// Ignored by metadata parser because included in built-in authentication profile
// Access key to use for accessing PostgreSQL.
AWSAccessKey string `json:"awsAccessKey" mapstructure:"awsAccessKey"`
// Secret key to use for accessing PostgreSQL.
AWSSecretKey string `json:"awsSecretKey" mapstructure:"awsSecretKey"`
// AWS region in which PostgreSQL is deployed.
AWSRegion string `json:"awsRegion" mapstructure:"awsRegion"`
}
// TODO: Delete in Dapr 1.17 so we can move all IAM fields to use the defaults of: // TODO: Delete in Dapr 1.17 so we can move all IAM fields to use the defaults of:
// accessKey and secretKey and region as noted in the docs, and Options struct above. // accessKey and secretKey and region as noted in the docs, and Options struct above.
type DeprecatedKafkaIAM struct { type DeprecatedKafkaIAM struct {
@ -55,14 +37,6 @@ type DeprecatedKafkaIAM struct {
StsSessionName string `json:"awsStsSessionName" mapstructure:"awsStsSessionName"` StsSessionName string `json:"awsStsSessionName" mapstructure:"awsStsSessionName"`
} }
type AWSIAMAuthOptions struct {
PoolConfig *pgxpool.Config `json:"poolConfig" mapstructure:"poolConfig"`
ConnectionString string `json:"connectionString" mapstructure:"connectionString"`
Region string `json:"region" mapstructure:"region"`
AccessKey string `json:"accessKey" mapstructure:"accessKey"`
SecretKey string `json:"secretKey" mapstructure:"secretKey"`
}
type Options struct { type Options struct {
Logger logger.Logger Logger logger.Logger
Properties map[string]string Properties map[string]string
@ -75,11 +49,20 @@ type Options struct {
Region string `json:"region" mapstructure:"region" mapstructurealiases:"awsRegion"` Region string `json:"region" mapstructure:"region" mapstructurealiases:"awsRegion"`
AccessKey string `json:"accessKey" mapstructure:"accessKey"` AccessKey string `json:"accessKey" mapstructure:"accessKey"`
SecretKey string `json:"secretKey" mapstructure:"secretKey"` SecretKey string `json:"secretKey" mapstructure:"secretKey"`
SessionName string `mapstructure:"sessionName"` SessionName string `json:"sessionName" mapstructure:"sessionName"`
AssumeRoleARN string `mapstructure:"assumeRoleArn"` AssumeRoleARN string `json:"assumeRoleArn" mapstructure:"assumeRoleArn"`
SessionToken string `json:"sessionToken" mapstructure:"sessionToken"`
Endpoint string Endpoint string
SessionToken string }
// TODO: Delete in Dapr 1.17 so we can move all IAM fields to use the defaults of:
// accessKey and secretKey and region as noted in the docs, and Options struct above.
type DeprecatedPostgresIAM struct {
// Access key to use for accessing PostgreSQL.
AccessKey string `json:"awsAccessKey" mapstructure:"awsAccessKey"`
// Secret key to use for accessing PostgreSQL.
SecretKey string `json:"awsSecretKey" mapstructure:"awsSecretKey"`
} }
func GetConfig(opts Options) *aws.Config { func GetConfig(opts Options) *aws.Config {
@ -106,9 +89,14 @@ type Provider interface {
ParameterStore() *ParameterStoreClients ParameterStore() *ParameterStoreClients
Kinesis() *KinesisClients Kinesis() *KinesisClients
Ses() *SesClients Ses() *SesClients
Kafka(KafkaOptions) (*KafkaClients, error) Kafka(KafkaOptions) (*KafkaClients, error)
// Postgres is an outlier to the others in the sense that we can update only it's config,
// as we use a max connection time of 8 minutes.
// This means that we can just update the config session credentials,
// and then in 8 minutes it will update to a new session automatically for us.
UpdatePostgres(context.Context, *pgxpool.Config)
Close() error Close() error
} }
@ -128,69 +116,6 @@ func NewEnvironmentSettings(md map[string]string) (EnvironmentSettings, error) {
return es, nil return es, nil
} }
func (opts *Options) GetAccessToken(ctx context.Context) (string, error) {
dbEndpoint := opts.PoolConfig.ConnConfig.Host + ":" + strconv.Itoa(int(opts.PoolConfig.ConnConfig.Port))
var authenticationToken string
// https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/UsingWithRDS.IAMDBAuth.Connecting.Go.html
// Default to load default config through aws credentials file (~/.aws/credentials)
awsCfg, err := config.LoadDefaultConfig(ctx)
// Note: in the event of an error with invalid config or failed to load config,
// then we fall back to using the access key and secret key.
switch {
case errors.Is(err, config.SharedConfigAssumeRoleError{}.Err),
errors.Is(err, config.SharedConfigLoadError{}.Err),
errors.Is(err, config.SharedConfigProfileNotExistError{}.Err):
// Validate if access key and secret access key are provided
if opts.AccessKey == "" || opts.SecretKey == "" {
return "", fmt.Errorf("failed to load default configuration for AWS using accessKey and secretKey: %w", err)
}
// Set credentials explicitly
awsCfg := v2creds.NewStaticCredentialsProvider(opts.AccessKey, opts.SecretKey, "")
authenticationToken, err = auth.BuildAuthToken(
ctx, dbEndpoint, opts.Region, opts.PoolConfig.ConnConfig.User, awsCfg)
if err != nil {
return "", fmt.Errorf("failed to create AWS authentication token: %w", err)
}
return authenticationToken, nil
case err != nil:
return "", errors.New("failed to load default AWS authentication configuration")
}
authenticationToken, err = auth.BuildAuthToken(
ctx, dbEndpoint, opts.Region, opts.PoolConfig.ConnConfig.User, awsCfg.Credentials)
if err != nil {
return "", fmt.Errorf("failed to create AWS authentication token: %w", err)
}
return authenticationToken, nil
}
func (opts *Options) InitiateAWSIAMAuth() error {
// Set max connection lifetime to 8 minutes in postgres connection pool configuration.
// Note: this will refresh connections before the 15 min expiration on the IAM AWS auth token,
// while leveraging the BeforeConnect hook to recreate the token in time dynamically.
opts.PoolConfig.MaxConnLifetime = time.Minute * 8
// Setup connection pool config needed for AWS IAM authentication
opts.PoolConfig.BeforeConnect = func(ctx context.Context, pgConfig *pgx.ConnConfig) error {
// Manually reset auth token with aws and reset the config password using the new iam token
pwd, errGetAccessToken := opts.GetAccessToken(ctx)
if errGetAccessToken != nil {
return fmt.Errorf("failed to refresh access token for iam authentication with PostgreSQL: %w", errGetAccessToken)
}
pgConfig.Password = pwd
opts.PoolConfig.ConnConfig.Password = pwd
return nil
}
return nil
}
// Coalesce is a helper function to return the first non-empty string from the inputs // Coalesce is a helper function to return the first non-empty string from the inputs
// This helps us to migrate away from the deprecated duplicate aws auth profile metadata fields in Dapr 1.17. // This helps us to migrate away from the deprecated duplicate aws auth profile metadata fields in Dapr 1.17.
func Coalesce(values ...string) string { func Coalesce(values ...string) string {

View File

@ -17,15 +17,22 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strconv"
"sync" "sync"
"time"
awsv2 "github.com/aws/aws-sdk-go-v2/aws" awsv2 "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/config"
v2creds "github.com/aws/aws-sdk-go-v2/credentials" v2creds "github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
"github.com/aws/aws-sdk-go-v2/feature/rds/auth"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/aws/session"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
@ -227,6 +234,104 @@ func (a *StaticAuth) Ses() *SesClients {
return a.clients.ses return a.clients.ses
} }
func (a *StaticAuth) UpdatePostgres(ctx context.Context, poolConfig *pgxpool.Config) {
a.mu.Lock()
defer a.mu.Unlock()
// Set max connection lifetime to 8 minutes in postgres connection pool configuration.
// Note: this will refresh connections before the 15 min expiration on the IAM AWS auth token,
// while leveraging the BeforeConnect hook to recreate the token in time dynamically.
poolConfig.MaxConnLifetime = time.Minute * 8
// Setup connection pool config needed for AWS IAM authentication
poolConfig.BeforeConnect = func(ctx context.Context, pgConfig *pgx.ConnConfig) error {
// Manually reset auth token with aws and reset the config password using the new iam token
pwd, err := a.getDatabaseToken(ctx, poolConfig)
if err != nil {
return fmt.Errorf("failed to get database token: %w", err)
}
pgConfig.Password = pwd
poolConfig.ConnConfig.Password = pwd
return nil
}
}
// https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/UsingWithRDS.IAMDBAuth.Connecting.Go.html
func (a *StaticAuth) getDatabaseToken(ctx context.Context, poolConfig *pgxpool.Config) (string, error) {
dbEndpoint := poolConfig.ConnConfig.Host + ":" + strconv.Itoa(int(poolConfig.ConnConfig.Port))
// First, check if there are credentials set explicitly with accesskey and secretkey
var creds credentials.Value
if a.session != nil {
var err error
creds, err = a.session.Config.Credentials.Get()
if err != nil {
a.logger.Infof("failed to get access key and secret key, will fallback to reading the default AWS credentials file: %w", err)
}
}
if creds.AccessKeyID != "" && creds.SecretAccessKey != "" {
creds, err := a.session.Config.Credentials.Get()
if err != nil {
return "", fmt.Errorf("failed to retrieve session credentials: %w", err)
}
awsCfg := v2creds.NewStaticCredentialsProvider(creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken)
authenticationToken, err := auth.BuildAuthToken(
ctx, dbEndpoint, *a.region, poolConfig.ConnConfig.User, awsCfg)
if err != nil {
return "", fmt.Errorf("failed to create AWS authentication token: %w", err)
}
return authenticationToken, nil
}
// Second, check if we are assuming a role instead
if a.assumeRoleARN != nil {
awsCfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return "", fmt.Errorf("failed to load default AWS authentication configuration %w", err)
}
stsClient := sts.NewFromConfig(awsCfg)
assumeRoleCfg, err := config.LoadDefaultConfig(ctx,
config.WithRegion(*a.region),
config.WithCredentialsProvider(
awsv2.NewCredentialsCache(
stscreds.NewAssumeRoleProvider(stsClient, *a.assumeRoleARN, func(aro *stscreds.AssumeRoleOptions) {
if a.sessionName != "" {
aro.RoleSessionName = a.sessionName
}
}),
),
),
)
if err != nil {
return "", fmt.Errorf("failed to assume aws role %w", err)
}
authenticationToken, err := auth.BuildAuthToken(
ctx, dbEndpoint, *a.region, poolConfig.ConnConfig.User, assumeRoleCfg.Credentials)
if err != nil {
return "", fmt.Errorf("failed to create AWS authentication token: %w", err)
}
return authenticationToken, nil
}
// Lastly, and by default, just use the default aws configuration
awsCfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return "", fmt.Errorf("failed to load default AWS authentication configuration %w", err)
}
authenticationToken, err := auth.BuildAuthToken(ctx, dbEndpoint, *a.region, poolConfig.ConnConfig.User, awsCfg.Credentials)
if err != nil {
return "", fmt.Errorf("failed to create AWS authentication token: %w", err)
}
return authenticationToken, nil
}
func (a *StaticAuth) Kafka(opts KafkaOptions) (*KafkaClients, error) { func (a *StaticAuth) Kafka(opts KafkaOptions) (*KafkaClients, error) {
a.mu.Lock() a.mu.Lock()
defer a.mu.Unlock() defer a.mu.Unlock()

View File

@ -22,9 +22,14 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"runtime" "runtime"
"strconv"
"sync" "sync"
"time" "time"
awsv2 "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
v2creds "github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/feature/rds/auth"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/aws/arn"
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
@ -34,6 +39,12 @@ import (
"github.com/aws/rolesanywhere-credential-helper/rolesanywhere" "github.com/aws/rolesanywhere-credential-helper/rolesanywhere"
"github.com/aws/rolesanywhere-credential-helper/rolesanywhere/rolesanywhereiface" "github.com/aws/rolesanywhere-credential-helper/rolesanywhere/rolesanywhereiface"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
"github.com/aws/aws-sdk-go-v2/service/sts"
cryptopem "github.com/dapr/kit/crypto/pem" cryptopem "github.com/dapr/kit/crypto/pem"
spiffecontext "github.com/dapr/kit/crypto/spiffe/context" spiffecontext "github.com/dapr/kit/crypto/spiffe/context"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
@ -72,6 +83,7 @@ type x509 struct {
trustProfileArn *string trustProfileArn *string
trustAnchorArn *string trustAnchorArn *string
assumeRoleArn *string assumeRoleArn *string
sessionName string
} }
func newX509(ctx context.Context, opts Options, cfg *aws.Config) (*x509, error) { func newX509(ctx context.Context, opts Options, cfg *aws.Config) (*x509, error) {
@ -296,6 +308,105 @@ func (a *x509) Ses() *SesClients {
return a.clients.ses return a.clients.ses
} }
// https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/UsingWithRDS.IAMDBAuth.Connecting.Go.html
func (a *x509) getDatabaseToken(ctx context.Context, poolConfig *pgxpool.Config) (string, error) {
dbEndpoint := poolConfig.ConnConfig.Host + ":" + strconv.Itoa(int(poolConfig.ConnConfig.Port))
// First, check if there are credentials set explicitly with accesskey and secretkey
var creds credentials.Value
if a.session != nil {
var err error
creds, err = a.session.Config.Credentials.Get()
if err != nil {
a.logger.Infof("failed to get access key and secret key, will fallback to reading the default AWS credentials file: %w", err)
}
}
if creds.AccessKeyID != "" && creds.SecretAccessKey != "" {
creds, err := a.session.Config.Credentials.Get()
if err != nil {
return "", fmt.Errorf("failed to retrieve session credentials: %w", err)
}
awsCfg := v2creds.NewStaticCredentialsProvider(creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken)
authenticationToken, err := auth.BuildAuthToken(
ctx, dbEndpoint, *a.region, poolConfig.ConnConfig.User, awsCfg)
if err != nil {
return "", fmt.Errorf("failed to create AWS authentication token: %w", err)
}
return authenticationToken, nil
}
// Second, check if we are assuming a role instead
if a.assumeRoleArn != nil {
awsCfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return "", fmt.Errorf("failed to load default AWS authentication configuration %w", err)
}
stsClient := sts.NewFromConfig(awsCfg)
assumeRoleCfg, err := config.LoadDefaultConfig(ctx,
config.WithRegion(*a.region),
config.WithCredentialsProvider(
awsv2.NewCredentialsCache(
stscreds.NewAssumeRoleProvider(stsClient, *a.assumeRoleArn, func(aro *stscreds.AssumeRoleOptions) {
if a.sessionName != "" {
aro.RoleSessionName = a.sessionName
}
}),
),
),
)
if err != nil {
return "", fmt.Errorf("failed to assume aws role %w", err)
}
authenticationToken, err := auth.BuildAuthToken(
ctx, dbEndpoint, *a.region, poolConfig.ConnConfig.User, assumeRoleCfg.Credentials)
if err != nil {
return "", fmt.Errorf("failed to create AWS authentication token: %w", err)
}
return authenticationToken, nil
}
// Lastly, and by default, just use the default aws configuration
awsCfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return "", fmt.Errorf("failed to load default AWS authentication configuration %w", err)
}
authenticationToken, err := auth.BuildAuthToken(
ctx, dbEndpoint, *a.region, poolConfig.ConnConfig.User, awsCfg.Credentials)
if err != nil {
return "", fmt.Errorf("failed to create AWS authentication token: %w", err)
}
return authenticationToken, nil
}
func (a *x509) UpdatePostgres(ctx context.Context, poolConfig *pgxpool.Config) {
a.mu.Lock()
defer a.mu.Unlock()
// Set max connection lifetime to 8 minutes in postgres connection pool configuration.
// Note: this will refresh connections before the 15 min expiration on the IAM AWS auth token,
// while leveraging the BeforeConnect hook to recreate the token in time dynamically.
poolConfig.MaxConnLifetime = time.Minute * 8
// Setup connection pool config needed for AWS IAM authentication
poolConfig.BeforeConnect = func(ctx context.Context, pgConfig *pgx.ConnConfig) error {
// Manually reset auth token with aws and reset the config password using the new iam token
pwd, err := a.getDatabaseToken(ctx, poolConfig)
if err != nil {
return fmt.Errorf("failed to get database token: %w", err)
}
pgConfig.Password = pwd
poolConfig.ConnConfig.Password = pwd
return nil
}
}
func (a *x509) Kafka(opts KafkaOptions) (*KafkaClients, error) { func (a *x509) Kafka(opts KafkaOptions) (*KafkaClients, error) {
a.mu.Lock() a.mu.Lock()
defer a.mu.Unlock() defer a.mu.Unlock()

View File

@ -26,6 +26,7 @@ import (
"github.com/dapr/components-contrib/common/authentication/aws" "github.com/dapr/components-contrib/common/authentication/aws"
"github.com/dapr/components-contrib/common/authentication/azure" "github.com/dapr/components-contrib/common/authentication/azure"
"github.com/dapr/components-contrib/metadata" "github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger"
) )
// PostgresAuthMetadata contains authentication metadata for PostgreSQL components. // PostgresAuthMetadata contains authentication metadata for PostgreSQL components.
@ -86,16 +87,43 @@ func (m *PostgresAuthMetadata) InitWithMetadata(meta map[string]string, opts Ini
return nil return nil
} }
func (m *PostgresAuthMetadata) ValidateAwsIamFields() (string, string, string, error) { func (m *PostgresAuthMetadata) BuildAwsIamOptions(logger logger.Logger, properties map[string]string) (*aws.Options, error) {
awsRegion, _ := metadata.GetMetadataProperty(m.awsEnv.Metadata, "AWSRegion") awsRegion, _ := metadata.GetMetadataProperty(m.awsEnv.Metadata, "AWSRegion")
if awsRegion == "" { if awsRegion == "" {
return "", "", "", errors.New("metadata property AWSRegion is missing") return nil, errors.New("metadata property AWSRegion is missing")
} }
// Note: access key and secret keys can be optional // Note: access key and secret keys can be optional
// in the event users are leveraging the credential files for an access token. // in the event users are leveraging the credential files for an access token.
awsAccessKey, _ := metadata.GetMetadataProperty(m.awsEnv.Metadata, "AWSAccessKey") awsAccessKey, _ := metadata.GetMetadataProperty(m.awsEnv.Metadata, "AWSAccessKey")
// This is needed as we remove the awsAccessKey field to use the builtin AWS profile 'accessKey' field instead.
accessKey, _ := metadata.GetMetadataProperty(m.awsEnv.Metadata, "AccessKey")
if awsAccessKey == "" || accessKey != "" {
awsAccessKey = accessKey
}
awsSecretKey, _ := metadata.GetMetadataProperty(m.awsEnv.Metadata, "AWSSecretKey") awsSecretKey, _ := metadata.GetMetadataProperty(m.awsEnv.Metadata, "AWSSecretKey")
return awsRegion, awsAccessKey, awsSecretKey, nil // This is needed as we remove the awsSecretKey field to use the builtin AWS profile 'secretKey' field instead.
secretKey, _ := metadata.GetMetadataProperty(m.awsEnv.Metadata, "SecretKey")
if awsSecretKey == "" || secretKey != "" {
awsSecretKey = secretKey
}
sessionToken, _ := metadata.GetMetadataProperty(m.awsEnv.Metadata, "sessionToken")
assumeRoleArn, _ := metadata.GetMetadataProperty(m.awsEnv.Metadata, "assumeRoleArn")
sessionName, _ := metadata.GetMetadataProperty(m.awsEnv.Metadata, "sessionName")
if sessionName == "" {
sessionName = "DaprDefaultSession"
}
return &aws.Options{
Region: awsRegion,
AccessKey: awsAccessKey,
SecretKey: awsSecretKey,
SessionToken: sessionToken,
AssumeRoleARN: assumeRoleArn,
SessionName: sessionName,
Logger: logger,
Properties: properties,
}, nil
} }
// GetPgxPoolConfig returns the pgxpool.Config object that contains the credentials for connecting to PostgreSQL. // GetPgxPoolConfig returns the pgxpool.Config object that contains the credentials for connecting to PostgreSQL.
@ -154,27 +182,6 @@ func (m *PostgresAuthMetadata) GetPgxPoolConfig() (*pgxpool.Config, error) {
cc.Password = at.Token cc.Password = at.Token
return nil return nil
} }
case m.UseAWSIAM:
// We should use AWS IAM
awsRegion, awsAccessKey, awsSecretKey, err := m.ValidateAwsIamFields()
if err != nil {
err = fmt.Errorf("failed to validate AWS IAM authentication fields: %w", err)
return nil, err
}
awsOpts := aws.Options{
PoolConfig: config,
ConnectionString: m.ConnectionString,
Region: awsRegion,
AccessKey: awsAccessKey,
SecretKey: awsSecretKey,
}
err = awsOpts.InitiateAWSIAMAuth()
if err != nil {
err = fmt.Errorf("failed to initiate AWS IAM authentication rotation: %w", err)
return nil, err
}
} }
return config, nil return config, nil

View File

@ -39,7 +39,7 @@ type pgMetadata struct {
Timeout time.Duration `mapstructure:"timeout" mapstructurealiases:"timeoutInSeconds"` Timeout time.Duration `mapstructure:"timeout" mapstructurealiases:"timeoutInSeconds"`
CleanupInterval *time.Duration `mapstructure:"cleanupInterval" mapstructurealiases:"cleanupIntervalInSeconds"` CleanupInterval *time.Duration `mapstructure:"cleanupInterval" mapstructurealiases:"cleanupIntervalInSeconds"`
aws.AWSIAM `mapstructure:",squash"` aws.DeprecatedPostgresIAM `mapstructure:",squash"`
} }
func (m *pgMetadata) InitWithMetadata(meta state.Metadata, opts pgauth.InitWithMetadataOpts) error { func (m *pgMetadata) InitWithMetadata(meta state.Metadata, opts pgauth.InitWithMetadataOpts) error {

View File

@ -28,6 +28,7 @@ import (
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
awsAuth "github.com/dapr/components-contrib/common/authentication/aws"
pgauth "github.com/dapr/components-contrib/common/authentication/postgresql" pgauth "github.com/dapr/components-contrib/common/authentication/postgresql"
pginterfaces "github.com/dapr/components-contrib/common/component/postgresql/interfaces" pginterfaces "github.com/dapr/components-contrib/common/component/postgresql/interfaces"
pgtransactions "github.com/dapr/components-contrib/common/component/postgresql/transactions" pgtransactions "github.com/dapr/components-contrib/common/component/postgresql/transactions"
@ -54,6 +55,8 @@ type PostgreSQL struct {
etagColumn string etagColumn string
enableAzureAD bool enableAzureAD bool
enableAWSIAM bool enableAWSIAM bool
awsAuthProvider awsAuth.Provider
} }
type Options struct { type Options struct {
@ -96,16 +99,31 @@ func (p *PostgreSQL) Init(ctx context.Context, meta state.Metadata) error {
AWSIAMEnabled: p.enableAWSIAM, AWSIAMEnabled: p.enableAWSIAM,
} }
err := p.metadata.InitWithMetadata(meta, opts) if err := p.metadata.InitWithMetadata(meta, opts); err != nil {
if err != nil {
return fmt.Errorf("failed to parse metadata: %w", err) return fmt.Errorf("failed to parse metadata: %w", err)
} }
var err error
config, err := p.metadata.GetPgxPoolConfig() config, err := p.metadata.GetPgxPoolConfig()
if err != nil { if err != nil {
return err return err
} }
if opts.AWSIAMEnabled && p.metadata.UseAWSIAM {
opts, validateErr := p.metadata.BuildAwsIamOptions(p.logger, meta.Properties)
if validateErr != nil {
return fmt.Errorf("failed to validate AWS IAM authentication fields: %w", validateErr)
}
var provider awsAuth.Provider
provider, err = awsAuth.NewProvider(ctx, *opts, awsAuth.GetConfig(*opts))
if err != nil {
return err
}
p.awsAuthProvider = provider
p.awsAuthProvider.UpdatePostgres(ctx, config)
}
connCtx, connCancel := context.WithTimeout(ctx, p.metadata.Timeout) connCtx, connCancel := context.WithTimeout(ctx, p.metadata.Timeout)
p.db, err = pgxpool.NewWithConfig(connCtx, config) p.db, err = pgxpool.NewWithConfig(connCtx, config)
connCancel() connCancel()
@ -491,11 +509,15 @@ func (p *PostgreSQL) Close() error {
p.db = nil p.db = nil
} }
errs := make([]error, 2)
if p.gc != nil { if p.gc != nil {
return p.gc.Close() errs[0] = p.gc.Close()
} }
return nil if p.awsAuthProvider != nil {
errs[1] = p.awsAuthProvider.Close()
}
return errors.Join(errs...)
} }
// GetCleanupInterval returns the cleanupInterval property. // GetCleanupInterval returns the cleanupInterval property.

View File

@ -32,7 +32,7 @@ type metadata struct {
Timeout time.Duration `mapstructure:"timeout" mapstructurealiases:"timeoutInSeconds"` Timeout time.Duration `mapstructure:"timeout" mapstructurealiases:"timeoutInSeconds"`
ConfigTable string `mapstructure:"table"` ConfigTable string `mapstructure:"table"`
MaxIdleTimeoutOld time.Duration `mapstructure:"connMaxIdleTime"` // Deprecated alias for "connectionMaxIdleTime" MaxIdleTimeoutOld time.Duration `mapstructure:"connMaxIdleTime"` // Deprecated alias for "connectionMaxIdleTime"
aws.AWSIAM `mapstructure:",squash"` aws.DeprecatedPostgresIAM `mapstructure:",squash"`
} }
func (m *metadata) InitWithMetadata(meta map[string]string) error { func (m *metadata) InitWithMetadata(meta map[string]string) error {

View File

@ -46,25 +46,31 @@ builtinAuthenticationProfiles:
example: | example: |
"host=mydb.postgres.database.aws.com user=myapplication port=5432 dbname=dapr_test sslmode=require" "host=mydb.postgres.database.aws.com user=myapplication port=5432 dbname=dapr_test sslmode=require"
type: string type: string
- name: awsRegion
type: string
required: true
description: |
The AWS Region where the AWS Relational Database Service is deployed to.
example: '"us-east-1"'
- name: awsAccessKey - name: awsAccessKey
type: string type: string
required: true required: false
description: | description: |
Deprecated as of Dapr 1.17. Use 'accessKey' instead if using AWS IAM.
If both fields are set, then 'accessKey' value will be used.
AWS access key associated with an IAM account. AWS access key associated with an IAM account.
example: '"AKIAIOSFODNN7EXAMPLE"' example: '"AKIAIOSFODNN7EXAMPLE"'
- name: awsSecretKey - name: awsSecretKey
type: string type: string
required: true required: false
sensitive: true sensitive: true
description: | description: |
Deprecated as of Dapr 1.17. Use 'secretKey' instead if using AWS IAM.
If both fields are set, then 'secretKey' value will be used.
The secret key associated with the access key. The secret key associated with the access key.
example: '"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"' example: '"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"'
- name: awsRegion
type: string
required: false
description: |
This maintains backwards compatibility with existing fields.
It will be deprecated as of Dapr 1.17. Use 'region' instead.
The AWS Region where the AWS service is deployed to.
example: '"us-east-1"'
authenticationProfiles: authenticationProfiles:
- title: "Connection string" - title: "Connection string"
description: "Authenticate using a Connection String." description: "Authenticate using a Connection String."

View File

@ -31,6 +31,8 @@ import (
"github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
awsAuth "github.com/dapr/components-contrib/common/authentication/aws"
pgauth "github.com/dapr/components-contrib/common/authentication/postgresql"
"github.com/dapr/components-contrib/configuration" "github.com/dapr/components-contrib/configuration"
contribMetadata "github.com/dapr/components-contrib/metadata" contribMetadata "github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
@ -47,6 +49,10 @@ type ConfigurationStore struct {
wg sync.WaitGroup wg sync.WaitGroup
closed atomic.Bool closed atomic.Bool
lock sync.RWMutex lock sync.RWMutex
enableAzureAD bool
enableAWSIAM bool
awsAuthProvider awsAuth.Provider
} }
type subscription struct { type subscription struct {
@ -77,6 +83,10 @@ func NewPostgresConfigurationStore(logger logger.Logger) configuration.Store {
} }
func (p *ConfigurationStore) Init(ctx context.Context, metadata configuration.Metadata) error { func (p *ConfigurationStore) Init(ctx context.Context, metadata configuration.Metadata) error {
opts := pgauth.InitWithMetadataOpts{
AzureADEnabled: p.enableAzureAD,
AWSIAMEnabled: p.enableAWSIAM,
}
err := p.metadata.InitWithMetadata(metadata.Properties) err := p.metadata.InitWithMetadata(metadata.Properties)
if err != nil { if err != nil {
p.logger.Error(err) p.logger.Error(err)
@ -84,11 +94,37 @@ func (p *ConfigurationStore) Init(ctx context.Context, metadata configuration.Me
} }
p.ActiveSubscriptions = make(map[string]*subscription) p.ActiveSubscriptions = make(map[string]*subscription)
p.client, err = p.connectDB(ctx) config, err := p.metadata.GetPgxPoolConfig()
if err != nil { if err != nil {
return fmt.Errorf("error connecting to configuration store: '%w'", err) return fmt.Errorf("PostgreSQL configuration store connection error: %s", err)
} }
if opts.AWSIAMEnabled && p.metadata.UseAWSIAM {
opts, validateErr := p.metadata.BuildAwsIamOptions(p.logger, metadata.Properties)
if validateErr != nil {
return fmt.Errorf("failed to validate AWS IAM authentication fields: %w", validateErr)
}
var provider awsAuth.Provider
provider, err = awsAuth.NewProvider(ctx, *opts, awsAuth.GetConfig(*opts))
if err != nil {
return err
}
p.awsAuthProvider = provider
p.awsAuthProvider.UpdatePostgres(ctx, config)
}
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
return fmt.Errorf("PostgreSQL configuration store connection error: %w", err)
}
err = pool.Ping(ctx)
if err != nil {
return fmt.Errorf("PostgreSQL configuration store ping error: %w", err)
}
p.client = pool
err = p.client.Ping(ctx) err = p.client.Ping(ctx)
if err != nil { if err != nil {
return fmt.Errorf("unable to connect to configuration store: '%w'", err) return fmt.Errorf("unable to connect to configuration store: '%w'", err)
@ -304,25 +340,6 @@ func (p *ConfigurationStore) handleSubscribedChange(ctx context.Context, handler
} }
} }
func (p *ConfigurationStore) connectDB(ctx context.Context) (*pgxpool.Pool, error) {
config, err := p.metadata.GetPgxPoolConfig()
if err != nil {
return nil, fmt.Errorf("PostgreSQL configuration store connection error: %s", err)
}
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
return nil, fmt.Errorf("PostgreSQL configuration store connection error: %w", err)
}
err = pool.Ping(ctx)
if err != nil {
return nil, fmt.Errorf("PostgreSQL configuration store ping error: %w", err)
}
return pool, nil
}
func buildQuery(req *configuration.GetRequest, configTable string) (string, []interface{}, error) { func buildQuery(req *configuration.GetRequest, configTable string) (string, []interface{}, error) {
var query string var query string
var params []interface{} var params []interface{}
@ -436,5 +453,9 @@ func (p *ConfigurationStore) Close() error {
p.client.Close() p.client.Close()
} }
return nil errs := make([]error, 1)
if p.awsAuthProvider != nil {
errs[0] = p.awsAuthProvider.Close()
}
return errors.Join(errs...)
} }

2
go.mod
View File

@ -46,6 +46,7 @@ require (
github.com/aws/aws-sdk-go-v2/credentials v1.17.43 github.com/aws/aws-sdk-go-v2/credentials v1.17.43
github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.3.10 github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.3.10
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.17.3 github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.17.3
github.com/aws/aws-sdk-go-v2/service/sts v1.32.4
github.com/aws/rolesanywhere-credential-helper v1.0.4 github.com/aws/rolesanywhere-credential-helper v1.0.4
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874
github.com/camunda/zeebe/clients/go/v8 v8.2.12 github.com/camunda/zeebe/clients/go/v8 v8.2.12
@ -188,7 +189,6 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.4 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.24.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.32.4 // indirect
github.com/aws/smithy-go v1.22.0 // indirect github.com/aws/smithy-go v1.22.0 // indirect
github.com/awslabs/kinesis-aggregation/go v0.0.0-20210630091500-54e17340d32f // indirect github.com/awslabs/kinesis-aggregation/go v0.0.0-20210630091500-54e17340d32f // indirect
github.com/benbjohnson/clock v1.3.5 // indirect github.com/benbjohnson/clock v1.3.5 // indirect

View File

@ -29,7 +29,7 @@ builtinAuthenticationProfiles:
description: | description: |
This maintains backwards compatibility with existing fields. This maintains backwards compatibility with existing fields.
It will be deprecated as of Dapr 1.17. Use 'region' instead. It will be deprecated as of Dapr 1.17. Use 'region' instead.
The AWS Region where the AWS Relational Database Service is deployed to. The AWS Region where the AWS service is deployed to.
example: '"us-east-1"' example: '"us-east-1"'
- name: awsAccessKey - name: awsAccessKey
type: string type: string
@ -76,7 +76,7 @@ builtinAuthenticationProfiles:
If both fields are set, then 'sessionName' value will be used. If both fields are set, then 'sessionName' value will be used.
Represents the session name for assuming a role. Represents the session name for assuming a role.
example: '"MyAppSession"' example: '"MyAppSession"'
default: '"MSKSASLDefaultSession"' default: '"DaprDefaultSession"'
authenticationProfiles: authenticationProfiles:
- title: "OIDC Authentication" - title: "OIDC Authentication"
description: | description: |

View File

@ -53,16 +53,12 @@ builtinAuthenticationProfiles:
example: | example: |
"host=mydb.postgres.database.aws.com user=myapplication port=5432 dbname=dapr_test sslmode=require" "host=mydb.postgres.database.aws.com user=myapplication port=5432 dbname=dapr_test sslmode=require"
type: string type: string
- name: awsRegion
type: string
required: true
description: |
The AWS Region where the AWS Relational Database Service is deployed to.
example: '"us-east-1"'
- name: awsAccessKey - name: awsAccessKey
type: string type: string
required: true required: false
description: | description: |
Deprecated as of Dapr 1.17. Use 'accessKey' instead if using AWS IAM.
If both fields are set, then 'accessKey' value will be used.
AWS access key associated with an IAM account. AWS access key associated with an IAM account.
example: '"AKIAIOSFODNN7EXAMPLE"' example: '"AKIAIOSFODNN7EXAMPLE"'
- name: awsSecretKey - name: awsSecretKey
@ -70,8 +66,18 @@ builtinAuthenticationProfiles:
required: false required: false
sensitive: true sensitive: true
description: | description: |
Deprecated as of Dapr 1.17. Use 'secretKey' instead if using AWS IAM.
If both fields are set, then 'secretKey' value will be used.
The secret key associated with the access key. The secret key associated with the access key.
example: '"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"' example: '"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"'
- name: awsRegion
type: string
required: false
description: |
This maintains backwards compatibility with existing fields.
It will be deprecated as of Dapr 1.17. Use 'region' instead.
The AWS Region where the AWS service is deployed to.
example: '"us-east-1"'
authenticationProfiles: authenticationProfiles:
- title: "Connection string" - title: "Connection string"
description: "Authenticate using a Connection String" description: "Authenticate using a Connection String"

View File

@ -43,7 +43,7 @@ type pgMetadata struct {
Timeout time.Duration `mapstructure:"timeout" mapstructurealiases:"timeoutInSeconds"` Timeout time.Duration `mapstructure:"timeout" mapstructurealiases:"timeoutInSeconds"`
CleanupInterval *time.Duration `mapstructure:"cleanupInterval" mapstructurealiases:"cleanupIntervalInSeconds"` CleanupInterval *time.Duration `mapstructure:"cleanupInterval" mapstructurealiases:"cleanupIntervalInSeconds"`
aws.AWSIAM `mapstructure:",squash"` aws.DeprecatedPostgresIAM `mapstructure:",squash"`
} }
func (m *pgMetadata) InitWithMetadata(meta state.Metadata, opts pgauth.InitWithMetadataOpts) error { func (m *pgMetadata) InitWithMetadata(meta state.Metadata, opts pgauth.InitWithMetadataOpts) error {
@ -60,7 +60,7 @@ func (m *pgMetadata) InitWithMetadata(meta state.Metadata, opts pgauth.InitWithM
return err return err
} }
// Validate and sanitize input // Validate and sanitize inputq
err = m.PostgresAuthMetadata.InitWithMetadata(meta.Properties, opts) err = m.PostgresAuthMetadata.InitWithMetadata(meta.Properties, opts)
if err != nil { if err != nil {
return err return err

View File

@ -52,16 +52,12 @@ builtinAuthenticationProfiles:
example: | example: |
"host=mydb.postgres.database.aws.com user=myapplication port=5432 dbname=dapr_test sslmode=require" "host=mydb.postgres.database.aws.com user=myapplication port=5432 dbname=dapr_test sslmode=require"
type: string type: string
- name: awsRegion
type: string
required: true
description: |
The AWS Region where the AWS Relational Database Service is deployed to.
example: '"us-east-1"'
- name: awsAccessKey - name: awsAccessKey
type: string type: string
required: false required: false
description: | description: |
Deprecated as of Dapr 1.17. Use 'accessKey' instead if using AWS IAM.
If both fields are set, then 'accessKey' value will be used.
AWS access key associated with an IAM account. AWS access key associated with an IAM account.
example: '"AKIAIOSFODNN7EXAMPLE"' example: '"AKIAIOSFODNN7EXAMPLE"'
- name: awsSecretKey - name: awsSecretKey
@ -69,8 +65,18 @@ builtinAuthenticationProfiles:
required: false required: false
sensitive: true sensitive: true
description: | description: |
Deprecated as of Dapr 1.17. Use 'secretKey' instead if using AWS IAM.
If both fields are set, then 'secretKey' value will be used.
The secret key associated with the access key. The secret key associated with the access key.
example: '"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"' example: '"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"'
- name: awsRegion
type: string
required: false
description: |
This maintains backwards compatibility with existing fields.
It will be deprecated as of Dapr 1.17. Use 'region' instead.
The AWS Region where the AWS service is deployed to.
example: '"us-east-1"'
authenticationProfiles: authenticationProfiles:
- title: "Connection string" - title: "Connection string"
description: "Authenticate using a Connection String" description: "Authenticate using a Connection String"

View File

@ -28,6 +28,7 @@ import (
"github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
awsAuth "github.com/dapr/components-contrib/common/authentication/aws"
pgauth "github.com/dapr/components-contrib/common/authentication/postgresql" pgauth "github.com/dapr/components-contrib/common/authentication/postgresql"
pginterfaces "github.com/dapr/components-contrib/common/component/postgresql/interfaces" pginterfaces "github.com/dapr/components-contrib/common/component/postgresql/interfaces"
pgtransactions "github.com/dapr/components-contrib/common/component/postgresql/transactions" pgtransactions "github.com/dapr/components-contrib/common/component/postgresql/transactions"
@ -51,6 +52,8 @@ type PostgreSQL struct {
enableAzureAD bool enableAzureAD bool
enableAWSIAM bool enableAWSIAM bool
awsAuthProvider awsAuth.Provider
} }
type Options struct { type Options struct {
@ -98,6 +101,21 @@ func (p *PostgreSQL) Init(ctx context.Context, meta state.Metadata) (err error)
return err return err
} }
if opts.AWSIAMEnabled && p.metadata.UseAWSIAM {
opts, validateErr := p.metadata.BuildAwsIamOptions(p.logger, meta.Properties)
if validateErr != nil {
return fmt.Errorf("failed to validate AWS IAM authentication fields: %w", validateErr)
}
var provider awsAuth.Provider
provider, err = awsAuth.NewProvider(ctx, *opts, awsAuth.GetConfig(*opts))
if err != nil {
return err
}
p.awsAuthProvider = provider
p.awsAuthProvider.UpdatePostgres(ctx, config)
}
connCtx, connCancel := context.WithTimeout(ctx, p.metadata.Timeout) connCtx, connCancel := context.WithTimeout(ctx, p.metadata.Timeout)
defer connCancel() defer connCancel()
p.db, err = pgxpool.NewWithConfig(connCtx, config) p.db, err = pgxpool.NewWithConfig(connCtx, config)
@ -534,11 +552,15 @@ func (p *PostgreSQL) Close() error {
p.db = nil p.db = nil
} }
errs := make([]error, 2)
if p.gc != nil { if p.gc != nil {
return p.gc.Close() errs[0] = p.gc.Close()
} }
return nil if p.awsAuthProvider != nil {
errs[1] = p.awsAuthProvider.Close()
}
return errors.Join(errs...)
} }
// GetCleanupInterval returns the cleanupInterval property. // GetCleanupInterval returns the cleanupInterval property.