mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-28 03:19:47 +08:00
Decouple database server from database (#7771)
This commit is contained in:
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+63
-9
@@ -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.
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Vendored
+8
-7
@@ -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)
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user