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]
}
if componentTitle == "Apache Kafka" {
if componentTitle == "Apache Kafka" || strings.ToLower(componentTitle) == "postgresql" {
removeRequiredOnSomeAWSFields(&metadataPtr)
}
@ -55,17 +55,17 @@ func ParseBuiltinAuthenticationProfile(bi BuiltinAuthenticationProfile, componen
// 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
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)
}
// 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)
}
// 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)
}
@ -125,7 +125,7 @@ func removeSomeDeprecatedFieldsOnUnrelatedAuthProfiles(metadata []Metadata) []Me
filteredMetadata := []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
} else {
filteredMetadata = append(filteredMetadata, field)

View File

@ -35,7 +35,7 @@ builtinAuthenticationProfiles:
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 Relational Database Service is deployed to.
The AWS Region where the AWS service is deployed to.
example: '"us-east-1"'
- name: awsAccessKey
type: string
@ -82,7 +82,7 @@ builtinAuthenticationProfiles:
If both fields are set, then 'sessionName' value will be used.
Represents the session name for assuming a role.
example: '"MyAppSession"'
default: '"MSKSASLDefaultSession"'
default: '"DaprDefaultSession"'
authenticationProfiles:
- title: "OIDC Authentication"
description: |

View File

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

View File

@ -56,25 +56,31 @@ builtinAuthenticationProfiles:
example: |
"host=mydb.postgres.database.aws.com user=myapplication port=5432 dbname=dapr_test sslmode=require"
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
type: string
required: true
required: false
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.
example: '"AKIAIOSFODNN7EXAMPLE"'
- name: awsSecretKey
type: string
required: true
required: false
sensitive: true
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.
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:
- title: "Connection string"
description: "Authenticate using a Connection String"

View File

@ -26,6 +26,8 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"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/kit/logger"
)
@ -45,6 +47,11 @@ type Postgres struct {
logger logger.Logger
db *pgxpool.Pool
closed atomic.Bool
enableAzureAD bool
enableAWSIAM bool
awsAuthProvider awsAuth.Provider
}
// 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() {
return errors.New("cannot initialize a previously-closed component")
}
opts := pgauth.InitWithMetadataOpts{
AzureADEnabled: p.enableAzureAD,
AWSIAMEnabled: p.enableAWSIAM,
}
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 {
return err
}
poolConfig, err := m.GetPgxPoolConfig()
if err != nil {
return err
if opts.AWSIAMEnabled && m.UseAWSIAM {
opts, validateErr := m.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, poolConfig)
}
// 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
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) {

View File

@ -15,16 +15,8 @@ package aws
import (
"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/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/dapr/kit/logger"
@ -34,16 +26,6 @@ type EnvironmentSettings struct {
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:
// accessKey and secretKey and region as noted in the docs, and Options struct above.
type DeprecatedKafkaIAM struct {
@ -55,14 +37,6 @@ type DeprecatedKafkaIAM struct {
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 {
Logger logger.Logger
Properties map[string]string
@ -75,11 +49,20 @@ type Options struct {
Region string `json:"region" mapstructure:"region" mapstructurealiases:"awsRegion"`
AccessKey string `json:"accessKey" mapstructure:"accessKey"`
SecretKey string `json:"secretKey" mapstructure:"secretKey"`
SessionName string `mapstructure:"sessionName"`
AssumeRoleARN string `mapstructure:"assumeRoleArn"`
SessionName string `json:"sessionName" mapstructure:"sessionName"`
AssumeRoleARN string `json:"assumeRoleArn" mapstructure:"assumeRoleArn"`
SessionToken string `json:"sessionToken" mapstructure:"sessionToken"`
Endpoint string
SessionToken string
Endpoint 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 {
@ -106,9 +89,14 @@ type Provider interface {
ParameterStore() *ParameterStoreClients
Kinesis() *KinesisClients
Ses() *SesClients
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
}
@ -128,69 +116,6 @@ func NewEnvironmentSettings(md map[string]string) (EnvironmentSettings, error) {
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
// This helps us to migrate away from the deprecated duplicate aws auth profile metadata fields in Dapr 1.17.
func Coalesce(values ...string) string {

View File

@ -17,15 +17,22 @@ import (
"context"
"errors"
"fmt"
"strconv"
"sync"
"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/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/credentials"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/dapr/kit/logger"
)
@ -227,6 +234,104 @@ func (a *StaticAuth) Ses() *SesClients {
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) {
a.mu.Lock()
defer a.mu.Unlock()

View File

@ -22,9 +22,14 @@ import (
"fmt"
"net/http"
"runtime"
"strconv"
"sync"
"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/arn"
"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/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"
spiffecontext "github.com/dapr/kit/crypto/spiffe/context"
"github.com/dapr/kit/logger"
@ -72,6 +83,7 @@ type x509 struct {
trustProfileArn *string
trustAnchorArn *string
assumeRoleArn *string
sessionName string
}
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
}
// 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) {
a.mu.Lock()
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/azure"
"github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger"
)
// PostgresAuthMetadata contains authentication metadata for PostgreSQL components.
@ -86,16 +87,43 @@ func (m *PostgresAuthMetadata) InitWithMetadata(meta map[string]string, opts Ini
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")
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
// in the event users are leveraging the credential files for an access token.
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")
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.
@ -154,27 +182,6 @@ func (m *PostgresAuthMetadata) GetPgxPoolConfig() (*pgxpool.Config, error) {
cc.Password = at.Token
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

View File

@ -39,7 +39,7 @@ type pgMetadata struct {
Timeout time.Duration `mapstructure:"timeout" mapstructurealiases:"timeoutInSeconds"`
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 {

View File

@ -28,6 +28,7 @@ import (
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
awsAuth "github.com/dapr/components-contrib/common/authentication/aws"
pgauth "github.com/dapr/components-contrib/common/authentication/postgresql"
pginterfaces "github.com/dapr/components-contrib/common/component/postgresql/interfaces"
pgtransactions "github.com/dapr/components-contrib/common/component/postgresql/transactions"
@ -54,6 +55,8 @@ type PostgreSQL struct {
etagColumn string
enableAzureAD bool
enableAWSIAM bool
awsAuthProvider awsAuth.Provider
}
type Options struct {
@ -96,16 +99,31 @@ func (p *PostgreSQL) Init(ctx context.Context, meta state.Metadata) error {
AWSIAMEnabled: p.enableAWSIAM,
}
err := p.metadata.InitWithMetadata(meta, opts)
if err != nil {
if err := p.metadata.InitWithMetadata(meta, opts); err != nil {
return fmt.Errorf("failed to parse metadata: %w", err)
}
var err error
config, err := p.metadata.GetPgxPoolConfig()
if err != nil {
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)
p.db, err = pgxpool.NewWithConfig(connCtx, config)
connCancel()
@ -491,11 +509,15 @@ func (p *PostgreSQL) Close() error {
p.db = nil
}
errs := make([]error, 2)
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.

View File

@ -32,7 +32,7 @@ type metadata struct {
Timeout time.Duration `mapstructure:"timeout" mapstructurealiases:"timeoutInSeconds"`
ConfigTable string `mapstructure:"table"`
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 {

View File

@ -46,25 +46,31 @@ builtinAuthenticationProfiles:
example: |
"host=mydb.postgres.database.aws.com user=myapplication port=5432 dbname=dapr_test sslmode=require"
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
type: string
required: true
required: false
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.
example: '"AKIAIOSFODNN7EXAMPLE"'
- name: awsSecretKey
type: string
required: true
required: false
sensitive: true
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.
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:
- title: "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/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"
contribMetadata "github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger"
@ -47,6 +49,10 @@ type ConfigurationStore struct {
wg sync.WaitGroup
closed atomic.Bool
lock sync.RWMutex
enableAzureAD bool
enableAWSIAM bool
awsAuthProvider awsAuth.Provider
}
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 {
opts := pgauth.InitWithMetadataOpts{
AzureADEnabled: p.enableAzureAD,
AWSIAMEnabled: p.enableAWSIAM,
}
err := p.metadata.InitWithMetadata(metadata.Properties)
if err != nil {
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.client, err = p.connectDB(ctx)
config, err := p.metadata.GetPgxPoolConfig()
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)
if err != nil {
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) {
var query string
var params []interface{}
@ -436,5 +453,9 @@ func (p *ConfigurationStore) Close() error {
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/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/sts v1.32.4
github.com/aws/rolesanywhere-credential-helper v1.0.4
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874
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/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/sts v1.32.4 // indirect
github.com/aws/smithy-go v1.22.0 // indirect
github.com/awslabs/kinesis-aggregation/go v0.0.0-20210630091500-54e17340d32f // indirect
github.com/benbjohnson/clock v1.3.5 // indirect

View File

@ -29,7 +29,7 @@ builtinAuthenticationProfiles:
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 Relational Database Service is deployed to.
The AWS Region where the AWS service is deployed to.
example: '"us-east-1"'
- name: awsAccessKey
type: string
@ -76,7 +76,7 @@ builtinAuthenticationProfiles:
If both fields are set, then 'sessionName' value will be used.
Represents the session name for assuming a role.
example: '"MyAppSession"'
default: '"MSKSASLDefaultSession"'
default: '"DaprDefaultSession"'
authenticationProfiles:
- title: "OIDC Authentication"
description: |

View File

@ -53,16 +53,12 @@ builtinAuthenticationProfiles:
example: |
"host=mydb.postgres.database.aws.com user=myapplication port=5432 dbname=dapr_test sslmode=require"
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
type: string
required: true
required: false
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.
example: '"AKIAIOSFODNN7EXAMPLE"'
- name: awsSecretKey
@ -70,8 +66,18 @@ builtinAuthenticationProfiles:
required: false
sensitive: true
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.
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:
- title: "Connection string"
description: "Authenticate using a Connection String"

View File

@ -43,7 +43,7 @@ type pgMetadata struct {
Timeout time.Duration `mapstructure:"timeout" mapstructurealiases:"timeoutInSeconds"`
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 {
@ -60,7 +60,7 @@ func (m *pgMetadata) InitWithMetadata(meta state.Metadata, opts pgauth.InitWithM
return err
}
// Validate and sanitize input
// Validate and sanitize inputq
err = m.PostgresAuthMetadata.InitWithMetadata(meta.Properties, opts)
if err != nil {
return err

View File

@ -52,16 +52,12 @@ builtinAuthenticationProfiles:
example: |
"host=mydb.postgres.database.aws.com user=myapplication port=5432 dbname=dapr_test sslmode=require"
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
type: string
required: false
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.
example: '"AKIAIOSFODNN7EXAMPLE"'
- name: awsSecretKey
@ -69,8 +65,18 @@ builtinAuthenticationProfiles:
required: false
sensitive: true
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.
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:
- title: "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/pgxpool"
awsAuth "github.com/dapr/components-contrib/common/authentication/aws"
pgauth "github.com/dapr/components-contrib/common/authentication/postgresql"
pginterfaces "github.com/dapr/components-contrib/common/component/postgresql/interfaces"
pgtransactions "github.com/dapr/components-contrib/common/component/postgresql/transactions"
@ -51,6 +52,8 @@ type PostgreSQL struct {
enableAzureAD bool
enableAWSIAM bool
awsAuthProvider awsAuth.Provider
}
type Options struct {
@ -98,6 +101,21 @@ func (p *PostgreSQL) Init(ctx context.Context, meta state.Metadata) (err error)
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)
defer connCancel()
p.db, err = pgxpool.NewWithConfig(connCtx, config)
@ -534,11 +552,15 @@ func (p *PostgreSQL) Close() error {
p.db = nil
}
errs := make([]error, 2)
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.