Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
8a8b780
Add DynamoDB database access
GavinFrazar Dec 14, 2022
22e1d4b
add clarifying comment and refactor uri validation into helper func
GavinFrazar Dec 15, 2022
fd45d0e
update tests
GavinFrazar Dec 16, 2022
882fc3d
undo temp change to test code
GavinFrazar Dec 16, 2022
a80885f
move proxy getter back
GavinFrazar Dec 16, 2022
5612912
fixup changes
GavinFrazar Dec 16, 2022
bb93a20
Update lib/srv/db/dynamodb/engine.go
GavinFrazar Dec 16, 2022
5186ebe
Update lib/srv/db/dynamodb/test.go
GavinFrazar Dec 16, 2022
1c611c3
Update lib/srv/db/dynamodb/engine.go
GavinFrazar Dec 16, 2022
605f81b
address feedback
GavinFrazar Dec 16, 2022
b45a3d5
remove dependency on aws go sdk in api
GavinFrazar Dec 16, 2022
08a1d59
update tests
GavinFrazar Dec 16, 2022
06744f4
add test for region -> endpoint suffix
GavinFrazar Dec 17, 2022
07cfa9c
fix test
GavinFrazar Dec 17, 2022
218f176
fix test
GavinFrazar Dec 17, 2022
fe040f8
Update lib/srv/db/dynamodb/engine.go
GavinFrazar Dec 20, 2022
0e08902
Update lib/srv/db/dynamodb/engine.go
GavinFrazar Dec 20, 2022
c90b102
Update api/utils/aws/endpoint.go
GavinFrazar Dec 20, 2022
04229e8
Update tool/tsh/db.go
GavinFrazar Dec 20, 2022
d1bed50
ioutil -> io
GavinFrazar Dec 20, 2022
1458595
service -> serviceID
GavinFrazar Dec 20, 2022
959f8f1
Rename dynamodb config check func
GavinFrazar Dec 20, 2022
3e5af7a
Add reference link to aws error message json format
GavinFrazar Dec 20, 2022
ad92287
Remove superfluous content-length header removal
GavinFrazar Dec 20, 2022
fde3ffb
refactor dynamodb config checking to parse the endpoint
GavinFrazar Dec 21, 2022
c226860
Update and add tests for dynamodb endpoints and database creation
GavinFrazar Dec 21, 2022
c4d5cea
Add fuzz test for parsing dynamodb endpoint
GavinFrazar Dec 21, 2022
abf6d8e
Emit audit event with an error code when request fails to roundtrip
GavinFrazar Dec 21, 2022
4408735
Refactor dynamodb engine
GavinFrazar Dec 21, 2022
4f29e12
Update protos to include DB AWS ExternalID
GavinFrazar Dec 21, 2022
02d8bf4
Add DB AWS ExternalID to config
GavinFrazar Dec 21, 2022
bca6e2c
Fix comment wording
GavinFrazar Dec 21, 2022
da9ae5c
Pass the AWS externalID for signing in dynamodb engine
GavinFrazar Dec 21, 2022
c916bfd
Fix event code suffix for errors
GavinFrazar Dec 22, 2022
709f106
Don't require dynamodb --db-user for tsh db login
GavinFrazar Dec 22, 2022
039ea82
don't require elasticsearch tunnel
GavinFrazar Dec 22, 2022
6383bfb
display better command alternatives for unsupported tsh db subcommand…
GavinFrazar Dec 22, 2022
e218055
Always emit audit log for each request regardless of errors
GavinFrazar Dec 23, 2022
950b0d7
Add dynamodb to test plan
GavinFrazar Dec 23, 2022
3248ad3
Fix sign request args
GavinFrazar Dec 23, 2022
45dca3b
Remove back quotes
GavinFrazar Dec 28, 2022
dc2934d
Remove status code conversion
GavinFrazar Dec 28, 2022
6799f6b
Fix spelling
GavinFrazar Dec 28, 2022
91684f4
Remove unused field in test server
GavinFrazar Dec 28, 2022
ab74be5
Merge branch 'master' into gavinfrazar/add-dynamodb-database-access
GavinFrazar Dec 28, 2022
78e56a2
Merge branch 'master' into gavinfrazar/add-dynamodb-database-access
GavinFrazar Dec 28, 2022
3b51733
Refactor request body copying/closing
GavinFrazar Dec 29, 2022
56bf0fb
Merge branch 'master' into gavinfrazar/add-dynamodb-database-access
GavinFrazar Dec 29, 2022
3080e50
Merge branch 'master' into gavinfrazar/add-dynamodb-database-access
GavinFrazar Dec 29, 2022
ab82fc6
Merge branch 'master' into gavinfrazar/add-dynamodb-database-access
GavinFrazar Dec 30, 2022
2cc75ea
Merge branch 'master' into gavinfrazar/add-dynamodb-database-access
GavinFrazar Dec 30, 2022
4a5a472
remove unused local var
GavinFrazar Dec 30, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/ISSUE_TEMPLATE/testplan.md
Original file line number Diff line number Diff line change
Expand Up @@ -1141,6 +1141,7 @@ tsh bench sessions --max=5000 --web user ls
- [ ] Azure Cache for Redis.
- [ ] Elasticsearch.
- [ ] Cassandra/ScyllaDB.
- [ ] Dynamodb.
- [ ] Connect to a database within a remote cluster via a trusted cluster.
- [ ] Self-hosted Postgres.
- [ ] Self-hosted MySQL.
Expand All @@ -1161,6 +1162,7 @@ tsh bench sessions --max=5000 --web user ls
- [ ] Azure Cache for Redis.
- [ ] Elasticsearch.
- [ ] Cassandra/ScyllaDB.
- [ ] Dynamodb.
- [ ] Verify audit events.
- [ ] `db.session.start` is emitted when you connect.
- [ ] `db.session.end` is emitted when you disconnect.
Expand Down
2 changes: 2 additions & 0 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,8 @@ message AWS {
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "redshift_serverless,omitempty"
];
// ExternalID is an optional AWS external ID used to enable assuming an AWS role across accounts.
string ExternalID = 10 [(gogoproto.jsontag) = "external_id,omitempty"];
}

// SecretStore contains secret store configurations.
Expand Down
69 changes: 63 additions & 6 deletions api/types/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,10 @@ func (d *DatabaseV3) IsAWSKeyspaces() bool {
return d.GetType() == DatabaseTypeAWSKeyspaces
}

func (d *DatabaseV3) IsDynamoDB() bool {
return d.GetType() == DatabaseTypeDynamoDB
}

// IsAWSHosted returns true if database is hosted by AWS.
func (d *DatabaseV3) IsAWSHosted() bool {
_, ok := d.getAWSType()
Expand All @@ -405,8 +409,13 @@ func (d *DatabaseV3) IsCloudHosted() bool {
// getAWSType returns the database type.
func (d *DatabaseV3) getAWSType() (string, bool) {
aws := d.GetAWS()
if aws.AccountID != "" && d.Spec.Protocol == DatabaseTypeCassandra {
return DatabaseTypeAWSKeyspaces, true
switch d.Spec.Protocol {
case DatabaseTypeCassandra:
if aws.AccountID != "" {
return DatabaseTypeAWSKeyspaces, true
}
case DatabaseTypeDynamoDB:
return DatabaseTypeDynamoDB, true
}
if aws.Redshift.ClusterID != "" {
return DatabaseTypeRedshift, true
Expand Down Expand Up @@ -491,6 +500,10 @@ func (d *DatabaseV3) CheckAndSetDefaults() error {
if d.Spec.Protocol == "" {
return trace.BadParameter("database %q protocol is empty", d.GetName())
}
if d.IsDynamoDB() {
// DynamoDB gets its own checking logic for its unusual config.
return trace.Wrap(d.handleDynamoDBConfig())
}
if d.Spec.URI == "" {
switch {
case d.IsAWSKeyspaces() && d.GetAWS().Region != "":
Expand Down Expand Up @@ -604,11 +617,16 @@ func (d *DatabaseV3) CheckAndSetDefaults() error {
return trace.BadParameter("database %q AWS account ID is empty", d.GetName())
}
if d.Spec.AWS.Region == "" {
region, err := awsutils.CassandraEndpointRegion(d.Spec.URI)
if err != nil {
return trace.Wrap(err)
switch {
case d.IsAWSKeyspaces():
region, err := awsutils.CassandraEndpointRegion(d.Spec.URI)
if err != nil {
return trace.Wrap(err)
}
d.Spec.AWS.Region = region
default:
return trace.BadParameter("database %q AWS region is empty", d.GetName())
}
d.Spec.AWS.Region = region
}
case azureutils.IsCacheForRedisEndpoint(d.Spec.URI):
// ResourceID is required for fetching Redis tokens.
Expand Down Expand Up @@ -651,6 +669,43 @@ func (d *DatabaseV3) CheckAndSetDefaults() error {
return nil
}

// handleDynamoDBConfig handles DynamoDB configuration checking.
func (d *DatabaseV3) handleDynamoDBConfig() error {
if d.Spec.AWS.AccountID == "" {
return trace.BadParameter("database %q AWS account ID is empty", d.GetName())
}

info, err := awsutils.ParseDynamoDBEndpoint(d.Spec.URI)
switch {
case err != nil:
// when region parsing returns an error but the region is set, it's ok because we can just construct the URI using the region,
// so we check if the region is configured to see if this is really a configuration error.
if d.Spec.AWS.Region == "" {
// the AWS region is empty and we can't derive it from the URI, so this is a config error.
return trace.BadParameter("database %q AWS region is empty and cannot be derived from the URI %q",
d.GetName(), d.Spec.URI)
}
if awsutils.IsAWSEndpoint(d.Spec.URI) {
// The user configured an AWS URI that which doesn't look like a DynamoDB endpoint.
// The URI must look like <service>.<region>.<partition> or <region>.<partition>
return trace.Wrap(err)
}
Comment thread
GavinFrazar marked this conversation as resolved.
Outdated
case d.Spec.AWS.Region == "":
// if the AWS region is empty we can just use the region extracted from the URI.
d.Spec.AWS.Region = info.Region
case d.Spec.AWS.Region != info.Region:
// if the AWS region is not empty but doesn't match the URI, this may indicate a user configuration mistake.
return trace.BadParameter("database %q AWS region %q does not match the configured URI region %q, "+
" omit the URI and it will be derived automatically for the configured AWS region",
d.GetName(), d.Spec.AWS.Region, info.Region)
}

if d.Spec.URI == "" {
d.Spec.URI = awsutils.DynamoDBURIForRegion(d.Spec.AWS.Region)
}
return nil
}

// GetSecretStore returns secret store configurations.
func (d *DatabaseV3) GetSecretStore() SecretStore {
return d.Spec.AWS.SecretStore
Expand Down Expand Up @@ -689,6 +744,8 @@ const (
DatabaseTypeAWSKeyspaces = "keyspace"
// DatabaseTypeCassandra is AWS-hosted Keyspace database.
DatabaseTypeCassandra = "cassandra"
// DatabaseTypeDynamoDB is a DynamoDB database.
DatabaseTypeDynamoDB = "dynamodb"
)

// GetServerName returns the GCP database project and instance as "<project-id>:<instance-id>".
Expand Down
144 changes: 144 additions & 0 deletions api/types/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package types
import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -538,3 +540,145 @@ func TestDatabaseSelfHosted(t *testing.T) {
})
}
}

func TestDynamoDBConfig(t *testing.T) {
t.Parallel()

tests := []struct {
desc string
uri string
region string
account string
wantSpec DatabaseSpecV3
wantErrMsg string
}{
{
desc: "account and region and empty URI is correct",
region: "us-west-1",
account: "12345",
wantSpec: DatabaseSpecV3{
URI: "aws://dynamodb.us-west-1.amazonaws.com",
AWS: AWS{
Region: "us-west-1",
AccountID: "12345",
},
},
},
{
desc: "account and AWS URI and empty region is correct",
uri: "dynamodb.us-west-1.amazonaws.com",
account: "12345",
wantSpec: DatabaseSpecV3{
URI: "dynamodb.us-west-1.amazonaws.com",
AWS: AWS{
Region: "us-west-1",
AccountID: "12345",
},
},
},
{
desc: "account and AWS streams dynamodb URI and empty region is correct",
uri: "streams.dynamodb.us-west-1.amazonaws.com",
account: "12345",
wantSpec: DatabaseSpecV3{
URI: "streams.dynamodb.us-west-1.amazonaws.com",
AWS: AWS{
Region: "us-west-1",
AccountID: "12345",
},
},
},
{
desc: "account and AWS dax URI and empty region is correct",
uri: "dax.us-west-1.amazonaws.com",
account: "12345",
wantSpec: DatabaseSpecV3{
URI: "dax.us-west-1.amazonaws.com",
AWS: AWS{
Region: "us-west-1",
AccountID: "12345",
},
},
},
{
desc: "account and region and matching AWS URI region is correct",
uri: "dynamodb.us-west-1.amazonaws.com",
region: "us-west-1",
account: "12345",
wantSpec: DatabaseSpecV3{
URI: "dynamodb.us-west-1.amazonaws.com",
AWS: AWS{
Region: "us-west-1",
AccountID: "12345",
},
},
},
{
desc: "account and region and custom URI is correct",
uri: "localhost:8080",
region: "us-west-1",
account: "12345",
wantSpec: DatabaseSpecV3{
URI: "localhost:8080",
AWS: AWS{
Region: "us-west-1",
AccountID: "12345",
},
},
},
{
desc: "region and different AWS URI region is an error",
uri: "dynamodb.us-west-2.amazonaws.com",
region: "us-west-1",
account: "12345",
wantErrMsg: "does not match the configured URI",
},
{
desc: "invalid AWS URI is an error",
uri: "a.streams.dynamodb.us-west-1.amazonaws.com",
region: "us-west-1",
account: "12345",
wantErrMsg: "invalid DynamoDB endpoint",
},
{
desc: "custom URI and empty region is an error",
uri: "localhost:8080",
account: "12345",
wantErrMsg: "region is empty",
},
{
desc: "empty URI and empty region is an error",
account: "12345",
wantErrMsg: "region is empty",
},
{
desc: "missing account id",
wantErrMsg: "account ID is empty",
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.desc, func(t *testing.T) {
t.Parallel()
database, err := NewDatabaseV3(Metadata{
Name: "test",
}, DatabaseSpecV3{
Protocol: "dynamodb",
URI: tt.uri,
AWS: AWS{
Region: tt.region,
AccountID: tt.account,
},
})
if tt.wantErrMsg != "" {
require.Error(t, err)
require.ErrorContains(t, err, tt.wantErrMsg)
return
}
require.NoError(t, err)
diff := cmp.Diff(tt.wantSpec, database.Spec, cmpopts.IgnoreFields(DatabaseSpecV3{}, "Protocol"))
require.Empty(t, diff)
})
}
}
Loading