Decouple database server from database (#7771)

This commit is contained in:
Roman Tkachenko
2021-08-05 01:50:21 -07:00
committed by GitHub
parent 6b8a57f114
commit 629042ed30
40 changed files with 3060 additions and 1693 deletions
+3
View File
@@ -115,6 +115,9 @@ const (
// KindDatabaseServer is a database proxy server resource.
KindDatabaseServer = "db_server"
// KindDatabase is a database resource.
KindDatabase = "db"
// KindToken is a provisioning token resource
KindToken = "token"
+355
View File
@@ -0,0 +1,355 @@
/*
Copyright 2021 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package types
import (
"fmt"
"net"
"strings"
"time"
"github.com/gravitational/trace"
)
// Database represents a database proxied by a database server.
type Database interface {
// Resource provides common resource methods.
Resource
// GetNamespace returns the database namespace.
GetNamespace() string
// GetStaticLabels returns the database static labels.
GetStaticLabels() map[string]string
// SetStaticLabels sets the database static labels.
SetStaticLabels(map[string]string)
// GetDynamicLabels returns the database dynamic labels.
GetDynamicLabels() map[string]CommandLabel
// SetDynamicLabels sets the database dynamic labels.
SetDynamicLabels(map[string]CommandLabel)
// GetAllLabels returns combined static and dynamic labels.
GetAllLabels() map[string]string
// LabelsString returns all labels as a string.
LabelsString() string
// String returns string representation of the database.
String() string
// GetDescription returns the database description.
GetDescription() string
// GetProtocol returns the database protocol.
GetProtocol() string
// GetURI returns the database connection endpoint.
GetURI() string
// GetCA returns the database CA certificate.
GetCA() string
// SetCA sets the database CA certificate.
SetCA(string)
// GetAWS returns AWS information for RDS/Aurora databases.
GetAWS() AWS
// GetGCP returns GCP information for Cloud SQL databases.
GetGCP() GCPCloudSQL
// GetType returns the database authentication type: self-hosted, RDS, Redshift or Cloud SQL.
GetType() string
// IsRDS returns true if this is an RDS/Aurora database.
IsRDS() bool
// IsRedshift returns true if this is a Redshift database.
IsRedshift() bool
// IsCloudSQL returns true if this is a Cloud SQL database.
IsCloudSQL() bool
}
// NewDatabaseV3 creates a new database resource.
func NewDatabaseV3(meta Metadata, spec DatabaseSpecV3) (*DatabaseV3, error) {
database := &DatabaseV3{
Metadata: meta,
Spec: spec,
}
if err := database.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
return database, nil
}
// GetVersion returns the database resource version.
func (d *DatabaseV3) GetVersion() string {
return d.Version
}
// GetKind returns the database resource kind.
func (d *DatabaseV3) GetKind() string {
return d.Kind
}
// GetSubKind returns the database resource subkind.
func (d *DatabaseV3) GetSubKind() string {
return d.SubKind
}
// SetSubKind sets the database resource subkind.
func (d *DatabaseV3) SetSubKind(sk string) {
d.SubKind = sk
}
// GetResourceID returns the database resource ID.
func (d *DatabaseV3) GetResourceID() int64 {
return d.Metadata.ID
}
// SetResourceID sets the database resource ID.
func (d *DatabaseV3) SetResourceID(id int64) {
d.Metadata.ID = id
}
// GetMetadata returns the database resource metadata.
func (d *DatabaseV3) GetMetadata() Metadata {
return d.Metadata
}
// GetNamespace returns the database resource namespace.
func (d *DatabaseV3) GetNamespace() string {
return d.Metadata.Namespace
}
// SetExpiry sets the database resource expiration time.
func (d *DatabaseV3) SetExpiry(expiry time.Time) {
d.Metadata.SetExpiry(expiry)
}
// Expiry returns the database resource expiration time.
func (d *DatabaseV3) Expiry() time.Time {
return d.Metadata.Expiry()
}
// GetName returns the database resource name.
func (d *DatabaseV3) GetName() string {
return d.Metadata.Name
}
// SetName sets the database resource name.
func (d *DatabaseV3) SetName(name string) {
d.Metadata.Name = name
}
// GetStaticLabels returns the database static labels.
func (d *DatabaseV3) GetStaticLabels() map[string]string {
return d.Metadata.Labels
}
// SetStaticLabels sets the database static labels.
func (d *DatabaseV3) SetStaticLabels(sl map[string]string) {
d.Metadata.Labels = sl
}
// GetDynamicLabels returns the database dynamic labels.
func (d *DatabaseV3) GetDynamicLabels() map[string]CommandLabel {
if d.Spec.DynamicLabels == nil {
return nil
}
return V2ToLabels(d.Spec.DynamicLabels)
}
// SetDynamicLabels sets the database dynamic labels
func (d *DatabaseV3) SetDynamicLabels(dl map[string]CommandLabel) {
d.Spec.DynamicLabels = LabelsToV2(dl)
}
// GetAllLabels returns the database combined static and dynamic labels.
func (d *DatabaseV3) GetAllLabels() map[string]string {
return CombineLabels(d.Metadata.Labels, d.Spec.DynamicLabels)
}
// LabelsString returns all database labels as a string.
func (d *DatabaseV3) LabelsString() string {
return LabelsAsString(d.Metadata.Labels, d.Spec.DynamicLabels)
}
// GetDescription returns the database description.
func (d *DatabaseV3) GetDescription() string {
return d.Metadata.Description
}
// GetProtocol returns the database protocol.
func (d *DatabaseV3) GetProtocol() string {
return d.Spec.Protocol
}
// GetURI returns the database connection address.
func (d *DatabaseV3) GetURI() string {
return d.Spec.URI
}
// GetCA returns the database CA certificate.
func (d *DatabaseV3) GetCA() string {
return d.Spec.CACert
}
// SetCA sets the database CA certificate.
func (d *DatabaseV3) SetCA(bytes string) {
d.Spec.CACert = bytes
}
// GetAWS returns AWS information for RDS/Aurora/Redshift databases.
func (d *DatabaseV3) GetAWS() AWS {
return d.Spec.AWS
}
// GetGCP returns GCP information for Cloud SQL databases.
func (d *DatabaseV3) GetGCP() GCPCloudSQL {
return d.Spec.GCP
}
// IsRDS returns true if this is a AWS RDS/Aurora instance.
func (d *DatabaseV3) IsRDS() bool {
return d.GetType() == DatabaseTypeRDS
}
// IsRedshift returns true if this is a Redshift database instance.
func (d *DatabaseV3) IsRedshift() bool {
return d.GetType() == DatabaseTypeRedshift
}
// IsCloudSQL returns true if this database is a Cloud SQL instance.
func (d *DatabaseV3) IsCloudSQL() bool {
return d.GetType() == DatabaseTypeCloudSQL
}
// GetType returns the database type.
func (d *DatabaseV3) GetType() string {
if d.Spec.AWS.Redshift.ClusterID != "" {
return DatabaseTypeRedshift
}
if d.Spec.AWS.Region != "" {
return DatabaseTypeRDS
}
if d.Spec.GCP.ProjectID != "" {
return DatabaseTypeCloudSQL
}
return DatabaseTypeSelfHosted
}
// String returns the database string representation.
func (d *DatabaseV3) String() string {
return fmt.Sprintf("Database(Name=%v, Type=%v, Labels=%v)",
d.GetName(), d.GetType(), d.GetStaticLabels())
}
// setStaticFields sets static resource header and metadata fields.
func (d *DatabaseV3) setStaticFields() {
d.Kind = KindDatabase
d.Version = V3
}
// CheckAndSetDefaults checks and sets default values for any missing fields.
func (d *DatabaseV3) CheckAndSetDefaults() error {
d.setStaticFields()
if err := d.Metadata.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
}
for key := range d.Spec.DynamicLabels {
if !IsValidLabelKey(key) {
return trace.BadParameter("database %q invalid label key: %q", d.GetName(), key)
}
}
if d.Spec.Protocol == "" {
return trace.BadParameter("database %q protocol is empty", d.GetName())
}
if d.Spec.URI == "" {
return trace.BadParameter("database %q URI is empty", d.GetName())
}
// In case of RDS, Aurora or Redshift, AWS information such as region or
// cluster ID can be extracted from the endpoint if not provided.
switch {
case strings.Contains(d.Spec.URI, rdsEndpointSuffix):
region, err := parseRDSEndpoint(d.Spec.URI)
if err != nil {
return trace.Wrap(err)
}
if d.Spec.AWS.Region == "" {
d.Spec.AWS.Region = region
}
case strings.Contains(d.Spec.URI, redshiftEndpointSuffix):
clusterID, region, err := parseRedshiftEndpoint(d.Spec.URI)
if err != nil {
return trace.Wrap(err)
}
if d.Spec.AWS.Redshift.ClusterID == "" {
d.Spec.AWS.Redshift.ClusterID = clusterID
}
if d.Spec.AWS.Region == "" {
d.Spec.AWS.Region = region
}
}
return nil
}
// parseRDSEndpoint extracts region from the provided RDS endpoint.
func parseRDSEndpoint(endpoint string) (region string, err error) {
host, _, err := net.SplitHostPort(endpoint)
if err != nil {
return "", trace.Wrap(err)
}
// RDS/Aurora endpoint looks like this:
// aurora-instance-1.abcdefghijklmnop.us-west-1.rds.amazonaws.com
parts := strings.Split(host, ".")
if !strings.HasSuffix(host, rdsEndpointSuffix) || len(parts) != 6 {
return "", trace.BadParameter("failed to parse %v as RDS endpoint", endpoint)
}
return parts[2], nil
}
// parseRedshiftEndpoint extracts cluster ID and region from the provided Redshift endpoint.
func parseRedshiftEndpoint(endpoint string) (clusterID, region string, err error) {
host, _, err := net.SplitHostPort(endpoint)
if err != nil {
return "", "", trace.Wrap(err)
}
// Redshift endpoint looks like this:
// redshift-cluster-1.abcdefghijklmnop.us-east-1.rds.amazonaws.com
parts := strings.Split(host, ".")
if !strings.HasSuffix(host, redshiftEndpointSuffix) || len(parts) != 6 {
return "", "", trace.BadParameter("failed to parse %v as Redshift endpoint", endpoint)
}
return parts[0], parts[2], nil
}
const (
// DatabaseTypeSelfHosted is the self-hosted type of database.
DatabaseTypeSelfHosted = "self-hosted"
// DatabaseTypeRDS is AWS-hosted RDS or Aurora database.
DatabaseTypeRDS = "rds"
// DatabaseTypeRedshift is AWS Redshift database.
DatabaseTypeRedshift = "redshift"
// DatabaseTypeCloudSQL is GCP-hosted Cloud SQL database.
DatabaseTypeCloudSQL = "gcp"
)
// DeduplicateDatabases deduplicates databases by name.
func DeduplicateDatabases(databases []Database) (result []Database) {
seen := make(map[string]struct{})
for _, database := range databases {
if _, ok := seen[database.GetName()]; ok {
continue
}
seen[database.GetName()] = struct{}{}
result = append(result, database)
}
return result
}
const (
// rdsEndpointSuffix is the RDS/Aurora endpoint suffix.
rdsEndpointSuffix = ".rds.amazonaws.com"
// redshiftEndpointSuffix is the Redshift endpoint suffix.
redshiftEndpointSuffix = ".redshift.amazonaws.com"
)
+56
View File
@@ -0,0 +1,56 @@
/*
Copyright 2021 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package types
import (
"testing"
"github.com/stretchr/testify/require"
)
// TestDatabaseRDSEndpoint verifies AWS info is correctly populated
// based on the RDS endpoint.
func TestDatabaseRDSEndpoint(t *testing.T) {
database, err := NewDatabaseV3(Metadata{
Name: "rds",
}, DatabaseSpecV3{
Protocol: "postgres",
URI: "aurora-instance-1.abcdefghijklmnop.us-west-1.rds.amazonaws.com:5432",
})
require.NoError(t, err)
require.Equal(t, AWS{
Region: "us-west-1",
}, database.GetAWS())
}
// TestDatabaseRedshiftEndpoint verifies AWS info is correctly populated
// based on the Redshift endpoint.
func TestDatabaseRedshiftEndpoint(t *testing.T) {
database, err := NewDatabaseV3(Metadata{
Name: "redshift",
}, DatabaseSpecV3{
Protocol: "postgres",
URI: "redshift-cluster-1.abcdefghijklmnop.us-east-1.redshift.amazonaws.com:5438",
})
require.NoError(t, err)
require.Equal(t, AWS{
Region: "us-east-1",
Redshift: Redshift{
ClusterID: "redshift-cluster-1",
},
}, database.GetAWS())
}
+61 -232
View File
@@ -18,10 +18,10 @@ package types
import (
"fmt"
"net"
"strings"
"time"
"github.com/gravitational/teleport/api"
"github.com/gogo/protobuf/proto"
"github.com/gravitational/trace"
)
@@ -38,58 +38,25 @@ type DatabaseServer interface {
GetHostname() string
// GetHostID returns ID of the host the server is running on.
GetHostID() string
// GetStaticLabels returns server static labels.
GetStaticLabels() map[string]string
// SetStaticLabels sets server static labels.
SetStaticLabels(map[string]string)
// GetDynamicLabels returns server dynamic labels.
GetDynamicLabels() map[string]CommandLabel
// SetDynamicLabels sets server dynamic labels.
SetDynamicLabels(map[string]CommandLabel)
// GetAllLabels returns combined static and dynamic labels.
GetAllLabels() map[string]string
// LabelsString returns all labels as a string.
LabelsString() string
// GetRotation gets the state of certificate authority rotation.
GetRotation() Rotation
// SetRotation sets the state of certificate authority rotation.
SetRotation(Rotation)
// String returns string representation of the server.
String() string
// GetDescription returns the database server description.
GetDescription() string
// GetProtocol returns the database server protocol.
GetProtocol() string
// GetURI returns the database connection address.
GetURI() string
// GetCA returns the database CA certificate bytes.
GetCA() []byte
// SetCA sets the database CA certificate bytes.
SetCA([]byte)
// GetAWS returns AWS information for RDS/Aurora databases.
GetAWS() AWS
// GetGCP returns GCP information for Cloud SQL databases.
GetGCP() GCPCloudSQL
// GetType returns the database authentication type: self-hosted, RDS, Redshift or Cloud SQL.
GetType() string
// IsRDS returns true if this is an RDS/Aurora database.
IsRDS() bool
// IsRedshift returns true if this is a Redshift database.
IsRedshift() bool
// IsCloudSQL returns true if this is a Cloud SQL database.
IsCloudSQL() bool
// Copy returns a copy of this database server object.
Copy() DatabaseServer
// GetDatabases returns databases this database server proxies.
GetDatabases() []Database
// SetDatabases sets databases this database server proxies.
SetDatabases([]Database) error
}
// NewDatabaseServerV3 creates a new database server instance.
func NewDatabaseServerV3(name string, labels map[string]string, spec DatabaseServerSpecV3) (*DatabaseServerV3, error) {
func NewDatabaseServerV3(meta Metadata, spec DatabaseServerSpecV3) (*DatabaseServerV3, error) {
s := &DatabaseServerV3{
Metadata: Metadata{
Name: name,
Labels: labels,
},
Spec: spec,
Metadata: meta,
Spec: spec,
}
if err := s.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
@@ -182,107 +149,59 @@ func (s *DatabaseServerV3) SetRotation(r Rotation) {
s.Spec.Rotation = r
}
// GetStaticLabels returns the server static labels.
func (s *DatabaseServerV3) GetStaticLabels() map[string]string {
return s.Metadata.Labels
}
// SetStaticLabels sets the server static labels.
func (s *DatabaseServerV3) SetStaticLabels(sl map[string]string) {
s.Metadata.Labels = sl
}
// GetDynamicLabels returns the server dynamic labels.
func (s *DatabaseServerV3) GetDynamicLabels() map[string]CommandLabel {
if s.Spec.DynamicLabels == nil {
return nil
// GetDatabases returns databases this database server proxies.
func (s *DatabaseServerV3) GetDatabases() (databases []Database) {
if len(s.Spec.Databases) > 0 {
for _, database := range s.Spec.Databases {
databases = append(databases, database)
}
} else if s.Spec.URI != "" {
// Database server used to represent a single database in 7 and before,
// so in case any older database agents are still heartbeating back,
// adapt them to the current interface where server has a list of
// databases it proxies.
//
// DELETE IN 9.0.
databases = append(databases, &DatabaseV3{
Kind: KindDatabase,
Version: V3,
Metadata: Metadata{
Name: s.Metadata.Name,
Namespace: s.Metadata.Namespace,
Description: s.Spec.Description,
Labels: s.Metadata.Labels,
},
Spec: DatabaseSpecV3{
Protocol: s.Spec.Protocol,
URI: s.Spec.URI,
CACert: string(s.Spec.CACert),
DynamicLabels: s.Spec.DynamicLabels,
AWS: s.Spec.AWS,
GCP: s.Spec.GCP,
},
})
}
return V2ToLabels(s.Spec.DynamicLabels)
return databases
}
// SetDynamicLabels sets the server dynamic labels
func (s *DatabaseServerV3) SetDynamicLabels(dl map[string]CommandLabel) {
s.Spec.DynamicLabels = LabelsToV2(dl)
}
// GetAllLabels returns combines static and dynamic labels.
func (s *DatabaseServerV3) GetAllLabels() map[string]string {
return CombineLabels(s.Metadata.Labels, s.Spec.DynamicLabels)
}
// LabelsString returns all labels as a string.
func (s *DatabaseServerV3) LabelsString() string {
return LabelsAsString(s.Metadata.Labels, s.Spec.DynamicLabels)
}
// GetDescription returns the database server description.
func (s *DatabaseServerV3) GetDescription() string {
return s.Spec.Description
}
// GetProtocol returns the database server protocol.
func (s *DatabaseServerV3) GetProtocol() string {
return s.Spec.Protocol
}
// GetURI returns the database connection address.
func (s *DatabaseServerV3) GetURI() string {
return s.Spec.URI
}
// GetCA returns the database CA certificate bytes.
func (s *DatabaseServerV3) GetCA() []byte {
return s.Spec.CACert
}
// SetCA sets the database CA certificate bytes.
func (s *DatabaseServerV3) SetCA(bytes []byte) {
s.Spec.CACert = bytes
}
// GetAWS returns AWS information for RDS/Aurora databases.
func (s *DatabaseServerV3) GetAWS() AWS {
return s.Spec.AWS
}
// GetGCP returns GCP information for Cloud SQL databases.
func (s *DatabaseServerV3) GetGCP() GCPCloudSQL {
return s.Spec.GCP
}
// IsRDS returns true if this database represents AWS RDS/Aurora instance.
func (s *DatabaseServerV3) IsRDS() bool {
return s.GetType() == DatabaseTypeRDS
}
// IsRedshift returns true if this is a Redshift database instance.
func (s *DatabaseServerV3) IsRedshift() bool {
return s.GetType() == DatabaseTypeRedshift
}
// IsCloudSQL returns true if this database is a Cloud SQL instance.
func (s *DatabaseServerV3) IsCloudSQL() bool {
return s.GetType() == DatabaseTypeCloudSQL
}
// GetType returns the database type, self-hosted or AWS RDS.
func (s *DatabaseServerV3) GetType() string {
if s.Spec.AWS.Redshift.ClusterID != "" {
return DatabaseTypeRedshift
// SetDatabases sets databases this database server proxies.
func (s *DatabaseServerV3) SetDatabases(databases []Database) error {
databasesV3 := make([]*DatabaseV3, 0, len(databases))
for _, database := range databases {
databaseV3, ok := database.(*DatabaseV3)
if !ok {
return trace.BadParameter("expected *DatabaseV3, got %T", database)
}
databasesV3 = append(databasesV3, databaseV3)
}
if s.Spec.AWS.Region != "" {
return DatabaseTypeRDS
}
if s.Spec.GCP.ProjectID != "" {
return DatabaseTypeCloudSQL
}
return DatabaseTypeSelfHosted
s.Spec.Databases = databasesV3
return nil
}
// String returns the server string representation.
func (s *DatabaseServerV3) String() string {
return fmt.Sprintf("DatabaseServer(Name=%v, Type=%v, Version=%v, Labels=%v, HostID=%v)",
s.GetName(), s.GetType(), s.GetTeleportVersion(), s.GetStaticLabels(), s.Spec.HostID)
return fmt.Sprintf("DatabaseServer(Name=%v, Version=%v, Hostname=%v, HostID=%v, Databases=%v)",
s.GetName(), s.GetTeleportVersion(), s.GetHostname(), s.GetHostID(), s.GetDatabases())
}
// setStaticFields sets static resource header and metadata fields.
@@ -297,95 +216,28 @@ func (s *DatabaseServerV3) CheckAndSetDefaults() error {
if err := s.Metadata.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
}
for key := range s.Spec.DynamicLabels {
if !IsValidLabelKey(key) {
return trace.BadParameter("database server %q invalid label key: %q", s.GetName(), key)
}
}
if s.Spec.Protocol == "" {
return trace.BadParameter("database server %q protocol is empty", s.GetName())
}
if s.Spec.URI == "" {
return trace.BadParameter("database server %q URI is empty", s.GetName())
if s.Spec.HostID == "" {
return trace.BadParameter("missing database server HostID")
}
if s.Spec.Hostname == "" {
return trace.BadParameter("database server %q hostname is empty", s.GetName())
return trace.BadParameter("missing database server Hostname")
}
if s.Spec.HostID == "" {
return trace.BadParameter("database server %q host ID is empty", s.GetName())
if s.Spec.Version == "" {
s.Spec.Version = api.Version
}
// In case of RDS, Aurora or Redshift, AWS information such as region or
// cluster ID can be extracted from the endpoint if not provided.
switch {
case strings.Contains(s.Spec.URI, rdsEndpointSuffix):
region, err := parseRDSEndpoint(s.Spec.URI)
if err != nil {
for i := range s.Spec.Databases {
if err := s.Spec.Databases[i].CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
}
if s.Spec.AWS.Region == "" {
s.Spec.AWS.Region = region
}
case strings.Contains(s.Spec.URI, redshiftEndpointSuffix):
clusterID, region, err := parseRedshiftEndpoint(s.Spec.URI)
if err != nil {
return trace.Wrap(err)
}
if s.Spec.AWS.Redshift.ClusterID == "" {
s.Spec.AWS.Redshift.ClusterID = clusterID
}
if s.Spec.AWS.Region == "" {
s.Spec.AWS.Region = region
}
}
return nil
}
// parseRDSEndpoint extracts region from the provided RDS endpoint.
func parseRDSEndpoint(endpoint string) (region string, err error) {
host, _, err := net.SplitHostPort(endpoint)
if err != nil {
return "", trace.Wrap(err)
}
// RDS/Aurora endpoint looks like this:
// aurora-instance-1.abcdefghijklmnop.us-west-1.rds.amazonaws.com
parts := strings.Split(host, ".")
if !strings.HasSuffix(host, rdsEndpointSuffix) || len(parts) != 6 {
return "", trace.BadParameter("failed to parse %v as RDS endpoint", endpoint)
}
return parts[2], nil
}
// parseRedshiftEndpoint extracts cluster ID and region from the provided Redshift endpoint.
func parseRedshiftEndpoint(endpoint string) (clusterID, region string, err error) {
host, _, err := net.SplitHostPort(endpoint)
if err != nil {
return "", "", trace.Wrap(err)
}
// Redshift endpoint looks like this:
// redshift-cluster-1.abcdefghijklmnop.us-east-1.rds.amazonaws.com
parts := strings.Split(host, ".")
if !strings.HasSuffix(host, redshiftEndpointSuffix) || len(parts) != 6 {
return "", "", trace.BadParameter("failed to parse %v as Redshift endpoint", endpoint)
}
return parts[0], parts[2], nil
}
// Copy returns a copy of this database server object.
func (s *DatabaseServerV3) Copy() DatabaseServer {
return proto.Clone(s).(*DatabaseServerV3)
}
const (
// DatabaseTypeSelfHosted is the self-hosted type of database.
DatabaseTypeSelfHosted = "self-hosted"
// DatabaseTypeRDS is AWS-hosted RDS or Aurora database.
DatabaseTypeRDS = "rds"
// DatabaseTypeRedshift is AWS Redshift database.
DatabaseTypeRedshift = "redshift"
// DatabaseTypeCloudSQL is GCP-hosted Cloud SQL database.
DatabaseTypeCloudSQL = "gcp"
)
// SortedDatabaseServers implements sorter for database servers.
type SortedDatabaseServers []DatabaseServer
@@ -399,26 +251,3 @@ func (s SortedDatabaseServers) Less(i, j int) bool {
// Swap swaps two database servers.
func (s SortedDatabaseServers) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// DatabaseServers is a list of database servers.
type DatabaseServers []DatabaseServer
// DeduplicateDatabaseServers deduplicates database servers by name.
func DeduplicateDatabaseServers(servers []DatabaseServer) (result []DatabaseServer) {
seen := make(map[string]struct{})
for _, server := range servers {
if _, ok := seen[server.GetName()]; ok {
continue
}
seen[server.GetName()] = struct{}{}
result = append(result, server)
}
return result
}
const (
// rdsEndpointSuffix is the RDS/Aurora endpoint suffix.
rdsEndpointSuffix = ".rds.amazonaws.com"
// redshiftEndpointSuffix is the Redshift endpoint suffix.
redshiftEndpointSuffix = ".redshift.amazonaws.com"
)
+32 -27
View File
@@ -18,39 +18,44 @@ package types
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
// TestDatabaseServerRDSEndpoint verifies AWS info is correctly populated
// based on the RDS endpoint.
func TestDatabaseServerRDSEndpoint(t *testing.T) {
server, err := NewDatabaseServerV3("rds", nil, DatabaseServerSpecV3{
Protocol: "postgres",
URI: "aurora-instance-1.abcdefghijklmnop.us-west-1.rds.amazonaws.com:5432",
Hostname: "host-1",
HostID: "host-1",
// TestDatabaseServerGetDatabases verifies that older agents get adapted to
// the new database list interface for backward compatibility.
//
// DELETE IN 9.0.
func TestDatabaseServerGetDatabases(t *testing.T) {
server, err := NewDatabaseServerV3(Metadata{
Name: "server-1",
Labels: map[string]string{"a": "b"},
}, DatabaseServerSpecV3{
Description: "description",
Protocol: "postgres",
URI: "localhost:5432",
CACert: []byte("cert"),
AWS: AWS{Region: "us-east-1", Redshift: Redshift{ClusterID: "cluster-1"}},
Version: "1.0.0",
Hostname: "host",
HostID: "host-1",
DynamicLabels: map[string]CommandLabelV2{"c": {Period: Duration(time.Minute), Command: []string{"/bin/date"}}},
GCP: GCPCloudSQL{ProjectID: "project-1", InstanceID: "instance-1"},
})
require.NoError(t, err)
require.Equal(t, AWS{
Region: "us-west-1",
}, server.GetAWS())
}
// TestDatabaseServerRedshiftEndpoint verifies AWS info is correctly populated
// based on the Redshift endpoint.
func TestDatabaseServerRedshiftEndpoint(t *testing.T) {
server, err := NewDatabaseServerV3("redshift", nil, DatabaseServerSpecV3{
Protocol: "postgres",
URI: "redshift-cluster-1.abcdefghijklmnop.us-east-1.redshift.amazonaws.com:5438",
Hostname: "host-1",
HostID: "host-1",
database, err := NewDatabaseV3(Metadata{
Name: "server-1",
Description: "description",
Labels: map[string]string{"a": "b"},
}, DatabaseSpecV3{
Protocol: "postgres",
URI: "localhost:5432",
CACert: "cert",
AWS: AWS{Region: "us-east-1", Redshift: Redshift{ClusterID: "cluster-1"}},
DynamicLabels: map[string]CommandLabelV2{"c": {Period: Duration(time.Minute), Command: []string{"/bin/date"}}},
GCP: GCPCloudSQL{ProjectID: "project-1", InstanceID: "instance-1"},
})
require.NoError(t, err)
require.Equal(t, AWS{
Region: "us-east-1",
Redshift: Redshift{
ClusterID: "redshift-cluster-1",
},
}, server.GetAWS())
require.ElementsMatch(t, []Database{database}, server.GetDatabases())
}
+1789 -827
View File
File diff suppressed because it is too large Load Diff
+63 -9
View File
@@ -131,7 +131,7 @@ message ResourceHeader {
[ (gogoproto.nullable) = false, (gogoproto.jsontag) = "metadata,omitempty" ];
}
// DatabaseServerV2 represents a database access server.
// DatabaseServerV3 represents a database access server.
message DatabaseServerV3 {
option (gogoproto.goproto_stringer) = false;
option (gogoproto.stringer) = false;
@@ -151,15 +151,25 @@ message DatabaseServerV3 {
// DatabaseServerSpecV3 is the database server spec.
message DatabaseServerSpecV3 {
// Description is a free-form text describing this database server.
string Description = 1 [ (gogoproto.jsontag) = "description,omitempty" ];
//
// DEPRECATED: Moved to DatabaseSpecV3. DELETE IN 9.0.
string Description = 1 [ (gogoproto.jsontag) = "description,omitempty", deprecated = true ];
// Protocol is the database type e.g. postgres, mysql, etc.
string Protocol = 2 [ (gogoproto.jsontag) = "protocol" ];
//
// DEPRECATED: Moved to DatabaseSpecV3. DELETE IN 9.0.
string Protocol = 2 [ (gogoproto.jsontag) = "protocol", deprecated = true ];
// URI is the database connection address.
string URI = 3 [ (gogoproto.jsontag) = "uri" ];
//
// DEPRECATED: Moved to DatabaseSpecV3. DELETE IN 9.0.
string URI = 3 [ (gogoproto.jsontag) = "uri", deprecated = true ];
// CACert is an optional base64-encoded database CA certificate.
bytes CACert = 4 [ (gogoproto.jsontag) = "ca_cert,omitempty" ];
//
// DEPRECATED: Moved to DatabaseSpecV3. DELETE IN 9.0.
bytes CACert = 4 [ (gogoproto.jsontag) = "ca_cert,omitempty", deprecated = true ];
// AWS contains AWS specific settings for RDS/Aurora databases.
AWS AWS = 5 [ (gogoproto.nullable) = false, (gogoproto.jsontag) = "aws" ];
//
// DEPRECATED: Moved to DatabaseSpecV3. DELETE IN 9.0.
AWS AWS = 5 [ (gogoproto.nullable) = false, (gogoproto.jsontag) = "aws", deprecated = true ];
// Version is the Teleport version that the server is running.
string Version = 6 [ (gogoproto.jsontag) = "version" ];
// Hostname is the database server hostname.
@@ -167,13 +177,57 @@ message DatabaseServerSpecV3 {
// HostID is the ID of the host the database server is running on.
string HostID = 8 [ (gogoproto.jsontag) = "host_id" ];
// DynamicLabels is the database server dynamic labels.
map<string, CommandLabelV2> DynamicLabels = 9
[ (gogoproto.nullable) = false, (gogoproto.jsontag) = "dynamic_labels,omitempty" ];
//
// DEPRECATED: Moved to DatabaseSpecV3. DELETE IN 9.0.
map<string, CommandLabelV2> DynamicLabels = 9 [
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "dynamic_labels,omitempty",
deprecated = true
];
// Rotation contains the server CA rotation information.
Rotation Rotation = 10
[ (gogoproto.nullable) = false, (gogoproto.jsontag) = "rotation,omitempty" ];
// GCP contains parameters specific to GCP Cloud SQL databases.
GCPCloudSQL GCP = 11 [ (gogoproto.nullable) = false, (gogoproto.jsontag) = "gcp,omitempty" ];
//
// DEPRECATED: Moved to DatabaseSpecV3. DELETE IN 9.0.
GCPCloudSQL GCP = 11
[ (gogoproto.nullable) = false, (gogoproto.jsontag) = "gcp,omitempty", deprecated = true ];
// Databases is a list of databases this database server proxies.
repeated DatabaseV3 Databases = 12 [ (gogoproto.jsontag) = "databases,omitempty" ];
}
// DatabaseV3 represents a single proxied database.
message DatabaseV3 {
option (gogoproto.goproto_stringer) = false;
option (gogoproto.stringer) = false;
// Kind is the database resource kind.
string Kind = 1 [ (gogoproto.jsontag) = "kind" ];
// SubKind is an optional resource subkind.
string SubKind = 2 [ (gogoproto.jsontag) = "sub_kind,omitempty" ];
// Version is the resource version.
string Version = 3 [ (gogoproto.jsontag) = "version" ];
// Metadata is the database metadata.
Metadata Metadata = 4 [ (gogoproto.nullable) = false, (gogoproto.jsontag) = "metadata" ];
// Spec is the database spec.
DatabaseSpecV3 Spec = 5 [ (gogoproto.nullable) = false, (gogoproto.jsontag) = "spec" ];
}
// DatabaseSpecV3 is the database spec.
message DatabaseSpecV3 {
// Protocol is the database protocol: postgres, mysql, mongodb, etc.
string Protocol = 1 [ (gogoproto.jsontag) = "protocol" ];
// URI is the database connection endpoint.
string URI = 2 [ (gogoproto.jsontag) = "uri" ];
// CACert is the PEM-encoded database CA certificate.
string CACert = 3 [ (gogoproto.jsontag) = "ca_cert,omitempty" ];
// DynamicLabels is the database dynamic labels.
map<string, CommandLabelV2> DynamicLabels = 4
[ (gogoproto.nullable) = false, (gogoproto.jsontag) = "dynamic_labels,omitempty" ];
// AWS contains AWS specific settings for RDS/Aurora/Redshift databases.
AWS AWS = 5 [ (gogoproto.nullable) = false, (gogoproto.jsontag) = "aws,omitempty" ];
// GCP contains parameters specific to GCP Cloud SQL databases.
GCPCloudSQL GCP = 6 [ (gogoproto.nullable) = false, (gogoproto.jsontag) = "gcp,omitempty" ];
}
// AWS contains AWS specific settings such as region.
+7 -5
View File
@@ -687,11 +687,11 @@ func (p *databasePack) waitForLeaf(t *testing.T) {
logrus.WithError(err).Debugf("Leaf cluster access point is unavailable.")
continue
}
if !containsDBServer(servers, p.leaf.mysqlService.Name) {
if !containsDB(servers, p.leaf.mysqlService.Name) {
logrus.WithError(err).Debugf("Leaf db service %q is unavailable.", p.leaf.mysqlService.Name)
continue
}
if !containsDBServer(servers, p.leaf.postgresService.Name) {
if !containsDB(servers, p.leaf.postgresService.Name) {
logrus.WithError(err).Debugf("Leaf db service %q is unavailable.", p.leaf.postgresService.Name)
continue
}
@@ -702,10 +702,12 @@ func (p *databasePack) waitForLeaf(t *testing.T) {
}
}
func containsDBServer(servers []types.DatabaseServer, name string) bool {
func containsDB(servers []types.DatabaseServer, name string) bool {
for _, server := range servers {
if server.GetMetadata().Name == name {
return true
for _, database := range server.GetDatabases() {
if database.GetName() == name {
return true
}
}
}
return false
+8 -6
View File
@@ -2347,15 +2347,17 @@ func (a *Server) isMFARequired(ctx context.Context, checker services.AccessCheck
if t.Database.ServiceName == "" {
return nil, trace.BadParameter("missing ServiceName field in a database-only UserCertsRequest")
}
dbs, err := a.GetDatabaseServers(ctx, apidefaults.Namespace)
servers, err := a.GetDatabaseServers(ctx, apidefaults.Namespace)
if err != nil {
return nil, trace.Wrap(err)
}
var db types.DatabaseServer
for _, d := range dbs {
if d.GetName() == t.Database.ServiceName {
db = d
break
var db types.Database
for _, server := range servers {
for _, d := range server.GetDatabases() {
if d.GetName() == t.Database.ServiceName {
db = d
break
}
}
}
if db == nil {
+11 -8
View File
@@ -2679,20 +2679,23 @@ func (a *ServerWithRoles) GetDatabaseServers(ctx context.Context, namespace stri
if err != nil {
return nil, trace.Wrap(err)
}
// Filter out databases the caller doesn't have access to from each server.
var filtered []types.DatabaseServer
// MFA is not required to list the databases, but will be required to
// connect to them.
mfaParams := services.AccessMFAParams{Verified: true}
for _, server := range servers {
err := a.context.Checker.CheckAccessToDatabase(server, mfaParams, &services.DatabaseLabelsMatcher{Labels: server.GetAllLabels()})
if err != nil && !trace.IsAccessDenied(err) {
return nil, trace.Wrap(err)
} else if err == nil {
filtered = append(filtered, server)
// Filter out databases the caller doesn't have access to from each server.
var filtered []types.Database
for _, database := range server.GetDatabases() {
err := a.context.Checker.CheckAccessToDatabase(database, mfaParams, &services.DatabaseLabelsMatcher{Labels: database.GetAllLabels()})
if err != nil && !trace.IsAccessDenied(err) {
return nil, trace.Wrap(err)
} else if err == nil {
filtered = append(filtered, database)
}
}
server.SetDatabases(filtered)
}
return filtered, nil
return servers, nil
}
// UpsertDatabaseServer creates or updates a new database proxy server.
+37 -15
View File
@@ -409,22 +409,37 @@ func TestGetDatabaseServers(t *testing.T) {
srv := newTestTLSServer(t)
// Create test databases.
var dbs []*types.DatabaseV3
for i := 0; i < 5; i++ {
name := uuid.New()
db, err := types.NewDatabaseServerV3(name, map[string]string{"name": name}, types.DatabaseServerSpecV3{
db, err := types.NewDatabaseV3(types.Metadata{
Name: name,
Labels: map[string]string{"name": name},
}, types.DatabaseSpecV3{
Protocol: "postgres",
URI: "example.com",
Hostname: "host",
HostID: "hostid",
})
require.NoError(t, err)
_, err = srv.Auth().UpsertDatabaseServer(ctx, db)
require.NoError(t, err)
dbs = append(dbs, db)
}
name := uuid.New()
server, err := types.NewDatabaseServerV3(types.Metadata{
Name: name,
Labels: map[string]string{"name": name},
}, types.DatabaseServerSpecV3{
Hostname: "host",
HostID: "hostid",
Databases: dbs,
})
require.NoError(t, err)
_, err = srv.Auth().UpsertDatabaseServer(ctx, server)
require.NoError(t, err)
testServers, err := srv.Auth().GetDatabaseServers(ctx, defaults.Namespace)
require.NoError(t, err)
require.Equal(t, 1, len(testServers))
testDatabases := testServers[0].GetDatabases()
require.ElementsMatch(t, dbs, testDatabases)
// create user, role, and client
username := "user"
@@ -435,36 +450,43 @@ func TestGetDatabaseServers(t *testing.T) {
require.NoError(t, err)
// permit user to get the first database
role.SetDatabaseLabels(types.Allow, types.Labels{"name": {testServers[0].GetName()}})
role.SetDatabaseLabels(types.Allow, types.Labels{"name": {testDatabases[0].GetName()}})
require.NoError(t, srv.Auth().UpsertRole(ctx, role))
servers, err := clt.GetDatabaseServers(ctx, defaults.Namespace)
require.NoError(t, err)
require.EqualValues(t, 1, len(servers))
require.Empty(t, cmp.Diff(testServers[0:1], servers))
databases := servers[0].GetDatabases()
require.Equal(t, 1, len(databases))
require.Empty(t, cmp.Diff(testDatabases[0:1], databases))
// permit user to get all databases
role.SetDatabaseLabels(types.Allow, types.Labels{types.Wildcard: {types.Wildcard}})
require.NoError(t, srv.Auth().UpsertRole(ctx, role))
servers, err = clt.GetDatabaseServers(ctx, defaults.Namespace)
require.NoError(t, err)
require.EqualValues(t, len(testServers), len(servers))
require.Empty(t, cmp.Diff(testServers, servers))
require.Equal(t, 1, len(servers))
databases = servers[0].GetDatabases()
require.EqualValues(t, len(testDatabases), len(databases))
require.Empty(t, cmp.Diff(testDatabases, databases))
// deny user to get the first database
role.SetDatabaseLabels(types.Deny, types.Labels{"name": {testServers[0].GetName()}})
role.SetDatabaseLabels(types.Deny, types.Labels{"name": {testDatabases[0].GetName()}})
require.NoError(t, srv.Auth().UpsertRole(ctx, role))
servers, err = clt.GetDatabaseServers(ctx, defaults.Namespace)
require.NoError(t, err)
require.EqualValues(t, len(testServers[1:]), len(servers))
require.Empty(t, cmp.Diff(testServers[1:], servers))
require.Equal(t, 1, len(servers))
databases = servers[0].GetDatabases()
require.EqualValues(t, len(testDatabases[1:]), len(databases))
require.Empty(t, cmp.Diff(testDatabases[1:], databases))
// deny user to get all databases
role.SetDatabaseLabels(types.Deny, types.Labels{types.Wildcard: {types.Wildcard}})
require.NoError(t, srv.Auth().UpsertRole(ctx, role))
servers, err = clt.GetDatabaseServers(ctx, defaults.Namespace)
require.NoError(t, err)
require.EqualValues(t, 0, len(servers))
require.Empty(t, cmp.Diff([]types.DatabaseServer{}, servers))
require.Equal(t, 1, len(servers))
databases = servers[0].GetDatabases()
require.EqualValues(t, 0, len(databases))
}
func TestGetAppServers(t *testing.T) {
+3 -1
View File
@@ -588,7 +588,9 @@ func TestGenerateUserSingleUseCert(t *testing.T) {
err = srv.Auth().UpsertKubeService(ctx, k8sSrv)
require.NoError(t, err)
// Register a database.
db, err := types.NewDatabaseServerV3("db-a", nil, types.DatabaseServerSpecV3{
db, err := types.NewDatabaseServerV3(types.Metadata{
Name: "db-a",
}, types.DatabaseServerSpecV3{
Protocol: "postgres",
URI: "localhost",
Hostname: "localhost",
+8 -7
View File
@@ -1793,13 +1793,14 @@ func TestDatabaseServers(t *testing.T) {
ctx := context.Background()
// Upsert database server into backend.
server, err := types.NewDatabaseServerV3("foo", nil,
types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
Hostname: "localhost",
HostID: uuid.New(),
})
server, err := types.NewDatabaseServerV3(types.Metadata{
Name: "foo",
}, types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
Hostname: "localhost",
HostID: uuid.New(),
})
require.NoError(t, err)
_, err = p.presenceS.UpsertDatabaseServer(ctx, server)
+13
View File
@@ -1830,6 +1830,19 @@ func (tc *TeleportClient) ListDatabaseServers(ctx context.Context) ([]types.Data
return proxyClient.GetDatabaseServers(ctx, tc.Namespace)
}
// ListDatabases returns all registered databases.
func (tc *TeleportClient) ListDatabases(ctx context.Context) ([]types.Database, error) {
servers, err := tc.ListDatabaseServers(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
var databases []types.Database
for _, server := range servers {
databases = append(databases, server.GetDatabases()...)
}
return types.DeduplicateDatabases(databases), nil
}
// ListAllNodes is the same as ListNodes except that it ignores labels.
func (tc *TeleportClient) ListAllNodes(ctx context.Context) ([]types.Server, error) {
proxyClient, err := tc.ConnectToProxy(ctx)
+25 -15
View File
@@ -82,17 +82,20 @@ func (process *TeleportProcess) initDatabaseService() (retErr error) {
return trace.Wrap(err)
}
// Create database server for each of the proxied databases.
var databaseServers []types.DatabaseServer
// Create database server containing all databases defined
// in the static configuration.
var databases []*types.DatabaseV3
for _, db := range process.Config.Databases.Databases {
db, err := types.NewDatabaseServerV3(
db.Name,
db.StaticLabels,
types.DatabaseServerSpecV3{
db, err := types.NewDatabaseV3(
types.Metadata{
Name: db.Name,
Description: db.Description,
Protocol: db.Protocol,
URI: db.URI,
CACert: db.CACert,
Labels: db.StaticLabels,
},
types.DatabaseSpecV3{
Protocol: db.Protocol,
URI: db.URI,
CACert: string(db.CACert),
AWS: types.AWS{
Region: db.AWS.Region,
Redshift: types.Redshift{
@@ -104,14 +107,21 @@ func (process *TeleportProcess) initDatabaseService() (retErr error) {
InstanceID: db.GCP.InstanceID,
},
DynamicLabels: types.LabelsToV2(db.DynamicLabels),
Version: teleport.Version,
Hostname: process.Config.Hostname,
HostID: process.Config.HostUUID,
})
if err != nil {
return trace.Wrap(err)
}
databaseServers = append(databaseServers, db)
databases = append(databases, db)
}
databaseServer, err := types.NewDatabaseServerV3(types.Metadata{
Name: process.Config.HostUUID,
}, types.DatabaseServerSpecV3{
Hostname: process.Config.Hostname,
HostID: process.Config.HostUUID,
Databases: databases,
})
if err != nil {
return trace.Wrap(err)
}
clusterName := conn.ServerIdentity.Cert.Extensions[utils.CertExtensionAuthority]
@@ -168,7 +178,7 @@ func (process *TeleportProcess) initDatabaseService() (retErr error) {
Authorizer: authorizer,
TLSConfig: tlsConfig,
GetRotation: process.getRotation,
Servers: databaseServers,
Server: databaseServer,
OnHeartbeat: func(err error) {
if err != nil {
process.BroadcastEvent(Event{Name: TeleportDegradedEvent, Payload: teleport.ComponentDatabase})
@@ -230,7 +240,7 @@ func (process *TeleportProcess) initDatabaseService() (retErr error) {
})
process.BroadcastEvent(Event{Name: DatabasesReady, Payload: nil})
log.Infof("Database service has successfully started: %v.", databaseServers)
log.Infof("Database service has successfully started: %v.", databases)
// Block and wait while the server and agent pool are running.
if err := dbService.Wait(); err != nil {
+8 -7
View File
@@ -121,13 +121,14 @@ func TestDatabaseServersCRUD(t *testing.T) {
presence := NewPresenceService(backend)
// Create a database server.
server, err := types.NewDatabaseServerV3("foo", nil,
types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
Hostname: "localhost",
HostID: uuid.New(),
})
server, err := types.NewDatabaseServerV3(types.Metadata{
Name: "foo",
}, types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
Hostname: "localhost",
HostID: uuid.New(),
})
require.NoError(t, err)
// Initially expect not to be returned any servers.
+2 -2
View File
@@ -775,7 +775,7 @@ type AccessChecker interface {
// CheckAccessToDatabase checks whether a user has access to the provided
// database server.
CheckAccessToDatabase(server types.DatabaseServer, mfa AccessMFAParams, matchers ...RoleMatcher) error
CheckAccessToDatabase(server types.Database, mfa AccessMFAParams, matchers ...RoleMatcher) error
// CheckImpersonate checks whether current user is allowed to impersonate
// users and roles
@@ -1863,7 +1863,7 @@ func (m *DatabaseNameMatcher) String() string {
//
// The checker always checks the server namespace, other matchers are supplied
// by the caller.
func (set RoleSet) CheckAccessToDatabase(server types.DatabaseServer, mfa AccessMFAParams, matchers ...RoleMatcher) error {
func (set RoleSet) CheckAccessToDatabase(server types.Database, mfa AccessMFAParams, matchers ...RoleMatcher) error {
log := log.WithField(trace.Component, teleport.ComponentRBAC)
if mfa.AlwaysRequired && !mfa.Verified {
log.Debugf("Access to database %q denied, cluster requires per-session MFA", server.GetName())
+60 -69
View File
@@ -2189,23 +2189,21 @@ func TestBoolOptions(t *testing.T) {
}
func TestCheckAccessToDatabase(t *testing.T) {
dbStage, err := types.NewDatabaseServerV3("stage",
map[string]string{"env": "stage"},
types.DatabaseServerSpecV3{
Protocol: "protocol",
URI: "uri",
Hostname: "hostname",
HostID: "host_id",
})
dbStage, err := types.NewDatabaseV3(types.Metadata{
Name: "stage",
Labels: map[string]string{"env": "stage"},
}, types.DatabaseSpecV3{
Protocol: "protocol",
URI: "uri",
})
require.NoError(t, err)
dbProd, err := types.NewDatabaseServerV3("prod",
map[string]string{"env": "prod"},
types.DatabaseServerSpecV3{
Protocol: "protocol",
URI: "uri",
Hostname: "hostname",
HostID: "host_id",
})
dbProd, err := types.NewDatabaseV3(types.Metadata{
Name: "prod",
Labels: map[string]string{"env": "prod"},
}, types.DatabaseSpecV3{
Protocol: "protocol",
URI: "uri",
})
require.NoError(t, err)
roleDevStage := &types.RoleV4{
Metadata: types.Metadata{Name: "dev-stage", Namespace: defaults.Namespace},
@@ -2273,7 +2271,7 @@ func TestCheckAccessToDatabase(t *testing.T) {
}
require.NoError(t, roleDeny.CheckAndSetDefaults())
type access struct {
server types.DatabaseServer
server types.Database
dbName string
dbUser string
access bool
@@ -2365,23 +2363,21 @@ func TestCheckAccessToDatabase(t *testing.T) {
}
func TestCheckAccessToDatabaseUser(t *testing.T) {
dbStage, err := types.NewDatabaseServerV3("stage",
map[string]string{"env": "stage"},
types.DatabaseServerSpecV3{
Protocol: "protocol",
URI: "uri",
Hostname: "hostname",
HostID: "host_id",
})
dbStage, err := types.NewDatabaseV3(types.Metadata{
Name: "stage",
Labels: map[string]string{"env": "stage"},
}, types.DatabaseSpecV3{
Protocol: "protocol",
URI: "uri",
})
require.NoError(t, err)
dbProd, err := types.NewDatabaseServerV3("prod",
map[string]string{"env": "prod"},
types.DatabaseServerSpecV3{
Protocol: "protocol",
URI: "uri",
Hostname: "hostname",
HostID: "host_id",
})
dbProd, err := types.NewDatabaseV3(types.Metadata{
Name: "prod",
Labels: map[string]string{"env": "prod"},
}, types.DatabaseSpecV3{
Protocol: "protocol",
URI: "uri",
})
require.NoError(t, err)
roleDevStage := &types.RoleV4{
Metadata: types.Metadata{Name: "dev-stage", Namespace: defaults.Namespace},
@@ -2408,7 +2404,7 @@ func TestCheckAccessToDatabaseUser(t *testing.T) {
},
}
type access struct {
server types.DatabaseServer
server types.Database
dbUser string
access bool
}
@@ -2545,43 +2541,38 @@ func TestCheckDatabaseNamesAndUsers(t *testing.T) {
}
func TestCheckAccessToDatabaseService(t *testing.T) {
dbNoLabels, err := types.NewDatabaseServerV3("test",
nil,
types.DatabaseServerSpecV3{
Protocol: "protocol",
URI: "uri",
Hostname: "hostname",
HostID: "host_id",
})
dbNoLabels, err := types.NewDatabaseV3(types.Metadata{
Name: "test",
}, types.DatabaseSpecV3{
Protocol: "protocol",
URI: "uri",
})
require.NoError(t, err)
dbStage, err := types.NewDatabaseServerV3("stage",
map[string]string{"env": "stage"},
types.DatabaseServerSpecV3{
Protocol: "protocol",
URI: "uri",
Hostname: "hostname",
HostID: "host_id",
DynamicLabels: map[string]types.CommandLabelV2{"arch": {Result: "x86"}},
})
dbStage, err := types.NewDatabaseV3(types.Metadata{
Name: "stage",
Labels: map[string]string{"env": "stage"},
}, types.DatabaseSpecV3{
Protocol: "protocol",
URI: "uri",
DynamicLabels: map[string]types.CommandLabelV2{"arch": {Result: "x86"}},
})
require.NoError(t, err)
dbStage2, err := types.NewDatabaseServerV3("stage2",
map[string]string{"env": "stage"},
types.DatabaseServerSpecV3{
Protocol: "protocol",
URI: "uri",
Hostname: "hostname",
HostID: "host_id",
DynamicLabels: map[string]types.CommandLabelV2{"arch": {Result: "amd64"}},
})
dbStage2, err := types.NewDatabaseV3(types.Metadata{
Name: "stage2",
Labels: map[string]string{"env": "stage"},
}, types.DatabaseSpecV3{
Protocol: "protocol",
URI: "uri",
DynamicLabels: map[string]types.CommandLabelV2{"arch": {Result: "amd64"}},
})
require.NoError(t, err)
dbProd, err := types.NewDatabaseServerV3("prod",
map[string]string{"env": "prod"},
types.DatabaseServerSpecV3{
Protocol: "protocol",
URI: "uri",
Hostname: "hostname",
HostID: "host_id",
})
dbProd, err := types.NewDatabaseV3(types.Metadata{
Name: "prod",
Labels: map[string]string{"env": "prod"},
}, types.DatabaseSpecV3{
Protocol: "protocol",
URI: "uri",
})
require.NoError(t, err)
roleAdmin := &types.RoleV4{
Metadata: types.Metadata{Name: "admin", Namespace: apidefaults.Namespace},
@@ -2614,7 +2605,7 @@ func TestCheckAccessToDatabaseService(t *testing.T) {
},
}
type access struct {
server types.DatabaseServer
server types.Database
access bool
}
testCases := []struct {
+2 -12
View File
@@ -27,9 +27,8 @@ import (
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/trace"
"github.com/google/go-cmp/cmp"
"github.com/gravitational/trace"
)
const (
@@ -121,21 +120,12 @@ func compareDatabaseServers(a, b types.DatabaseServer) int {
if !r.Matches(b.GetRotation()) {
return Different
}
if !utils.StringMapsEqual(a.GetStaticLabels(), b.GetStaticLabels()) {
return Different
}
if !cmp.Equal(a.GetDynamicLabels(), b.GetDynamicLabels()) {
if !cmp.Equal(a.GetDatabases(), b.GetDatabases()) {
return Different
}
if !a.Expiry().Equal(b.Expiry()) {
return OnlyTimestampsDifferent
}
if a.GetProtocol() != b.GetProtocol() {
return Different
}
if a.GetURI() != b.GetURI() {
return Different
}
return Equal
}
+187 -164
View File
@@ -393,6 +393,49 @@ func TestAccessDisabled(t *testing.T) {
require.Contains(t, err.Error(), "this Teleport cluster is not licensed for database access")
}
// TestCompatibilityWithOldAgents verifies that older database agents where
// each database was represented as a single DatabaseServer are supported.
//
// DELETE IN 9.0.
func TestCompatibilityWithOldAgents(t *testing.T) {
ctx := context.Background()
testCtx := setupTestContext(ctx, t)
go testCtx.startProxy()
postgresServer, err := postgres.NewTestServer(common.TestServerConfig{
Name: "postgres",
AuthClient: testCtx.authClient,
})
require.NoError(t, err)
go postgresServer.Serve()
t.Cleanup(func() { postgresServer.Close() })
resource, err := types.NewDatabaseServerV3(types.Metadata{
Name: "postgres",
}, types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: net.JoinHostPort("localhost", postgresServer.Port()),
HostID: testCtx.hostID,
Hostname: constants.APIDomain,
})
require.NoError(t, err)
databaseServer := testCtx.setupDatabaseServer(ctx, t, resource)
go func() {
for conn := range testCtx.proxyConn {
go databaseServer.HandleConnection(conn)
}
}()
testCtx.createUserAndRole(ctx, t, "alice", "admin", []string{"postgres"}, []string{"postgres"})
// Make sure we can connect successfully.
psql, err := testCtx.postgresClient(ctx, "alice", "postgres", "postgres", "postgres")
require.NoError(t, err)
err = psql.Close(ctx)
require.NoError(t, err)
}
type testContext struct {
hostID string
clusterName string
@@ -422,24 +465,24 @@ type testContext struct {
type testPostgres struct {
// db is the test Postgres database server.
db *postgres.TestServer
// server is the resource representing this Postgres server.
server types.DatabaseServer
// resource is the resource representing this Postgres database.
resource types.Database
}
// testMySQL represents a single proxied MySQL database.
type testMySQL struct {
// db is the test MySQL database server.
db *mysql.TestServer
// server is the resource representing this MySQL server.
server types.DatabaseServer
// resource is the resource representing this MySQL database.
resource types.Database
}
// testMongoDB represents a single proxied MongoDB database.
type testMongoDB struct {
// db is the test MongoDB database server.
db *mongodb.TestServer
// server is the resource representing this MongoDB server.
server types.DatabaseServer
// resource is the resource representing this MongoDB database.
resource types.Database
}
// startProxy starts all proxy services required to handle connections.
@@ -654,9 +697,9 @@ func setupTestContext(ctx context.Context, t *testing.T, withDatabases ...withDa
require.NoError(t, err)
// Set up database servers used by this test.
var databaseServers []types.DatabaseServer
var databases []types.Database
for _, withDatabase := range withDatabases {
databaseServers = append(databaseServers, withDatabase(t, ctx, testCtx))
databases = append(databases, withDatabase(t, ctx, testCtx))
}
// Establish fake reversetunnel b/w database proxy and database service.
@@ -695,17 +738,25 @@ func setupTestContext(ctx context.Context, t *testing.T, withDatabases ...withDa
require.NoError(t, err)
// Create database service server.
if len(databaseServers) > 0 {
testCtx.server = testCtx.setupDatabaseServer(ctx, t, testCtx.hostID,
databaseServers...)
if len(databases) > 0 {
resource, err := types.NewDatabaseServerV3(types.Metadata{
Name: testCtx.hostID,
}, types.DatabaseServerSpecV3{
HostID: testCtx.hostID,
Hostname: constants.APIDomain,
})
require.NoError(t, err)
err = resource.SetDatabases(databases)
require.NoError(t, err)
testCtx.server = testCtx.setupDatabaseServer(ctx, t, resource)
}
return testCtx
}
func (c *testContext) setupDatabaseServer(ctx context.Context, t *testing.T, hostID string, servers ...types.DatabaseServer) *Server {
func (c *testContext) setupDatabaseServer(ctx context.Context, t *testing.T, resource types.DatabaseServer) *Server {
// Database service credentials.
serverIdentity, err := auth.NewServerIdentity(c.authServer, hostID, types.RoleDatabase)
serverIdentity, err := auth.NewServerIdentity(c.authServer, resource.GetHostID(), types.RoleDatabase)
require.NoError(t, err)
tlsConfig, err := serverIdentity.TLSConfig(nil)
require.NoError(t, err)
@@ -729,7 +780,11 @@ func (c *testContext) setupDatabaseServer(ctx context.Context, t *testing.T, hos
})
require.NoError(t, err)
// Create a lock watcher for the DB service.
// Create resource representing this database server agent.
_, err = c.authClient.UpsertDatabaseServer(ctx, resource)
require.NoError(t, err)
// Create database server agent itself.
server, err := New(ctx, Config{
Clock: clockwork.NewFakeClockAt(time.Now()),
DataDir: t.TempDir(),
@@ -737,7 +792,7 @@ func (c *testContext) setupDatabaseServer(ctx context.Context, t *testing.T, hos
AccessPoint: c.authClient,
StreamEmitter: c.authClient,
Authorizer: dbAuthorizer,
Servers: servers,
Server: resource,
TLSConfig: tlsConfig,
Auth: testAuth,
GetRotation: func(types.SystemRole) (*types.Rotation, error) {
@@ -760,10 +815,10 @@ func (c *testContext) setupDatabaseServer(ctx context.Context, t *testing.T, hos
return server
}
type withDatabaseOption func(t *testing.T, ctx context.Context, testCtx *testContext) types.DatabaseServer
type withDatabaseOption func(t *testing.T, ctx context.Context, testCtx *testContext) types.Database
func withSelfHostedPostgres(name string) withDatabaseOption {
return func(t *testing.T, ctx context.Context, testCtx *testContext) types.DatabaseServer {
return func(t *testing.T, ctx context.Context, testCtx *testContext) types.Database {
postgresServer, err := postgres.NewTestServer(common.TestServerConfig{
Name: name,
AuthClient: testCtx.authClient,
@@ -771,28 +826,24 @@ func withSelfHostedPostgres(name string) withDatabaseOption {
require.NoError(t, err)
go postgresServer.Serve()
t.Cleanup(func() { postgresServer.Close() })
server, err := types.NewDatabaseServerV3(name, nil,
types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: net.JoinHostPort("localhost", postgresServer.Port()),
Version: teleport.Version,
Hostname: constants.APIDomain,
HostID: testCtx.hostID,
DynamicLabels: dynamicLabels,
})
require.NoError(t, err)
_, err = testCtx.authClient.UpsertDatabaseServer(ctx, server)
database, err := types.NewDatabaseV3(types.Metadata{
Name: name,
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: net.JoinHostPort("localhost", postgresServer.Port()),
DynamicLabels: dynamicLabels,
})
require.NoError(t, err)
testCtx.postgres[name] = testPostgres{
db: postgresServer,
server: server,
db: postgresServer,
resource: database,
}
return server
return database
}
}
func withRDSPostgres(name, authToken string) withDatabaseOption {
return func(t *testing.T, ctx context.Context, testCtx *testContext) types.DatabaseServer {
return func(t *testing.T, ctx context.Context, testCtx *testContext) types.Database {
postgresServer, err := postgres.NewTestServer(common.TestServerConfig{
Name: name,
AuthClient: testCtx.authClient,
@@ -801,33 +852,29 @@ func withRDSPostgres(name, authToken string) withDatabaseOption {
require.NoError(t, err)
go postgresServer.Serve()
t.Cleanup(func() { postgresServer.Close() })
server, err := types.NewDatabaseServerV3(name, nil,
types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: net.JoinHostPort("localhost", postgresServer.Port()),
Version: teleport.Version,
Hostname: constants.APIDomain,
HostID: testCtx.hostID,
DynamicLabels: dynamicLabels,
AWS: types.AWS{
Region: testAWSRegion,
},
// Set CA cert, otherwise we will attempt to download RDS roots.
CACert: testCtx.hostCA.GetActiveKeys().TLS[0].Cert,
})
require.NoError(t, err)
_, err = testCtx.authClient.UpsertDatabaseServer(ctx, server)
database, err := types.NewDatabaseV3(types.Metadata{
Name: name,
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: net.JoinHostPort("localhost", postgresServer.Port()),
DynamicLabels: dynamicLabels,
AWS: types.AWS{
Region: testAWSRegion,
},
// Set CA cert, otherwise we will attempt to download RDS roots.
CACert: string(testCtx.hostCA.GetActiveKeys().TLS[0].Cert),
})
require.NoError(t, err)
testCtx.postgres[name] = testPostgres{
db: postgresServer,
server: server,
db: postgresServer,
resource: database,
}
return server
return database
}
}
func withRedshiftPostgres(name, authToken string) withDatabaseOption {
return func(t *testing.T, ctx context.Context, testCtx *testContext) types.DatabaseServer {
return func(t *testing.T, ctx context.Context, testCtx *testContext) types.Database {
postgresServer, err := postgres.NewTestServer(common.TestServerConfig{
Name: name,
AuthClient: testCtx.authClient,
@@ -836,34 +883,30 @@ func withRedshiftPostgres(name, authToken string) withDatabaseOption {
require.NoError(t, err)
go postgresServer.Serve()
t.Cleanup(func() { postgresServer.Close() })
server, err := types.NewDatabaseServerV3(name, nil,
types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: net.JoinHostPort("localhost", postgresServer.Port()),
Version: teleport.Version,
Hostname: constants.APIDomain,
HostID: testCtx.hostID,
DynamicLabels: dynamicLabels,
AWS: types.AWS{
Region: testAWSRegion,
Redshift: types.Redshift{ClusterID: "redshift-cluster-1"},
},
// Set CA cert, otherwise we will attempt to download Redshift roots.
CACert: testCtx.hostCA.GetActiveKeys().TLS[0].Cert,
})
require.NoError(t, err)
_, err = testCtx.authClient.UpsertDatabaseServer(ctx, server)
database, err := types.NewDatabaseV3(types.Metadata{
Name: name,
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: net.JoinHostPort("localhost", postgresServer.Port()),
DynamicLabels: dynamicLabels,
AWS: types.AWS{
Region: testAWSRegion,
Redshift: types.Redshift{ClusterID: "redshift-cluster-1"},
},
// Set CA cert, otherwise we will attempt to download Redshift roots.
CACert: string(testCtx.hostCA.GetActiveKeys().TLS[0].Cert),
})
require.NoError(t, err)
testCtx.postgres[name] = testPostgres{
db: postgresServer,
server: server,
db: postgresServer,
resource: database,
}
return server
return database
}
}
func withCloudSQLPostgres(name, authToken string) withDatabaseOption {
return func(t *testing.T, ctx context.Context, testCtx *testContext) types.DatabaseServer {
return func(t *testing.T, ctx context.Context, testCtx *testContext) types.Database {
postgresServer, err := postgres.NewTestServer(common.TestServerConfig{
Name: name,
AuthClient: testCtx.authClient,
@@ -875,34 +918,30 @@ func withCloudSQLPostgres(name, authToken string) withDatabaseOption {
require.NoError(t, err)
go postgresServer.Serve()
t.Cleanup(func() { postgresServer.Close() })
server, err := types.NewDatabaseServerV3(name, nil,
types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: net.JoinHostPort("localhost", postgresServer.Port()),
Version: teleport.Version,
Hostname: constants.APIDomain,
HostID: testCtx.hostID,
DynamicLabels: dynamicLabels,
GCP: types.GCPCloudSQL{
ProjectID: "project-1",
InstanceID: "instance-1",
},
// Set CA cert to pass cert validation.
CACert: testCtx.hostCA.GetActiveKeys().TLS[0].Cert,
})
require.NoError(t, err)
_, err = testCtx.authClient.UpsertDatabaseServer(ctx, server)
database, err := types.NewDatabaseV3(types.Metadata{
Name: name,
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: net.JoinHostPort("localhost", postgresServer.Port()),
DynamicLabels: dynamicLabels,
GCP: types.GCPCloudSQL{
ProjectID: "project-1",
InstanceID: "instance-1",
},
// Set CA cert to pass cert validation.
CACert: string(testCtx.hostCA.GetActiveKeys().TLS[0].Cert),
})
require.NoError(t, err)
testCtx.postgres[name] = testPostgres{
db: postgresServer,
server: server,
db: postgresServer,
resource: database,
}
return server
return database
}
}
func withSelfHostedMySQL(name string) withDatabaseOption {
return func(t *testing.T, ctx context.Context, testCtx *testContext) types.DatabaseServer {
return func(t *testing.T, ctx context.Context, testCtx *testContext) types.Database {
mysqlServer, err := mysql.NewTestServer(common.TestServerConfig{
Name: name,
AuthClient: testCtx.authClient,
@@ -910,28 +949,24 @@ func withSelfHostedMySQL(name string) withDatabaseOption {
require.NoError(t, err)
go mysqlServer.Serve()
t.Cleanup(func() { mysqlServer.Close() })
server, err := types.NewDatabaseServerV3(name, nil,
types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolMySQL,
URI: net.JoinHostPort("localhost", mysqlServer.Port()),
Version: teleport.Version,
Hostname: constants.APIDomain,
HostID: testCtx.hostID,
DynamicLabels: dynamicLabels,
})
require.NoError(t, err)
_, err = testCtx.authClient.UpsertDatabaseServer(ctx, server)
database, err := types.NewDatabaseV3(types.Metadata{
Name: name,
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolMySQL,
URI: net.JoinHostPort("localhost", mysqlServer.Port()),
DynamicLabels: dynamicLabels,
})
require.NoError(t, err)
testCtx.mysql[name] = testMySQL{
db: mysqlServer,
server: server,
db: mysqlServer,
resource: database,
}
return server
return database
}
}
func withRDSMySQL(name, authUser, authToken string) withDatabaseOption {
return func(t *testing.T, ctx context.Context, testCtx *testContext) types.DatabaseServer {
return func(t *testing.T, ctx context.Context, testCtx *testContext) types.Database {
mysqlServer, err := mysql.NewTestServer(common.TestServerConfig{
Name: name,
AuthClient: testCtx.authClient,
@@ -941,33 +976,29 @@ func withRDSMySQL(name, authUser, authToken string) withDatabaseOption {
require.NoError(t, err)
go mysqlServer.Serve()
t.Cleanup(func() { mysqlServer.Close() })
server, err := types.NewDatabaseServerV3(name, nil,
types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolMySQL,
URI: net.JoinHostPort("localhost", mysqlServer.Port()),
Version: teleport.Version,
Hostname: constants.APIDomain,
HostID: testCtx.hostID,
DynamicLabels: dynamicLabels,
AWS: types.AWS{
Region: testAWSRegion,
},
// Set CA cert, otherwise we will attempt to download RDS roots.
CACert: testCtx.hostCA.GetActiveKeys().TLS[0].Cert,
})
require.NoError(t, err)
_, err = testCtx.authClient.UpsertDatabaseServer(ctx, server)
database, err := types.NewDatabaseV3(types.Metadata{
Name: name,
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolMySQL,
URI: net.JoinHostPort("localhost", mysqlServer.Port()),
DynamicLabels: dynamicLabels,
AWS: types.AWS{
Region: testAWSRegion,
},
// Set CA cert, otherwise we will attempt to download RDS roots.
CACert: string(testCtx.hostCA.GetActiveKeys().TLS[0].Cert),
})
require.NoError(t, err)
testCtx.mysql[name] = testMySQL{
db: mysqlServer,
server: server,
db: mysqlServer,
resource: database,
}
return server
return database
}
}
func withCloudSQLMySQL(name, authUser, authToken string) withDatabaseOption {
return func(t *testing.T, ctx context.Context, testCtx *testContext) types.DatabaseServer {
return func(t *testing.T, ctx context.Context, testCtx *testContext) types.Database {
mysqlServer, err := mysql.NewTestServer(common.TestServerConfig{
Name: name,
AuthClient: testCtx.authClient,
@@ -980,34 +1011,30 @@ func withCloudSQLMySQL(name, authUser, authToken string) withDatabaseOption {
require.NoError(t, err)
go mysqlServer.Serve()
t.Cleanup(func() { mysqlServer.Close() })
server, err := types.NewDatabaseServerV3(name, nil,
types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolMySQL,
URI: net.JoinHostPort("localhost", mysqlServer.Port()),
Version: teleport.Version,
Hostname: constants.APIDomain,
HostID: testCtx.hostID,
DynamicLabels: dynamicLabels,
GCP: types.GCPCloudSQL{
ProjectID: "project-1",
InstanceID: "instance-1",
},
// Set CA cert to pass cert validation.
CACert: testCtx.hostCA.GetActiveKeys().TLS[0].Cert,
})
require.NoError(t, err)
_, err = testCtx.authClient.UpsertDatabaseServer(ctx, server)
database, err := types.NewDatabaseV3(types.Metadata{
Name: name,
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolMySQL,
URI: net.JoinHostPort("localhost", mysqlServer.Port()),
DynamicLabels: dynamicLabels,
GCP: types.GCPCloudSQL{
ProjectID: "project-1",
InstanceID: "instance-1",
},
// Set CA cert to pass cert validation.
CACert: string(testCtx.hostCA.GetActiveKeys().TLS[0].Cert),
})
require.NoError(t, err)
testCtx.mysql[name] = testMySQL{
db: mysqlServer,
server: server,
db: mysqlServer,
resource: database,
}
return server
return database
}
}
func withSelfHostedMongo(name string) withDatabaseOption {
return func(t *testing.T, ctx context.Context, testCtx *testContext) types.DatabaseServer {
return func(t *testing.T, ctx context.Context, testCtx *testContext) types.Database {
mongoServer, err := mongodb.NewTestServer(common.TestServerConfig{
Name: name,
AuthClient: testCtx.authClient,
@@ -1015,23 +1042,19 @@ func withSelfHostedMongo(name string) withDatabaseOption {
require.NoError(t, err)
go mongoServer.Serve()
t.Cleanup(func() { mongoServer.Close() })
server, err := types.NewDatabaseServerV3(name, nil,
types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolMongoDB,
URI: net.JoinHostPort("localhost", mongoServer.Port()),
Version: teleport.Version,
Hostname: constants.APIDomain,
HostID: testCtx.hostID,
DynamicLabels: dynamicLabels,
})
require.NoError(t, err)
_, err = testCtx.authClient.UpsertDatabaseServer(ctx, server)
database, err := types.NewDatabaseV3(types.Metadata{
Name: name,
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolMongoDB,
URI: net.JoinHostPort("localhost", mongoServer.Port()),
DynamicLabels: dynamicLabels,
})
require.NoError(t, err)
testCtx.mongo[name] = testMongoDB{
db: mongoServer,
server: server,
db: mongoServer,
resource: database,
}
return server
return database
}
}
+29 -29
View File
@@ -34,28 +34,28 @@ import (
// initCACert initializes the provided server's CA certificate in case of a
// cloud hosted database instance.
func (s *Server) initCACert(ctx context.Context, server types.DatabaseServer) error {
func (s *Server) initCACert(ctx context.Context, database types.Database) error {
// CA certificate may be set explicitly via configuration.
if len(server.GetCA()) != 0 {
if len(database.GetCA()) != 0 {
return nil
}
// Can only download it for cloud-hosted instances.
switch server.GetType() {
switch database.GetType() {
case types.DatabaseTypeRDS, types.DatabaseTypeRedshift, types.DatabaseTypeCloudSQL:
default:
return nil
}
// It's not set so download it or see if it's already downloaded.
bytes, err := s.getCACert(ctx, server)
bytes, err := s.getCACert(ctx, database)
if err != nil {
return trace.Wrap(err)
}
// Make sure the cert we got is valid just in case.
if _, err := tlsca.ParseCertificatePEM(bytes); err != nil {
return trace.Wrap(err, "CA certificate for %v doesn't appear to be a valid x509 certificate: %s",
server, bytes)
database, bytes)
}
server.SetCA(bytes)
database.SetCA(string(bytes))
return nil
}
@@ -64,9 +64,9 @@ func (s *Server) initCACert(ctx context.Context, server types.DatabaseServer) er
//
// The cert can already be cached in the filesystem, otherwise we will attempt
// to download it.
func (s *Server) getCACert(ctx context.Context, server types.DatabaseServer) ([]byte, error) {
func (s *Server) getCACert(ctx context.Context, database types.Database) ([]byte, error) {
// Auto-downloaded certs reside in the data directory.
filePath, err := s.getCACertPath(server)
filePath, err := s.getCACertPath(database)
if err != nil {
return nil, trace.Wrap(err)
}
@@ -81,8 +81,8 @@ func (s *Server) getCACert(ctx context.Context, server types.DatabaseServer) ([]
return ioutil.ReadFile(filePath)
}
// Otherwise download it.
s.log.Debugf("Downloading CA certificate for %v.", server)
bytes, err := s.cfg.CADownloader.Download(ctx, server)
s.log.Debugf("Downloading CA certificate for %v.", database)
bytes, err := s.cfg.CADownloader.Download(ctx, database)
if err != nil {
return nil, trace.Wrap(err)
}
@@ -97,25 +97,25 @@ func (s *Server) getCACert(ctx context.Context, server types.DatabaseServer) ([]
// getCACertPath returns the path where automatically downloaded root certificate
// for the provided database is stored in the filesystem.
func (s *Server) getCACertPath(server types.DatabaseServer) (string, error) {
func (s *Server) getCACertPath(database types.Database) (string, error) {
// All RDS and Redshift instances share the same root CA which can be
// downloaded from a well-known URL (sometimes region-specific). Each
// Cloud SQL instance has its own CA.
switch server.GetType() {
switch database.GetType() {
case types.DatabaseTypeRDS:
return filepath.Join(s.cfg.DataDir, filepath.Base(rdsCAURLForServer(server))), nil
return filepath.Join(s.cfg.DataDir, filepath.Base(rdsCAURLForDatabase(database))), nil
case types.DatabaseTypeRedshift:
return filepath.Join(s.cfg.DataDir, filepath.Base(redshiftCAURL)), nil
case types.DatabaseTypeCloudSQL:
return filepath.Join(s.cfg.DataDir, fmt.Sprintf("%v-root.pem", server.GetName())), nil
return filepath.Join(s.cfg.DataDir, fmt.Sprintf("%v-root.pem", database.GetName())), nil
}
return "", trace.BadParameter("%v doesn't support automatic CA download", server)
return "", trace.BadParameter("%v doesn't support automatic CA download", database)
}
// CADownloader defines interface for cloud databases CA cert downloaders.
type CADownloader interface {
// Download downloads CA certificate for the provided database instance.
Download(context.Context, types.DatabaseServer) ([]byte, error)
Download(context.Context, types.Database) ([]byte, error)
}
type realDownloader struct {
@@ -128,16 +128,16 @@ func NewRealDownloader(dataDir string) CADownloader {
}
// Download downloads CA certificate for the provided cloud database instance.
func (d *realDownloader) Download(ctx context.Context, server types.DatabaseServer) ([]byte, error) {
switch server.GetType() {
func (d *realDownloader) Download(ctx context.Context, database types.Database) ([]byte, error) {
switch database.GetType() {
case types.DatabaseTypeRDS:
return d.downloadFromURL(rdsCAURLForServer(server))
return d.downloadFromURL(rdsCAURLForDatabase(database))
case types.DatabaseTypeRedshift:
return d.downloadFromURL(redshiftCAURL)
case types.DatabaseTypeCloudSQL:
return d.downloadForCloudSQL(ctx, server)
return d.downloadForCloudSQL(ctx, database)
}
return nil, trace.BadParameter("%v doesn't support automatic CA download", server)
return nil, trace.BadParameter("%v doesn't support automatic CA download", database)
}
// downloadFromURL downloads root certificate from the provided URL.
@@ -163,28 +163,28 @@ func (d *realDownloader) downloadFromURL(downloadURL string) ([]byte, error) {
//
// This database service GCP IAM role should have "cloudsql.instances.get"
// permission in order for this to work.
func (d *realDownloader) downloadForCloudSQL(ctx context.Context, server types.DatabaseServer) ([]byte, error) {
func (d *realDownloader) downloadForCloudSQL(ctx context.Context, database types.Database) ([]byte, error) {
sqladminService, err := sqladmin.NewService(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
instance, err := sqladmin.NewInstancesService(sqladminService).Get(
server.GetGCP().ProjectID, server.GetGCP().InstanceID).Context(ctx).Do()
database.GetGCP().ProjectID, database.GetGCP().InstanceID).Context(ctx).Do()
if err != nil {
return nil, trace.BadParameter(cloudSQLDownloadError, server.GetName(),
err, server.GetGCP().InstanceID)
return nil, trace.BadParameter(cloudSQLDownloadError, database.GetName(),
err, database.GetGCP().InstanceID)
}
if instance.ServerCaCert != nil {
return []byte(instance.ServerCaCert.Cert), nil
}
return nil, trace.NotFound("Cloud SQL instance %v does not contain server CA certificate info: %v",
server, instance)
database, instance)
}
// rdsCAURLForServer returns root certificate download URL based on the region
// rdsCAURLForDatabase returns root certificate download URL based on the region
// of the provided RDS server instance.
func rdsCAURLForServer(server types.DatabaseServer) string {
if u, ok := rdsCAURLs[server.GetAWS().Region]; ok {
func rdsCAURLForDatabase(database types.Database) string {
if u, ok := rdsCAURLs[database.GetAWS().Region]; ok {
return u
}
return rdsDefaultCAURL
+87 -84
View File
@@ -20,7 +20,6 @@ import (
"context"
"testing"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/defaults"
@@ -34,108 +33,106 @@ func TestInitCACert(t *testing.T) {
ctx := context.Background()
testCtx := setupTestContext(ctx, t)
selfHosted, err := types.NewDatabaseServerV3("self-hosted", nil,
types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
Version: teleport.Version,
Hostname: constants.APIDomain,
HostID: "host-id",
})
selfHosted, err := types.NewDatabaseV3(types.Metadata{
Name: "self-hosted",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
})
require.NoError(t, err)
rds, err := types.NewDatabaseServerV3("rds", nil,
types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
Version: teleport.Version,
Hostname: constants.APIDomain,
HostID: "host-id",
AWS: types.AWS{Region: "us-east-1"},
})
rds, err := types.NewDatabaseV3(types.Metadata{
Name: "rds",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
AWS: types.AWS{Region: "us-east-1"},
})
require.NoError(t, err)
rdsWithCert, err := types.NewDatabaseServerV3("rds-with-cert", nil,
types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
Version: teleport.Version,
Hostname: constants.APIDomain,
HostID: "host-id",
AWS: types.AWS{Region: "us-east-1"},
CACert: []byte("rds-test-cert"),
})
rdsWithCert, err := types.NewDatabaseV3(types.Metadata{
Name: "rds-with-cert",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
AWS: types.AWS{Region: "us-east-1"},
CACert: "rds-test-cert",
})
require.NoError(t, err)
redshift, err := types.NewDatabaseServerV3("redshift", nil,
types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
Version: teleport.Version,
Hostname: constants.APIDomain,
HostID: "host-id",
AWS: types.AWS{Region: "us-east-1", Redshift: types.Redshift{ClusterID: "cluster-id"}},
})
redshift, err := types.NewDatabaseV3(types.Metadata{
Name: "redshift",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
AWS: types.AWS{Region: "us-east-1", Redshift: types.Redshift{ClusterID: "cluster-id"}},
})
require.NoError(t, err)
cloudSQL, err := types.NewDatabaseServerV3("cloud-sql", nil,
types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
Version: teleport.Version,
Hostname: constants.APIDomain,
HostID: "host-id",
GCP: types.GCPCloudSQL{ProjectID: "project-id", InstanceID: "instance-id"},
})
cloudSQL, err := types.NewDatabaseV3(types.Metadata{
Name: "cloud-sql",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
GCP: types.GCPCloudSQL{ProjectID: "project-id", InstanceID: "instance-id"},
})
require.NoError(t, err)
allServers := []types.DatabaseServer{
allDatabases := []*types.DatabaseV3{
selfHosted, rds, rdsWithCert, redshift, cloudSQL,
}
tests := []struct {
desc string
server string
cert []byte
desc string
database string
cert string
}{
{
desc: "shouldn't download self-hosted CA",
server: selfHosted.GetName(),
cert: selfHosted.GetCA(),
desc: "shouldn't download self-hosted CA",
database: selfHosted.GetName(),
cert: selfHosted.GetCA(),
},
{
desc: "should download RDS CA when it's not set",
server: rds.GetName(),
cert: []byte(fixtures.TLSCACertPEM),
desc: "should download RDS CA when it's not set",
database: rds.GetName(),
cert: fixtures.TLSCACertPEM,
},
{
desc: "shouldn't download RDS CA when it's set",
server: rdsWithCert.GetName(),
cert: rdsWithCert.GetCA(),
desc: "shouldn't download RDS CA when it's set",
database: rdsWithCert.GetName(),
cert: rdsWithCert.GetCA(),
},
{
desc: "should download Redshift CA when it's not set",
server: redshift.GetName(),
cert: []byte(fixtures.TLSCACertPEM),
desc: "should download Redshift CA when it's not set",
database: redshift.GetName(),
cert: fixtures.TLSCACertPEM,
},
{
desc: "should download Cloud SQL CA when it's not set",
server: cloudSQL.GetName(),
cert: []byte(fixtures.TLSCACertPEM),
desc: "should download Cloud SQL CA when it's not set",
database: cloudSQL.GetName(),
cert: fixtures.TLSCACertPEM,
},
}
databaseServer := testCtx.setupDatabaseServer(ctx, t, "host-id", allServers...)
resource, err := types.NewDatabaseServerV3(types.Metadata{
Name: testCtx.hostID,
}, types.DatabaseServerSpecV3{
HostID: testCtx.hostID,
Hostname: constants.APIDomain,
Databases: allDatabases,
})
require.NoError(t, err)
databaseServer := testCtx.setupDatabaseServer(ctx, t, resource)
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
var server types.DatabaseServer
for _, s := range databaseServer.cfg.Servers {
if s.GetName() == test.server {
server = s
var database types.Database
for _, db := range databaseServer.cfg.Server.GetDatabases() {
if db.GetName() == test.database {
database = db
}
}
require.NotNil(t, server)
require.Equal(t, test.cert, server.GetCA())
require.NotNil(t, database)
require.Equal(t, test.cert, database.GetCA())
})
}
}
@@ -146,21 +143,27 @@ func TestInitCACertCaching(t *testing.T) {
ctx := context.Background()
testCtx := setupTestContext(ctx, t)
rds, err := types.NewDatabaseServerV3("rds", nil,
types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
Version: teleport.Version,
Hostname: constants.APIDomain,
HostID: "host-id",
AWS: types.AWS{Region: "us-east-1"},
})
rds, err := types.NewDatabaseV3(types.Metadata{
Name: "rds",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
AWS: types.AWS{Region: "us-east-1"},
})
require.NoError(t, err)
databaseServer := testCtx.setupDatabaseServer(ctx, t, "host-id", rds)
resource, err := types.NewDatabaseServerV3(types.Metadata{
Name: testCtx.hostID,
}, types.DatabaseServerSpecV3{
HostID: testCtx.hostID,
Hostname: constants.APIDomain,
Databases: []*types.DatabaseV3{rds},
})
require.NoError(t, err)
databaseServer := testCtx.setupDatabaseServer(ctx, t, resource)
require.Equal(t, 1, databaseServer.cfg.CADownloader.(*fakeDownloader).count)
rds.SetCA(nil)
rds.SetCA("")
require.NoError(t, databaseServer.initCACert(ctx, rds))
require.Equal(t, 1, databaseServer.cfg.CADownloader.(*fakeDownloader).count)
}
@@ -172,7 +175,7 @@ type fakeDownloader struct {
count int
}
func (d *fakeDownloader) Download(context.Context, types.DatabaseServer) ([]byte, error) {
func (d *fakeDownloader) Download(context.Context, types.Database) ([]byte, error) {
d.count++
return d.cert, nil
}
+9 -9
View File
@@ -103,9 +103,9 @@ func (a *audit) OnSessionStart(ctx context.Context, session *Session, sessionErr
WithMFA: session.Identity.MFAVerified,
},
DatabaseMetadata: events.DatabaseMetadata{
DatabaseService: session.Server.GetName(),
DatabaseProtocol: session.Server.GetProtocol(),
DatabaseURI: session.Server.GetURI(),
DatabaseService: session.Database.GetName(),
DatabaseProtocol: session.Database.GetProtocol(),
DatabaseURI: session.Database.GetURI(),
DatabaseName: session.DatabaseName,
DatabaseUser: session.DatabaseUser,
},
@@ -143,9 +143,9 @@ func (a *audit) OnSessionEnd(ctx context.Context, session *Session) {
WithMFA: session.Identity.MFAVerified,
},
DatabaseMetadata: events.DatabaseMetadata{
DatabaseService: session.Server.GetName(),
DatabaseProtocol: session.Server.GetProtocol(),
DatabaseURI: session.Server.GetURI(),
DatabaseService: session.Database.GetName(),
DatabaseProtocol: session.Database.GetProtocol(),
DatabaseURI: session.Database.GetURI(),
DatabaseName: session.DatabaseName,
DatabaseUser: session.DatabaseUser,
},
@@ -173,9 +173,9 @@ func (a *audit) OnQuery(ctx context.Context, session *Session, query Query) {
WithMFA: session.Identity.MFAVerified,
},
DatabaseMetadata: events.DatabaseMetadata{
DatabaseService: session.Server.GetName(),
DatabaseProtocol: session.Server.GetProtocol(),
DatabaseURI: session.Server.GetURI(),
DatabaseService: session.Database.GetName(),
DatabaseProtocol: session.Database.GetProtocol(),
DatabaseURI: session.Database.GetURI(),
DatabaseName: database,
DatabaseUser: session.DatabaseUser,
},
+15 -15
View File
@@ -111,14 +111,14 @@ func NewAuth(config AuthConfig) (Auth, error) {
// GetRDSAuthToken returns authorization token that will be used as a password
// when connecting to RDS and Aurora databases.
func (a *dbAuth) GetRDSAuthToken(sessionCtx *Session) (string, error) {
awsSession, err := a.cfg.Clients.GetAWSSession(sessionCtx.Server.GetAWS().Region)
awsSession, err := a.cfg.Clients.GetAWSSession(sessionCtx.Database.GetAWS().Region)
if err != nil {
return "", trace.Wrap(err)
}
a.cfg.Log.Debugf("Generating RDS auth token for %s.", sessionCtx)
token, err := rdsutils.BuildAuthToken(
sessionCtx.Server.GetURI(),
sessionCtx.Server.GetAWS().Region,
sessionCtx.Database.GetURI(),
sessionCtx.Database.GetAWS().Region,
sessionCtx.DatabaseUser,
awsSession.Config.Credentials)
if err != nil {
@@ -130,7 +130,7 @@ Make sure that Teleport database agent's IAM policy is attached and has "rds-con
permissions:
%v
`, err, GetRDSPolicy(sessionCtx.Server.GetAWS().Region))
`, err, GetRDSPolicy(sessionCtx.Database.GetAWS().Region))
}
return token, nil
}
@@ -138,13 +138,13 @@ permissions:
// GetRedshiftAuthToken returns authorization token that will be used as a
// password when connecting to Redshift databases.
func (a *dbAuth) GetRedshiftAuthToken(sessionCtx *Session) (string, string, error) {
awsSession, err := a.cfg.Clients.GetAWSSession(sessionCtx.Server.GetAWS().Region)
awsSession, err := a.cfg.Clients.GetAWSSession(sessionCtx.Database.GetAWS().Region)
if err != nil {
return "", "", trace.Wrap(err)
}
a.cfg.Log.Debugf("Generating Redshift auth token for %s.", sessionCtx)
resp, err := redshift.New(awsSession).GetClusterCredentials(&redshift.GetClusterCredentialsInput{
ClusterIdentifier: aws.String(sessionCtx.Server.GetAWS().Redshift.ClusterID),
ClusterIdentifier: aws.String(sessionCtx.Database.GetAWS().Redshift.ClusterID),
DbUser: aws.String(sessionCtx.DatabaseUser),
DbName: aws.String(sessionCtx.DatabaseName),
// TODO(r0mant): Do not auto-create database account if DbUser doesn't
@@ -248,8 +248,8 @@ func (a *dbAuth) GetCloudSQLPassword(ctx context.Context, sessionCtx *Session) (
// updateCloudSQLUser makes a request to Cloud SQL API to update the provided user.
func (a *dbAuth) updateCloudSQLUser(ctx context.Context, sessionCtx *Session, gcpCloudSQL *sqladmin.Service, user *sqladmin.User) error {
_, err := gcpCloudSQL.Users.Update(
sessionCtx.Server.GetGCP().ProjectID,
sessionCtx.Server.GetGCP().InstanceID,
sessionCtx.Database.GetGCP().ProjectID,
sessionCtx.Database.GetGCP().InstanceID,
user).Name(sessionCtx.DatabaseUser).Host("%").Context(ctx).Do()
if err != nil {
return trace.AccessDenied(`Could not update Cloud SQL user %q password:
@@ -276,8 +276,8 @@ func (a *dbAuth) GetTLSConfig(ctx context.Context, sessionCtx *Session) (*tls.Co
// of replica set the driver may dial multiple servers and will set
// ServerName itself. For Postgres/MySQL we're always connecting to the
// server specified in URI so set ServerName ourselves.
if sessionCtx.Server.GetProtocol() != defaults.ProtocolMongoDB {
addr, err := utils.ParseAddr(sessionCtx.Server.GetURI())
if sessionCtx.Database.GetProtocol() != defaults.ProtocolMongoDB {
addr, err := utils.ParseAddr(sessionCtx.Database.GetURI())
if err != nil {
return nil, trace.Wrap(err)
}
@@ -285,8 +285,8 @@ func (a *dbAuth) GetTLSConfig(ctx context.Context, sessionCtx *Session) (*tls.Co
}
// Add CA certificate to the trusted pool if it's present, e.g. when
// connecting to RDS/Aurora which require AWS CA.
if len(sessionCtx.Server.GetCA()) != 0 {
if !tlsConfig.RootCAs.AppendCertsFromPEM(sessionCtx.Server.GetCA()) {
if len(sessionCtx.Database.GetCA()) != 0 {
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(sessionCtx.Database.GetCA())) {
return nil, trace.BadParameter("invalid server CA certificate")
}
}
@@ -308,11 +308,11 @@ func (a *dbAuth) GetTLSConfig(ctx context.Context, sessionCtx *Session) (*tls.Co
//
// See the following Go issue for more context:
// https://github.com/golang/go/issues/40748
if sessionCtx.Server.IsCloudSQL() {
if sessionCtx.Database.IsCloudSQL() {
// Cloud SQL server presented certificates encode instance names as
// "<project-id>:<instance-id>" in CommonName. This is verified against
// the ServerName in a custom connection verification step (see below).
tlsConfig.ServerName = fmt.Sprintf("%v:%v", sessionCtx.Server.GetGCP().ProjectID, sessionCtx.Server.GetGCP().InstanceID)
tlsConfig.ServerName = fmt.Sprintf("%v:%v", sessionCtx.Database.GetGCP().ProjectID, sessionCtx.Database.GetGCP().InstanceID)
// This just disables default verification.
tlsConfig.InsecureSkipVerify = true
// This will verify CN and cert chain on each connection.
@@ -320,7 +320,7 @@ func (a *dbAuth) GetTLSConfig(ctx context.Context, sessionCtx *Session) (*tls.Co
}
// RDS/Aurora/Redshift and Cloud SQL auth is done with an auth token so
// don't generate a client certificate and exit here.
if sessionCtx.Server.IsRDS() || sessionCtx.Server.IsRedshift() || sessionCtx.Server.IsCloudSQL() {
if sessionCtx.Database.IsRDS() || sessionCtx.Database.IsRedshift() || sessionCtx.Database.IsCloudSQL() {
return tlsConfig, nil
}
// Otherwise, when connecting to an onprem database, generate a client
+3 -1
View File
@@ -34,6 +34,8 @@ type Session struct {
ClusterName string
// Server is the database server handling the connection.
Server types.DatabaseServer
// Database is the database user is connecting to.
Database types.Database
// Identity is the identity of the connecting Teleport user.
Identity tlsca.Identity
// Checker is the access checker for the identity.
@@ -53,5 +55,5 @@ type Session struct {
// String returns string representation of the session parameters.
func (c *Session) String() string {
return fmt.Sprintf("db[%v] identity[%v] dbUser[%v] dbName[%v]",
c.Server.GetName(), c.Identity.Username, c.DatabaseUser, c.DatabaseName)
c.Database.GetName(), c.Identity.Username, c.DatabaseUser, c.DatabaseName)
}
+28 -20
View File
@@ -22,7 +22,6 @@ import (
"net"
"testing"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/defaults"
@@ -49,35 +48,44 @@ func TestHA(t *testing.T) {
// Offline database server will be tried first and trigger connection error.
offlineHostID := "host-id-1"
offlineDBServer, err := types.NewDatabaseServerV3("postgres", nil,
types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: net.JoinHostPort("localhost", postgresServer.Port()),
Version: teleport.Version,
Hostname: constants.APIDomain,
HostID: offlineHostID,
})
offlineDB, err := types.NewDatabaseV3(types.Metadata{
Name: "postgres",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: net.JoinHostPort("localhost", postgresServer.Port()),
})
require.NoError(t, err)
_, err = testCtx.authClient.UpsertDatabaseServer(ctx, offlineDBServer)
offlineResource, err := types.NewDatabaseServerV3(types.Metadata{
Name: offlineHostID,
}, types.DatabaseServerSpecV3{
HostID: offlineHostID,
Hostname: constants.APIDomain,
Databases: []*types.DatabaseV3{offlineDB},
})
require.NoError(t, err)
testCtx.setupDatabaseServer(ctx, t, offlineResource)
testCtx.fakeRemoteSite.OfflineTunnels = map[string]struct{}{
fmt.Sprintf("%v.%v", offlineHostID, testCtx.clusterName): {},
}
// Online database server will serve the connection.
onlineHostID := "host-id-2"
onlineDBServer, err := types.NewDatabaseServerV3("postgres", nil,
types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: net.JoinHostPort("localhost", postgresServer.Port()),
Version: teleport.Version,
Hostname: constants.APIDomain,
HostID: onlineHostID,
})
onlineDB, err := types.NewDatabaseV3(types.Metadata{
Name: "postgres",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: net.JoinHostPort("localhost", postgresServer.Port()),
})
require.NoError(t, err)
_, err = testCtx.authClient.UpsertDatabaseServer(ctx, onlineDBServer)
onlineResource, err := types.NewDatabaseServerV3(types.Metadata{
Name: onlineHostID,
}, types.DatabaseServerSpecV3{
HostID: onlineHostID,
Hostname: constants.APIDomain,
Databases: []*types.DatabaseV3{onlineDB},
})
require.NoError(t, err)
onlineServer := testCtx.setupDatabaseServer(ctx, t, onlineHostID, onlineDBServer)
onlineServer := testCtx.setupDatabaseServer(ctx, t, onlineResource)
go func() {
for conn := range testCtx.proxyConn {
go onlineServer.HandleConnection(conn)
+3 -3
View File
@@ -147,15 +147,15 @@ func (e *Engine) getConnectionOptions(ctx context.Context, sessionCtx *common.Se
// getConnectionString returns connection string for the server.
func getConnectionString(sessionCtx *common.Session) (connstring.ConnString, error) {
uri, err := url.Parse(sessionCtx.Server.GetURI())
uri, err := url.Parse(sessionCtx.Database.GetURI())
if err != nil {
return connstring.ConnString{}, trace.Wrap(err)
}
switch uri.Scheme {
case connstring.SchemeMongoDB, connstring.SchemeMongoDBSRV:
return connstring.ParseAndValidate(sessionCtx.Server.GetURI())
return connstring.ParseAndValidate(sessionCtx.Database.GetURI())
}
return connstring.ConnString{Hosts: []string{sessionCtx.Server.GetURI()}}, nil
return connstring.ConnString{Hosts: []string{sessionCtx.Database.GetURI()}}, nil
}
// getServerSelector returns selector for picking the server to connect to,
+4 -4
View File
@@ -160,8 +160,8 @@ func (e *Engine) authorizeConnection(ctx context.Context, sessionCtx *common.Ses
// Only the username is checked upon initial connection. MongoDB sends
// database name with each protocol message (for query, update, etc.)
// so it is checked when we receive a message from client.
err = sessionCtx.Checker.CheckAccessToDatabase(sessionCtx.Server, mfaParams,
&services.DatabaseLabelsMatcher{Labels: sessionCtx.Server.GetAllLabels()},
err = sessionCtx.Checker.CheckAccessToDatabase(sessionCtx.Database, mfaParams,
&services.DatabaseLabelsMatcher{Labels: sessionCtx.Database.GetAllLabels()},
&services.DatabaseUserMatcher{User: sessionCtx.DatabaseUser})
if err != nil {
e.Audit.OnSessionStart(e.Context, sessionCtx, err)
@@ -186,9 +186,9 @@ func (e *Engine) authorizeClientMessage(sessionCtx *common.Session, message prot
e.Log.Warnf("No database info in message: %v.", message)
return nil
}
err := sessionCtx.Checker.CheckAccessToDatabase(sessionCtx.Server,
err := sessionCtx.Checker.CheckAccessToDatabase(sessionCtx.Database,
services.AccessMFAParams{Verified: true},
&services.DatabaseLabelsMatcher{Labels: sessionCtx.Server.GetAllLabels()},
&services.DatabaseLabelsMatcher{Labels: sessionCtx.Database.GetAllLabels()},
&services.DatabaseUserMatcher{User: sessionCtx.DatabaseUser},
&services.DatabaseNameMatcher{Name: database})
e.Audit.OnQuery(e.Context, sessionCtx, common.Query{
+8 -8
View File
@@ -138,8 +138,8 @@ func (e *Engine) checkAccess(ctx context.Context, sessionCtx *common.Session) er
// on queries, we might be able to restrict db_names as well e.g. by
// detecting full-qualified table names like db.table, until then the
// proper way is to use MySQL grants system.
err = sessionCtx.Checker.CheckAccessToDatabase(sessionCtx.Server, mfaParams,
&services.DatabaseLabelsMatcher{Labels: sessionCtx.Server.GetAllLabels()},
err = sessionCtx.Checker.CheckAccessToDatabase(sessionCtx.Database, mfaParams,
&services.DatabaseLabelsMatcher{Labels: sessionCtx.Database.GetAllLabels()},
&services.DatabaseUserMatcher{User: sessionCtx.DatabaseUser})
if err != nil {
e.Audit.OnSessionStart(e.Context, sessionCtx, err)
@@ -156,12 +156,12 @@ func (e *Engine) connect(ctx context.Context, sessionCtx *common.Session) (*clie
}
var password string
switch {
case sessionCtx.Server.IsRDS():
case sessionCtx.Database.IsRDS():
password, err = e.Auth.GetRDSAuthToken(sessionCtx)
if err != nil {
return nil, trace.Wrap(err)
}
case sessionCtx.Server.IsCloudSQL():
case sessionCtx.Database.IsCloudSQL():
// For Cloud SQL MySQL there is no IAM auth so we use one-time passwords
// by resetting the database user password for each connection. Thus,
// acquire a lock to make sure all connection attempts to the same
@@ -187,7 +187,7 @@ func (e *Engine) connect(ctx context.Context, sessionCtx *common.Session) (*clie
}
}
// TODO(r0mant): Set CLIENT_INTERACTIVE flag on the client?
conn, err := client.Connect(sessionCtx.Server.GetURI(),
conn, err := client.Connect(sessionCtx.Database.GetURI(),
sessionCtx.DatabaseUser,
password,
sessionCtx.DatabaseName,
@@ -195,7 +195,7 @@ func (e *Engine) connect(ctx context.Context, sessionCtx *common.Session) (*clie
conn.SetTLSConfig(tlsConfig)
})
if err != nil {
if trace.IsAccessDenied(common.ConvertError(err)) && sessionCtx.Server.IsRDS() {
if trace.IsAccessDenied(common.ConvertError(err)) && sessionCtx.Database.IsRDS() {
return nil, trace.AccessDenied(`Could not connect to database:
%v
@@ -204,7 +204,7 @@ Make sure that IAM auth is enabled for MySQL user %q and Teleport database
agent's IAM policy has "rds-connect" permissions:
%v
`, common.ConvertError(err), sessionCtx.DatabaseUser, common.GetRDSPolicy(sessionCtx.Server.GetAWS().Region))
`, common.ConvertError(err), sessionCtx.DatabaseUser, common.GetRDSPolicy(sessionCtx.Database.GetAWS().Region))
}
return nil, trace.Wrap(err)
}
@@ -291,7 +291,7 @@ func (e *Engine) makeAcquireSemaphoreConfig(sessionCtx *common.Session) services
// in a minute anyway.
Request: types.AcquireSemaphoreRequest{
SemaphoreKind: "gcp-mysql-token",
SemaphoreName: fmt.Sprintf("%v-%v", sessionCtx.Server.GetName(), sessionCtx.DatabaseUser),
SemaphoreName: fmt.Sprintf("%v-%v", sessionCtx.Database.GetName(), sessionCtx.DatabaseUser),
MaxLeases: 1,
Expires: e.Clock.Now().Add(time.Minute),
},
+6 -6
View File
@@ -179,8 +179,8 @@ func (e *Engine) checkAccess(ctx context.Context, sessionCtx *common.Session) er
Verified: sessionCtx.Identity.MFAVerified != "",
AlwaysRequired: ap.GetRequireSessionMFA(),
}
err = sessionCtx.Checker.CheckAccessToDatabase(sessionCtx.Server, mfaParams,
&services.DatabaseLabelsMatcher{Labels: sessionCtx.Server.GetAllLabels()},
err = sessionCtx.Checker.CheckAccessToDatabase(sessionCtx.Database, mfaParams,
&services.DatabaseLabelsMatcher{Labels: sessionCtx.Database.GetAllLabels()},
&services.DatabaseUserMatcher{User: sessionCtx.DatabaseUser},
&services.DatabaseNameMatcher{Name: sessionCtx.DatabaseName})
if err != nil {
@@ -204,7 +204,7 @@ func (e *Engine) connect(ctx context.Context, sessionCtx *common.Session) (*pgpr
// messages b/w server and client e.g. to get client's password.
conn, err := pgconn.ConnectConfig(ctx, connectConfig)
if err != nil {
if trace.IsAccessDenied(common.ConvertError(err)) && sessionCtx.Server.IsRDS() {
if trace.IsAccessDenied(common.ConvertError(err)) && sessionCtx.Database.IsRDS() {
return nil, nil, trace.AccessDenied(`Could not connect to database:
%v
@@ -213,7 +213,7 @@ Make sure that Postgres user %q has "rds_iam" role and Teleport database
agent's IAM policy has "rds-connect" permissions:
%v
`, common.ConvertError(err), sessionCtx.DatabaseUser, common.GetRDSPolicy(sessionCtx.Server.GetAWS().Region))
`, common.ConvertError(err), sessionCtx.DatabaseUser, common.GetRDSPolicy(sessionCtx.Database.GetAWS().Region))
}
return nil, nil, trace.Wrap(err)
}
@@ -369,7 +369,7 @@ func (e *Engine) getConnectConfig(ctx context.Context, sessionCtx *common.Sessio
// string so parse the basic template and then fill in the rest of
// parameters such as TLS configuration.
config, err := pgconn.ParseConfig(fmt.Sprintf("postgres://%s@%s/?database=%s",
sessionCtx.DatabaseUser, sessionCtx.Server.GetURI(), sessionCtx.DatabaseName))
sessionCtx.DatabaseUser, sessionCtx.Database.GetURI(), sessionCtx.DatabaseName))
if err != nil {
return nil, trace.Wrap(err)
}
@@ -381,7 +381,7 @@ func (e *Engine) getConnectConfig(ctx context.Context, sessionCtx *common.Sessio
config.RuntimeParams = sessionCtx.StartupParameters
// AWS RDS/Aurora and GCP Cloud SQL use IAM authentication so request an
// auth token and use it as a password.
switch sessionCtx.Server.GetType() {
switch sessionCtx.Database.GetType() {
case types.DatabaseTypeRDS:
config.Password, err = e.Auth.GetRDSAuthToken(sessionCtx)
if err != nil {
+6 -4
View File
@@ -498,19 +498,21 @@ func (s *ProxyServer) getDatabaseServers(ctx context.Context, identity tlsca.Ide
if err != nil {
return nil, nil, trace.Wrap(err)
}
s.log.Debugf("Available database servers on %v: %s.", cluster.GetName(), servers)
s.log.Debugf("Available databases in %v: %s.", cluster.GetName(), servers)
// Find out which database servers proxy the database a user is
// connecting to using routing information from identity.
var result []types.DatabaseServer
for _, server := range servers {
if server.GetName() == identity.RouteToDatabase.ServiceName {
result = append(result, server)
for _, database := range server.GetDatabases() {
if database.GetName() == identity.RouteToDatabase.ServiceName {
result = append(result, server)
}
}
}
if len(result) != 0 {
return cluster, result, nil
}
return nil, nil, trace.NotFound("database %q not found among registered database servers on cluster %q",
return nil, nil, trace.NotFound("database %q not found among registered databases in cluster %q",
identity.RouteToDatabase.ServiceName,
identity.RouteToCluster)
}
+44 -35
View File
@@ -63,8 +63,8 @@ type Config struct {
Authorizer auth.Authorizer
// GetRotation returns the certificate rotation state.
GetRotation func(role types.SystemRole) (*types.Rotation, error)
// Servers contains a list of database servers this service proxies.
Servers types.DatabaseServers
// Server is the resource representing this database server.
Server types.DatabaseServer
// OnHeartbeat is called after every heartbeat. Used to update process state.
OnHeartbeat func(error)
// Auth is responsible for generating database auth tokens.
@@ -117,8 +117,8 @@ func (c *Config) CheckAndSetDefaults(ctx context.Context) (err error) {
if c.GetRotation == nil {
return trace.BadParameter("missing GetRotation")
}
if len(c.Servers) == 0 {
return trace.BadParameter("missing Servers")
if c.Server == nil {
return trace.BadParameter("missing Server")
}
if c.CADownloader == nil {
c.CADownloader = NewRealDownloader(c.DataDir)
@@ -140,7 +140,7 @@ type Server struct {
closeFunc context.CancelFunc
// middleware extracts identity from client certificates.
middleware *auth.Middleware
// dynamicLabels contains dynamic labels for database servers.
// dynamicLabels contains dynamic labels for databases.
dynamicLabels map[string]*labels.Dynamic
// heartbeats holds hearbeats for database servers.
heartbeats map[string]*srv.Heartbeat
@@ -176,10 +176,16 @@ func New(ctx context.Context, config Config) (*Server, error) {
server.cfg.TLSConfig.GetConfigForClient = getConfigForClient(
server.cfg.TLSConfig, server.cfg.AccessPoint, server.log)
// Initialize the heartbeat which will periodically register
// this server with the auth server.
if err := server.initHeartbeat(ctx, server.cfg.Server); err != nil {
return nil, trace.Wrap(err)
}
// Perform various initialization actions on each proxied database, like
// starting up dynamic labels and loading root certs for RDS dbs.
for _, db := range server.cfg.Servers {
if err := server.initDatabaseServer(ctx, db); err != nil {
for _, db := range server.cfg.Server.GetDatabases() {
if err := server.initDatabase(ctx, db); err != nil {
return nil, trace.Wrap(err, "failed to initialize %v", server)
}
}
@@ -187,33 +193,30 @@ func New(ctx context.Context, config Config) (*Server, error) {
return server, nil
}
func (s *Server) initDatabaseServer(ctx context.Context, server types.DatabaseServer) error {
if err := s.initDynamicLabels(ctx, server); err != nil {
func (s *Server) initDatabase(ctx context.Context, database types.Database) error {
if err := s.initDynamicLabels(ctx, database); err != nil {
return trace.Wrap(err)
}
if err := s.initHeartbeat(ctx, server); err != nil {
if err := s.initCACert(ctx, database); err != nil {
return trace.Wrap(err)
}
if err := s.initCACert(ctx, server); err != nil {
return trace.Wrap(err)
}
s.log.Debugf("Initialized %v.", server)
s.log.Debugf("Initialized %v.", database)
return nil
}
func (s *Server) initDynamicLabels(ctx context.Context, server types.DatabaseServer) error {
if len(server.GetDynamicLabels()) == 0 {
func (s *Server) initDynamicLabels(ctx context.Context, database types.Database) error {
if len(database.GetDynamicLabels()) == 0 {
return nil // Nothing to do.
}
dynamic, err := labels.NewDynamic(ctx, &labels.DynamicConfig{
Labels: server.GetDynamicLabels(),
Labels: database.GetDynamicLabels(),
Log: s.log,
})
if err != nil {
return trace.Wrap(err)
}
dynamic.Sync()
s.dynamicLabels[server.GetName()] = dynamic
s.dynamicLabels[database.GetName()] = dynamic
return nil
}
@@ -245,10 +248,15 @@ func (s *Server) getServerInfoFunc(server types.DatabaseServer) func() (types.Re
server = server.Copy()
s.mu.RUnlock()
// Update dynamic labels.
labels, ok := s.dynamicLabels[server.GetName()]
if ok {
server.SetDynamicLabels(labels.Get())
databases := make([]types.Database, 0, len(server.GetDatabases()))
for _, database := range server.GetDatabases() {
labels, ok := s.dynamicLabels[database.GetName()]
if ok {
database.SetDynamicLabels(labels.Get())
}
databases = append(databases, database)
}
server.SetDatabases(databases)
// Update CA rotation state.
rotation, err := s.cfg.GetRotation(types.RoleDatabase)
if err != nil && !trace.IsNotFound(err) {
@@ -371,7 +379,7 @@ func (s *Server) handleConnection(ctx context.Context, conn net.Conn) error {
identity: sessionCtx.Identity,
checker: sessionCtx.Checker,
clock: s.cfg.Clock,
serverID: sessionCtx.Server.GetHostID(),
serverID: s.cfg.Server.GetHostID(),
authClient: s.cfg.AuthClient,
teleportUser: sessionCtx.Identity.Username,
emitter: s.cfg.AuthClient,
@@ -397,7 +405,7 @@ func (s *Server) dispatch(sessionCtx *common.Session, streamWriter events.Stream
if err != nil {
return nil, trace.Wrap(err)
}
switch sessionCtx.Server.GetProtocol() {
switch sessionCtx.Database.GetProtocol() {
case defaults.ProtocolPostgres:
return &postgres.Engine{
Auth: s.cfg.Auth,
@@ -425,7 +433,7 @@ func (s *Server) dispatch(sessionCtx *common.Session, streamWriter events.Stream
}, nil
}
return nil, trace.BadParameter("unsupported database protocol %q",
sessionCtx.Server.GetProtocol())
sessionCtx.Database.GetProtocol())
}
func (s *Server) authorize(ctx context.Context) (*common.Session, error) {
@@ -444,23 +452,24 @@ func (s *Server) authorize(ctx context.Context) (*common.Session, error) {
identity := authContext.Identity.GetIdentity()
s.log.Debugf("Client identity: %#v.", identity)
// Fetch the requested database server.
var server types.DatabaseServer
for _, s := range s.cfg.Servers {
if s.GetName() == identity.RouteToDatabase.ServiceName {
server = s
var database types.Database
for _, db := range s.cfg.Server.GetDatabases() {
if db.GetName() == identity.RouteToDatabase.ServiceName {
database = db
}
}
if server == nil {
return nil, trace.NotFound("%q not found among registered database servers: %v",
identity.RouteToDatabase.ServiceName, s.cfg.Servers)
if database == nil {
return nil, trace.NotFound("%q not found among registered databases: %v",
identity.RouteToDatabase.ServiceName, s.cfg.Server.GetDatabases())
}
s.log.Debugf("Will connect to database %q at %v.", server.GetName(),
server.GetURI())
s.log.Debugf("Will connect to database %q at %v.", database.GetName(),
database.GetURI())
id := uuid.New()
return &common.Session{
ID: id,
ClusterName: identity.RouteToCluster,
Server: server,
Server: s.cfg.Server,
Database: database,
Identity: identity,
DatabaseUser: identity.RouteToDatabase.Username,
DatabaseName: identity.RouteToDatabase.Database,
@@ -469,7 +478,7 @@ func (s *Server) authorize(ctx context.Context) (*common.Session, error) {
Statements: common.NewStatementsCache(),
Log: s.log.WithFields(logrus.Fields{
"id": id,
"db": server.GetName(),
"db": database.GetName(),
}),
}, nil
}
+15 -13
View File
@@ -40,35 +40,37 @@ func TestDatabaseServerStart(t *testing.T) {
require.NoError(t, err)
tests := []struct {
server types.DatabaseServer
database types.Database
}{
{
server: testCtx.postgres["postgres"].server,
database: testCtx.postgres["postgres"].resource,
},
{
server: testCtx.mysql["mysql"].server,
database: testCtx.mysql["mysql"].resource,
},
{
server: testCtx.mongo["mongo"].server,
database: testCtx.mongo["mongo"].resource,
},
}
for _, test := range tests {
labels, ok := testCtx.server.dynamicLabels[test.server.GetName()]
labels, ok := testCtx.server.dynamicLabels[test.database.GetName()]
require.True(t, ok)
require.Equal(t, "test", labels.Get()["echo"].GetResult())
heartbeat, ok := testCtx.server.heartbeats[test.server.GetName()]
require.True(t, ok)
err = heartbeat.ForceSend(time.Second)
require.NoError(t, err)
}
heartbeat, ok := testCtx.server.heartbeats[testCtx.server.cfg.Server.GetName()]
require.True(t, ok)
err = heartbeat.ForceSend(time.Second)
require.NoError(t, err)
// Make sure servers were announced and their labels updated.
servers, err := testCtx.authClient.GetDatabaseServers(ctx, apidefaults.Namespace)
require.NoError(t, err)
for _, server := range servers {
require.Equal(t, map[string]string{"echo": "test"}, server.GetAllLabels())
require.Len(t, servers, 1)
require.Len(t, servers[0].GetDatabases(), 3)
for _, database := range servers[0].GetDatabases() {
require.Equal(t, map[string]string{"echo": "test"}, database.GetAllLabels())
}
}
+4 -1
View File
@@ -1995,7 +1995,10 @@ func TestClusterDatabasesGet(t *testing.T) {
require.Len(t, dbs, 0)
// Register a database.
db, err := types.NewDatabaseServerV3("test-db-name", map[string]string{"test-field": "test-value"}, types.DatabaseServerSpecV3{
db, err := types.NewDatabaseServerV3(types.Metadata{
Name: "test-db-name",
Labels: map[string]string{"test-field": "test-value"},
}, types.DatabaseServerSpecV3{
Description: "test-description",
Protocol: "test-protocol",
URI: "test-uri",
+10 -2
View File
@@ -20,8 +20,10 @@ import (
"net/http"
apidefaults "github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/reversetunnel"
"github.com/gravitational/teleport/lib/web/ui"
"github.com/gravitational/trace"
"github.com/julienschmidt/httprouter"
)
@@ -50,10 +52,16 @@ func (h *Handler) clusterDatabasesGet(w http.ResponseWriter, r *http.Request, p
}
// Get a list of database servers.
dbServers, err := clt.GetDatabaseServers(r.Context(), apidefaults.Namespace)
servers, err := clt.GetDatabaseServers(r.Context(), apidefaults.Namespace)
if err != nil {
return nil, trace.Wrap(err)
}
return ui.MakeDatabases(h.auth.clusterName, dbServers), nil
// Make a list of all proxied databases.
var databases []types.Database
for _, server := range servers {
databases = append(databases, server.GetDatabases()...)
}
return ui.MakeDatabases(h.auth.clusterName, types.DeduplicateDatabases(databases)), nil
}
+10 -10
View File
@@ -160,20 +160,20 @@ type Database struct {
Labels []Label `json:"labels"`
}
// MakeDatabases creates server database objects.
func MakeDatabases(clusterName string, servers []types.DatabaseServer) []Database {
uiServers := make([]Database, 0, len(servers))
for _, server := range servers {
// MakeDatabases creates database objects.
func MakeDatabases(clusterName string, databases []types.Database) []Database {
uiServers := make([]Database, 0, len(databases))
for _, database := range databases {
uiLabels := []Label{}
for name, value := range server.GetStaticLabels() {
for name, value := range database.GetStaticLabels() {
uiLabels = append(uiLabels, Label{
Name: name,
Value: value,
})
}
for name, cmd := range server.GetDynamicLabels() {
for name, cmd := range database.GetDynamicLabels() {
uiLabels = append(uiLabels, Label{
Name: name,
Value: cmd.GetResult(),
@@ -183,10 +183,10 @@ func MakeDatabases(clusterName string, servers []types.DatabaseServer) []Databas
sort.Sort(sortedLabels(uiLabels))
uiServers = append(uiServers, Database{
Name: server.GetName(),
Desc: server.GetDescription(),
Protocol: server.GetProtocol(),
Type: server.GetType(),
Name: database.GetName(),
Desc: database.GetDescription(),
Protocol: database.GetProtocol(),
Type: database.GetType(),
Labels: uiLabels,
})
}
+6 -4
View File
@@ -579,11 +579,13 @@ func (c *dbCollection) resources() (r []types.Resource) {
}
func (c *dbCollection) writeText(w io.Writer) error {
t := asciitable.MakeTable([]string{"Name", "Protocol", "Address", "Labels"})
t := asciitable.MakeTable([]string{"Name", "Protocol", "URI", "Labels", "Hostname", "Version"})
for _, server := range c.servers {
t.AddRow([]string{
server.GetName(), server.GetProtocol(), server.GetURI(), server.LabelsString(),
})
for _, database := range server.GetDatabases() {
t.AddRow([]string{
database.GetName(), database.GetProtocol(), database.GetURI(), database.LabelsString(), server.GetHostname(), server.GetTeleportVersion(),
})
}
}
_, err := t.AsBuffer().WriteTo(w)
return trace.Wrap(err)
+18 -13
View File
@@ -17,6 +17,7 @@ limitations under the License.
package common
import (
"fmt"
"testing"
"github.com/gravitational/trace"
@@ -71,31 +72,35 @@ func TestDatabaseResource(t *testing.T) {
var out []*types.DatabaseServerV3
t.Run("get all databases", func(t *testing.T) {
buff, err := runResourceCommand(t, fileConfig, []string{"get", "db", "--format=json"})
require.NoError(t, err)
mustDecodeJSON(t, buff, &out)
require.Len(t, out, 2)
})
t.Run("get example database", func(t *testing.T) {
buff, err := runResourceCommand(t, fileConfig, []string{"get", "db/example", "--format=json"})
t.Run("get all database servers", func(t *testing.T) {
buff, err := runResourceCommand(t, fileConfig, []string{"get", types.KindDatabaseServer, "--format=json"})
require.NoError(t, err)
mustDecodeJSON(t, buff, &out)
require.Len(t, out, 1)
require.Len(t, out[0].GetDatabases(), 2)
})
t.Run("remove example2 database", func(t *testing.T) {
_, err := runResourceCommand(t, fileConfig, []string{"rm", "db/example2"})
server := fmt.Sprintf("%v/%v", types.KindDatabaseServer, out[0].GetName())
t.Run("get specific database server", func(t *testing.T) {
buff, err := runResourceCommand(t, fileConfig, []string{"get", server, "--format=json"})
require.NoError(t, err)
mustDecodeJSON(t, buff, &out)
require.Len(t, out, 1)
require.Len(t, out[0].GetDatabases(), 2)
})
t.Run("remove database server", func(t *testing.T) {
_, err := runResourceCommand(t, fileConfig, []string{"rm", server})
require.NoError(t, err)
_, err = runResourceCommand(t, fileConfig, []string{"get", "db/example2", "--format=json"})
_, err = runResourceCommand(t, fileConfig, []string{"get", server, "--format=json"})
require.Error(t, err)
require.IsType(t, &trace.NotFoundError{}, err.(*trace.TraceErr).OrigError())
buff, err := runResourceCommand(t, fileConfig, []string{"get", "db", "--format=json"})
require.NoError(t, err)
mustDecodeJSON(t, buff, &out)
require.Len(t, out, 1)
require.Len(t, out, 0)
})
}
+12 -13
View File
@@ -42,9 +42,9 @@ func onListDatabases(cf *CLIConf) error {
if err != nil {
return trace.Wrap(err)
}
var servers []types.DatabaseServer
var databases []types.Database
err = client.RetryWithRelogin(cf.Context, tc, func() error {
servers, err = tc.ListDatabaseServers(cf.Context)
databases, err = tc.ListDatabases(cf.Context)
return trace.Wrap(err)
})
if err != nil {
@@ -60,11 +60,10 @@ func onListDatabases(cf *CLIConf) error {
if err != nil {
return trace.Wrap(err)
}
sort.Slice(servers, func(i, j int) bool {
return servers[i].GetName() < servers[j].GetName()
sort.Slice(databases, func(i, j int) bool {
return databases[i].GetName() < databases[j].GetName()
})
showDatabases(tc.SiteName, types.DeduplicateDatabaseServers(servers),
profile.Databases, cf.Verbose)
showDatabases(tc.SiteName, databases, profile.Databases, cf.Verbose)
return nil
}
@@ -74,12 +73,12 @@ func onDatabaseLogin(cf *CLIConf) error {
if err != nil {
return trace.Wrap(err)
}
var servers []types.DatabaseServer
var databases []types.Database
err = client.RetryWithRelogin(cf.Context, tc, func() error {
allServers, err := tc.ListDatabaseServers(cf.Context)
for _, server := range allServers {
if server.GetName() == cf.DatabaseService {
servers = append(servers, server)
allDatabases, err := tc.ListDatabases(cf.Context)
for _, database := range allDatabases {
if database.GetName() == cf.DatabaseService {
databases = append(databases, database)
}
}
return trace.Wrap(err)
@@ -87,13 +86,13 @@ func onDatabaseLogin(cf *CLIConf) error {
if err != nil {
return trace.Wrap(err)
}
if len(servers) == 0 {
if len(databases) == 0 {
return trace.NotFound(
"database %q not found, use 'tsh db ls' to see registered databases", cf.DatabaseService)
}
err = databaseLogin(cf, tc, tlsca.RouteToDatabase{
ServiceName: cf.DatabaseService,
Protocol: servers[0].GetProtocol(),
Protocol: databases[0].GetProtocol(),
Username: cf.DatabaseUser,
Database: cf.DatabaseName,
}, false)
+13 -13
View File
@@ -1294,11 +1294,11 @@ func showApps(servers []types.Server, active []tlsca.RouteToApp, verbose bool) {
}
}
func showDatabases(cluster string, servers []types.DatabaseServer, active []tlsca.RouteToDatabase, verbose bool) {
func showDatabases(cluster string, databases []types.Database, active []tlsca.RouteToDatabase, verbose bool) {
if verbose {
t := asciitable.MakeTable([]string{"Name", "Description", "Protocol", "Type", "URI", "Labels", "Connect", "Expires"})
for _, server := range servers {
name := server.GetName()
for _, database := range databases {
name := database.GetName()
var connect string
for _, a := range active {
if a.ServiceName == name {
@@ -1308,20 +1308,20 @@ func showDatabases(cluster string, servers []types.DatabaseServer, active []tlsc
}
t.AddRow([]string{
name,
server.GetDescription(),
server.GetProtocol(),
server.GetType(),
server.GetURI(),
server.LabelsString(),
database.GetDescription(),
database.GetProtocol(),
database.GetType(),
database.GetURI(),
database.LabelsString(),
connect,
server.Expiry().Format(constants.HumanDateFormatSeconds),
database.Expiry().Format(constants.HumanDateFormatSeconds),
})
}
fmt.Println(t.AsBuffer().String())
} else {
t := asciitable.MakeTable([]string{"Name", "Description", "Labels", "Connect"})
for _, server := range servers {
name := server.GetName()
for _, database := range databases {
name := database.GetName()
var connect string
for _, a := range active {
if a.ServiceName == name {
@@ -1331,8 +1331,8 @@ func showDatabases(cluster string, servers []types.DatabaseServer, active []tlsc
}
t.AddRow([]string{
name,
server.GetDescription(),
server.LabelsString(),
database.GetDescription(),
database.LabelsString(),
connect,
})
}