Skip to content

Commit 72c92fb

Browse files
sicoyleyaron2
andauthored
feat(postgres): add iam roles anywhere auth profile (#3604)
Signed-off-by: Samantha Coyle <[email protected]> Co-authored-by: Yaron Schneider <[email protected]>
1 parent 1e095ed commit 72c92fb

File tree

20 files changed

+461
-195
lines changed

20 files changed

+461
-195
lines changed

.build-tools/pkg/metadataschema/builtin-authentication-profiles.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func ParseBuiltinAuthenticationProfile(bi BuiltinAuthenticationProfile, componen
3838
metadataPtr[j] = &profile.Metadata[j]
3939
}
4040

41-
if componentTitle == "Apache Kafka" {
41+
if componentTitle == "Apache Kafka" || strings.ToLower(componentTitle) == "postgresql" {
4242
removeRequiredOnSomeAWSFields(&metadataPtr)
4343
}
4444

@@ -55,17 +55,17 @@ func ParseBuiltinAuthenticationProfile(bi BuiltinAuthenticationProfile, componen
5555
// Note: We must apply the removal of deprecated fields after the merge!!
5656

5757
// Here, we remove some deprecated fields as we support the transition to a new auth profile
58-
if profile.Title == "AWS: Assume specific IAM Role" && componentTitle == "Apache Kafka" {
58+
if profile.Title == "AWS: Assume IAM Role" && componentTitle == "Apache Kafka" || profile.Title == "AWS: Assume IAM Role" && strings.ToLower(componentTitle) == "postgresql" {
5959
merged = removeSomeDeprecatedFieldsOnUnrelatedAuthProfiles(merged)
6060
}
6161

6262
// Here, there are no metadata fields that need deprecating
63-
if profile.Title == "AWS: Credentials from Environment Variables" && componentTitle == "Apache Kafka" {
63+
if profile.Title == "AWS: Credentials from Environment Variables" && componentTitle == "Apache Kafka" || profile.Title == "AWS: Credentials from Environment Variables" && strings.ToLower(componentTitle) == "postgresql" {
6464
merged = removeAllDeprecatedFieldsOnUnrelatedAuthProfiles(merged)
6565
}
6666

6767
// Here, this is a new auth profile, so rm all deprecating fields as unrelated.
68-
if profile.Title == "AWS: IAM Roles Anywhere" && componentTitle == "Apache Kafka" {
68+
if profile.Title == "AWS: IAM Roles Anywhere" && componentTitle == "Apache Kafka" || profile.Title == "AWS: IAM Roles Anywhere" && strings.ToLower(componentTitle) == "postgresql" {
6969
merged = removeAllDeprecatedFieldsOnUnrelatedAuthProfiles(merged)
7070
}
7171

@@ -125,7 +125,7 @@ func removeSomeDeprecatedFieldsOnUnrelatedAuthProfiles(metadata []Metadata) []Me
125125
filteredMetadata := []Metadata{}
126126

127127
for _, field := range metadata {
128-
if field.Name == "awsAccessKey" || field.Name == "awsSecretKey" || field.Name == "awsSessionToken" {
128+
if field.Name == "awsAccessKey" || field.Name == "awsSecretKey" || field.Name == "awsSessionToken" || field.Name == "awsRegion" {
129129
continue
130130
} else {
131131
filteredMetadata = append(filteredMetadata, field)

bindings/kafka/metadata.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ builtinAuthenticationProfiles:
3535
description: |
3636
This maintains backwards compatibility with existing fields.
3737
It will be deprecated as of Dapr 1.17. Use 'region' instead.
38-
The AWS Region where the AWS Relational Database Service is deployed to.
38+
The AWS Region where the AWS service is deployed to.
3939
example: '"us-east-1"'
4040
- name: awsAccessKey
4141
type: string
@@ -82,7 +82,7 @@ builtinAuthenticationProfiles:
8282
If both fields are set, then 'sessionName' value will be used.
8383
Represents the session name for assuming a role.
8484
example: '"MyAppSession"'
85-
default: '"MSKSASLDefaultSession"'
85+
default: '"DaprDefaultSession"'
8686
authenticationProfiles:
8787
- title: "OIDC Authentication"
8888
description: |

bindings/postgres/metadata.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const (
2828

2929
type psqlMetadata struct {
3030
pgauth.PostgresAuthMetadata `mapstructure:",squash"`
31-
aws.AWSIAM `mapstructure:",squash"`
31+
aws.DeprecatedPostgresIAM `mapstructure:",squash"`
3232
Timeout time.Duration `mapstructure:"timeout" mapstructurealiases:"timeoutInSeconds"`
3333
}
3434

bindings/postgres/metadata.yaml

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,25 +56,31 @@ builtinAuthenticationProfiles:
5656
example: |
5757
"host=mydb.postgres.database.aws.com user=myapplication port=5432 dbname=dapr_test sslmode=require"
5858
type: string
59-
- name: awsRegion
60-
type: string
61-
required: true
62-
description: |
63-
The AWS Region where the AWS Relational Database Service is deployed to.
64-
example: '"us-east-1"'
6559
- name: awsAccessKey
6660
type: string
67-
required: true
61+
required: false
6862
description: |
63+
Deprecated as of Dapr 1.17. Use 'accessKey' instead if using AWS IAM.
64+
If both fields are set, then 'accessKey' value will be used.
6965
AWS access key associated with an IAM account.
7066
example: '"AKIAIOSFODNN7EXAMPLE"'
7167
- name: awsSecretKey
7268
type: string
73-
required: true
69+
required: false
7470
sensitive: true
7571
description: |
72+
Deprecated as of Dapr 1.17. Use 'secretKey' instead if using AWS IAM.
73+
If both fields are set, then 'secretKey' value will be used.
7674
The secret key associated with the access key.
7775
example: '"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"'
76+
- name: awsRegion
77+
type: string
78+
required: false
79+
description: |
80+
This maintains backwards compatibility with existing fields.
81+
It will be deprecated as of Dapr 1.17. Use 'region' instead.
82+
The AWS Region where the AWS service is deployed to.
83+
example: '"us-east-1"'
7884
authenticationProfiles:
7985
- title: "Connection string"
8086
description: "Authenticate using a Connection String"

bindings/postgres/postgres.go

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import (
2626
"github.com/jackc/pgx/v5/pgxpool"
2727

2828
"github.com/dapr/components-contrib/bindings"
29+
awsAuth "github.com/dapr/components-contrib/common/authentication/aws"
30+
pgauth "github.com/dapr/components-contrib/common/authentication/postgresql"
2931
"github.com/dapr/components-contrib/metadata"
3032
"github.com/dapr/kit/logger"
3133
)
@@ -45,6 +47,11 @@ type Postgres struct {
4547
logger logger.Logger
4648
db *pgxpool.Pool
4749
closed atomic.Bool
50+
51+
enableAzureAD bool
52+
enableAWSIAM bool
53+
54+
awsAuthProvider awsAuth.Provider
4855
}
4956

5057
// NewPostgres returns a new PostgreSQL output binding.
@@ -59,18 +66,36 @@ func (p *Postgres) Init(ctx context.Context, meta bindings.Metadata) error {
5966
if p.closed.Load() {
6067
return errors.New("cannot initialize a previously-closed component")
6168
}
62-
69+
opts := pgauth.InitWithMetadataOpts{
70+
AzureADEnabled: p.enableAzureAD,
71+
AWSIAMEnabled: p.enableAWSIAM,
72+
}
6373
m := psqlMetadata{}
64-
err := m.InitWithMetadata(meta.Properties)
65-
if err != nil {
74+
if err := m.InitWithMetadata(meta.Properties); err != nil {
6675
return err
6776
}
6877

78+
var err error
6979
poolConfig, err := m.GetPgxPoolConfig()
7080
if err != nil {
7181
return err
7282
}
7383

84+
if opts.AWSIAMEnabled && m.UseAWSIAM {
85+
opts, validateErr := m.BuildAwsIamOptions(p.logger, meta.Properties)
86+
if validateErr != nil {
87+
return fmt.Errorf("failed to validate AWS IAM authentication fields: %w", validateErr)
88+
}
89+
90+
var provider awsAuth.Provider
91+
provider, err = awsAuth.NewProvider(ctx, *opts, awsAuth.GetConfig(*opts))
92+
if err != nil {
93+
return err
94+
}
95+
p.awsAuthProvider = provider
96+
p.awsAuthProvider.UpdatePostgres(ctx, poolConfig)
97+
}
98+
7499
// This context doesn't control the lifetime of the connection pool, and is
75100
// only scoped to postgres creating resources at init.
76101
connCtx, connCancel := context.WithTimeout(ctx, m.Timeout)
@@ -186,7 +211,11 @@ func (p *Postgres) Close() error {
186211
}
187212
p.db = nil
188213

189-
return nil
214+
errs := make([]error, 1)
215+
if p.awsAuthProvider != nil {
216+
errs[0] = p.awsAuthProvider.Close()
217+
}
218+
return errors.Join(errs...)
190219
}
191220

192221
func (p *Postgres) query(ctx context.Context, sql string, args ...any) (result []byte, err error) {

common/authentication/aws/aws.go

Lines changed: 20 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,8 @@ package aws
1515

1616
import (
1717
"context"
18-
"errors"
19-
"fmt"
20-
"strconv"
21-
"time"
22-
23-
"github.com/aws/aws-sdk-go-v2/config"
24-
v2creds "github.com/aws/aws-sdk-go-v2/credentials"
25-
"github.com/aws/aws-sdk-go-v2/feature/rds/auth"
18+
2619
"github.com/aws/aws-sdk-go/aws"
27-
"github.com/jackc/pgx/v5"
2820
"github.com/jackc/pgx/v5/pgxpool"
2921

3022
"github.com/dapr/kit/logger"
@@ -34,16 +26,6 @@ type EnvironmentSettings struct {
3426
Metadata map[string]string
3527
}
3628

37-
type AWSIAM struct {
38-
// Ignored by metadata parser because included in built-in authentication profile
39-
// Access key to use for accessing PostgreSQL.
40-
AWSAccessKey string `json:"awsAccessKey" mapstructure:"awsAccessKey"`
41-
// Secret key to use for accessing PostgreSQL.
42-
AWSSecretKey string `json:"awsSecretKey" mapstructure:"awsSecretKey"`
43-
// AWS region in which PostgreSQL is deployed.
44-
AWSRegion string `json:"awsRegion" mapstructure:"awsRegion"`
45-
}
46-
4729
// TODO: Delete in Dapr 1.17 so we can move all IAM fields to use the defaults of:
4830
// accessKey and secretKey and region as noted in the docs, and Options struct above.
4931
type DeprecatedKafkaIAM struct {
@@ -55,14 +37,6 @@ type DeprecatedKafkaIAM struct {
5537
StsSessionName string `json:"awsStsSessionName" mapstructure:"awsStsSessionName"`
5638
}
5739

58-
type AWSIAMAuthOptions struct {
59-
PoolConfig *pgxpool.Config `json:"poolConfig" mapstructure:"poolConfig"`
60-
ConnectionString string `json:"connectionString" mapstructure:"connectionString"`
61-
Region string `json:"region" mapstructure:"region"`
62-
AccessKey string `json:"accessKey" mapstructure:"accessKey"`
63-
SecretKey string `json:"secretKey" mapstructure:"secretKey"`
64-
}
65-
6640
type Options struct {
6741
Logger logger.Logger
6842
Properties map[string]string
@@ -75,11 +49,20 @@ type Options struct {
7549
Region string `json:"region" mapstructure:"region" mapstructurealiases:"awsRegion"`
7650
AccessKey string `json:"accessKey" mapstructure:"accessKey"`
7751
SecretKey string `json:"secretKey" mapstructure:"secretKey"`
78-
SessionName string `mapstructure:"sessionName"`
79-
AssumeRoleARN string `mapstructure:"assumeRoleArn"`
52+
SessionName string `json:"sessionName" mapstructure:"sessionName"`
53+
AssumeRoleARN string `json:"assumeRoleArn" mapstructure:"assumeRoleArn"`
54+
SessionToken string `json:"sessionToken" mapstructure:"sessionToken"`
8055

81-
Endpoint string
82-
SessionToken string
56+
Endpoint string
57+
}
58+
59+
// TODO: Delete in Dapr 1.17 so we can move all IAM fields to use the defaults of:
60+
// accessKey and secretKey and region as noted in the docs, and Options struct above.
61+
type DeprecatedPostgresIAM struct {
62+
// Access key to use for accessing PostgreSQL.
63+
AccessKey string `json:"awsAccessKey" mapstructure:"awsAccessKey"`
64+
// Secret key to use for accessing PostgreSQL.
65+
SecretKey string `json:"awsSecretKey" mapstructure:"awsSecretKey"`
8366
}
8467

8568
func GetConfig(opts Options) *aws.Config {
@@ -106,9 +89,14 @@ type Provider interface {
10689
ParameterStore() *ParameterStoreClients
10790
Kinesis() *KinesisClients
10891
Ses() *SesClients
109-
11092
Kafka(KafkaOptions) (*KafkaClients, error)
11193

94+
// Postgres is an outlier to the others in the sense that we can update only it's config,
95+
// as we use a max connection time of 8 minutes.
96+
// This means that we can just update the config session credentials,
97+
// and then in 8 minutes it will update to a new session automatically for us.
98+
UpdatePostgres(context.Context, *pgxpool.Config)
99+
112100
Close() error
113101
}
114102

@@ -128,69 +116,6 @@ func NewEnvironmentSettings(md map[string]string) (EnvironmentSettings, error) {
128116
return es, nil
129117
}
130118

131-
func (opts *Options) GetAccessToken(ctx context.Context) (string, error) {
132-
dbEndpoint := opts.PoolConfig.ConnConfig.Host + ":" + strconv.Itoa(int(opts.PoolConfig.ConnConfig.Port))
133-
var authenticationToken string
134-
135-
// https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/UsingWithRDS.IAMDBAuth.Connecting.Go.html
136-
// Default to load default config through aws credentials file (~/.aws/credentials)
137-
awsCfg, err := config.LoadDefaultConfig(ctx)
138-
// Note: in the event of an error with invalid config or failed to load config,
139-
// then we fall back to using the access key and secret key.
140-
switch {
141-
case errors.Is(err, config.SharedConfigAssumeRoleError{}.Err),
142-
errors.Is(err, config.SharedConfigLoadError{}.Err),
143-
errors.Is(err, config.SharedConfigProfileNotExistError{}.Err):
144-
// Validate if access key and secret access key are provided
145-
if opts.AccessKey == "" || opts.SecretKey == "" {
146-
return "", fmt.Errorf("failed to load default configuration for AWS using accessKey and secretKey: %w", err)
147-
}
148-
149-
// Set credentials explicitly
150-
awsCfg := v2creds.NewStaticCredentialsProvider(opts.AccessKey, opts.SecretKey, "")
151-
authenticationToken, err = auth.BuildAuthToken(
152-
ctx, dbEndpoint, opts.Region, opts.PoolConfig.ConnConfig.User, awsCfg)
153-
if err != nil {
154-
return "", fmt.Errorf("failed to create AWS authentication token: %w", err)
155-
}
156-
157-
return authenticationToken, nil
158-
case err != nil:
159-
return "", errors.New("failed to load default AWS authentication configuration")
160-
}
161-
162-
authenticationToken, err = auth.BuildAuthToken(
163-
ctx, dbEndpoint, opts.Region, opts.PoolConfig.ConnConfig.User, awsCfg.Credentials)
164-
if err != nil {
165-
return "", fmt.Errorf("failed to create AWS authentication token: %w", err)
166-
}
167-
168-
return authenticationToken, nil
169-
}
170-
171-
func (opts *Options) InitiateAWSIAMAuth() error {
172-
// Set max connection lifetime to 8 minutes in postgres connection pool configuration.
173-
// Note: this will refresh connections before the 15 min expiration on the IAM AWS auth token,
174-
// while leveraging the BeforeConnect hook to recreate the token in time dynamically.
175-
opts.PoolConfig.MaxConnLifetime = time.Minute * 8
176-
177-
// Setup connection pool config needed for AWS IAM authentication
178-
opts.PoolConfig.BeforeConnect = func(ctx context.Context, pgConfig *pgx.ConnConfig) error {
179-
// Manually reset auth token with aws and reset the config password using the new iam token
180-
pwd, errGetAccessToken := opts.GetAccessToken(ctx)
181-
if errGetAccessToken != nil {
182-
return fmt.Errorf("failed to refresh access token for iam authentication with PostgreSQL: %w", errGetAccessToken)
183-
}
184-
185-
pgConfig.Password = pwd
186-
opts.PoolConfig.ConnConfig.Password = pwd
187-
188-
return nil
189-
}
190-
191-
return nil
192-
}
193-
194119
// Coalesce is a helper function to return the first non-empty string from the inputs
195120
// This helps us to migrate away from the deprecated duplicate aws auth profile metadata fields in Dapr 1.17.
196121
func Coalesce(values ...string) string {

0 commit comments

Comments
 (0)