Skip to content

Commit 26a0c16

Browse files
authored
Add Utilities for RDS IAM Authentication (#1230)
1 parent 901bf27 commit 26a0c16

File tree

5 files changed

+181
-0
lines changed

5 files changed

+181
-0
lines changed

feature/rds/auth/connect.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package auth
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"strings"
8+
"time"
9+
10+
"github.com/aws/aws-sdk-go-v2/aws"
11+
"github.com/aws/aws-sdk-go-v2/aws/signer/v4"
12+
)
13+
14+
const (
15+
signingID = "rds-db"
16+
emptyPayloadHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
17+
)
18+
19+
// BuildAuthTokenOptions is the optional set of configuration properties for BuildAuthToken
20+
type BuildAuthTokenOptions struct{}
21+
22+
// BuildAuthToken will return an authorization token used as the password for a DB
23+
// connection.
24+
//
25+
// * endpoint - Endpoint consists of the port needed to connect to the DB. <host>:<port>
26+
// * region - Region is the location of where the DB is
27+
// * dbUser - User account within the database to sign in with
28+
// * creds - Credentials to be signed with
29+
//
30+
// The following example shows how to use BuildAuthToken to create an authentication
31+
// token for connecting to a MySQL database in RDS.
32+
//
33+
// authToken, err := BuildAuthToken(dbEndpoint, awsRegion, dbUser, awsCreds)
34+
//
35+
// // Create the MySQL DNS string for the DB connection
36+
// // user:password@protocol(endpoint)/dbname?<params>
37+
// connectStr = fmt.Sprintf("%s:%s@tcp(%s)/%s?allowCleartextPasswords=true&tls=rds",
38+
// dbUser, authToken, dbEndpoint, dbName,
39+
// )
40+
//
41+
// // Use db to perform SQL operations on database
42+
// db, err := sql.Open("mysql", connectStr)
43+
//
44+
// See http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html
45+
// for more information on using IAM database authentication with RDS.
46+
func BuildAuthToken(ctx context.Context, endpoint, region, dbUser string, creds aws.CredentialsProvider, optFns ...func(options *BuildAuthTokenOptions)) (string, error) {
47+
o := BuildAuthTokenOptions{}
48+
49+
for _, fn := range optFns {
50+
fn(&o)
51+
}
52+
53+
if creds == nil {
54+
return "", fmt.Errorf("credetials provider must not ne nil")
55+
}
56+
57+
// the scheme is arbitrary and is only needed because validation of the URL requires one.
58+
if !(strings.HasPrefix(endpoint, "http://") || strings.HasPrefix(endpoint, "https://")) {
59+
endpoint = "https://" + endpoint
60+
}
61+
62+
req, err := http.NewRequest("GET", endpoint, nil)
63+
if err != nil {
64+
return "", err
65+
}
66+
values := req.URL.Query()
67+
values.Set("Action", "connect")
68+
values.Set("DBUser", dbUser)
69+
req.URL.RawQuery = values.Encode()
70+
71+
signer := v4.NewSigner()
72+
73+
credentials, err := creds.Retrieve(ctx)
74+
if err != nil {
75+
return "", err
76+
}
77+
78+
// Expire Time: 15 minute
79+
query := req.URL.Query()
80+
query.Set("X-Amz-Expires", "900")
81+
req.URL.RawQuery = query.Encode()
82+
83+
signedURI, _, err := signer.PresignHTTP(ctx, credentials, req, emptyPayloadHash, signingID, region, time.Now().UTC())
84+
if err != nil {
85+
return "", err
86+
}
87+
88+
url := signedURI
89+
if strings.HasPrefix(url, "http://") {
90+
url = url[len("http://"):]
91+
} else if strings.HasPrefix(url, "https://") {
92+
url = url[len("https://"):]
93+
}
94+
95+
return url, nil
96+
}

feature/rds/auth/connect_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package auth_test
2+
3+
import (
4+
"context"
5+
"regexp"
6+
"testing"
7+
8+
"github.com/aws/aws-sdk-go-v2/aws"
9+
"github.com/aws/aws-sdk-go-v2/feature/rds/auth"
10+
)
11+
12+
func TestBuildAuthToken(t *testing.T) {
13+
cases := []struct {
14+
endpoint string
15+
region string
16+
user string
17+
expectedRegex string
18+
}{
19+
{
20+
"https://prod-instance.us-east-1.rds.amazonaws.com:3306",
21+
"us-west-2",
22+
"mysqlUser",
23+
`^prod-instance\.us-east-1\.rds\.amazonaws\.com:3306\?Action=connect.*?DBUser=mysqlUser.*`,
24+
},
25+
{
26+
"prod-instance.us-east-1.rds.amazonaws.com:3306",
27+
"us-west-2",
28+
"mysqlUser",
29+
`^prod-instance\.us-east-1\.rds\.amazonaws\.com:3306\?Action=connect.*?DBUser=mysqlUser.*`,
30+
},
31+
}
32+
33+
for _, c := range cases {
34+
creds := &staticCredentials{AccessKey: "AKID", SecretKey: "SECRET", Session: "SESSION"}
35+
url, err := auth.BuildAuthToken(context.Background(), c.endpoint, c.region, c.user, creds)
36+
if err != nil {
37+
t.Errorf("expect no error, got %v", err)
38+
}
39+
if re, a := regexp.MustCompile(c.expectedRegex), url; !re.MatchString(a) {
40+
t.Errorf("expect %s to match %s", re, a)
41+
}
42+
}
43+
}
44+
45+
type staticCredentials struct {
46+
AccessKey, SecretKey, Session string
47+
}
48+
49+
func (s *staticCredentials) Retrieve(ctx context.Context) (aws.Credentials, error) {
50+
return aws.Credentials{
51+
AccessKeyID: s.AccessKey,
52+
SecretAccessKey: s.SecretKey,
53+
SessionToken: s.Session,
54+
}, nil
55+
}

feature/rds/auth/doc.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Package auth is used to generate authentication tokens used to
2+
// connect to a given Amazon Relational Database Service (RDS) database.
3+
//
4+
// Before using the authentication please visit the docs here to ensure
5+
// the database has the proper policies to allow for IAM token authentication.
6+
// https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html#UsingWithRDS.IAMDBAuth.Availability
7+
// https://aws.github.io/aws-sdk-go-v2/docs/sdk-utilities/rds
8+
package auth

feature/rds/auth/go.mod

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module github.com/aws/aws-sdk-go-v2/feature/rds/auth
2+
3+
go 1.15
4+
5+
require github.com/aws/aws-sdk-go-v2 v1.3.3
6+
7+
replace github.com/aws/aws-sdk-go-v2 => ../../../

feature/rds/auth/go.sum

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
github.com/aws/aws-sdk-go-v2 v1.3.3 h1:BKKakJ9v28yopLRF6bg6FAMbVKmcXuZe+r688XMsxtk=
2+
github.com/aws/aws-sdk-go-v2 v1.3.3/go.mod h1:7OaACgj2SX3XGWnrIjGlJM22h6yD6MEWKvm7levnnM8=
3+
github.com/aws/smithy-go v1.3.1 h1:xJFO4pK0y9J8fCl34uGsSJX5KNnGbdARDlA5BPhXnwE=
4+
github.com/aws/smithy-go v1.3.1/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
5+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6+
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
7+
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
8+
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
9+
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
10+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
11+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
12+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
13+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
14+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
15+
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

0 commit comments

Comments
 (0)