Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[v15] Machine ID: Database Tunnel service (#40151)
* Add Database Tunnel service config * Start putting together the alpnproxy * Further flesh out database tunnel service * Reorganize and tidy * Rearrange to avoid state on struct * Tidy up logging * Cache proxypings better * Spell Cancel the american way * Remove unnecessary change to client credential output * Add integration test for db access tunnel * Fix mistakenley renamed trace * Fix test panicking * Remove unnecessary boolean field
- Loading branch information
1 parent
dfe76bf
commit 5a31752
Showing
10 changed files
with
885 additions
and
152 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
/* | ||
* Teleport | ||
* Copyright (C) 2024 Gravitational, Inc. | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU Affero General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU Affero General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU Affero General Public License | ||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
package config | ||
|
||
import ( | ||
"net/url" | ||
|
||
"github.com/gravitational/trace" | ||
"gopkg.in/yaml.v3" | ||
) | ||
|
||
const DatabaseTunnelServiceType = "database-tunnel" | ||
|
||
// DatabaseTunnelService opens an authenticated tunnel for Database Access. | ||
type DatabaseTunnelService struct { | ||
// Listen is the address on which database tunnel should listen. Example: | ||
// - "tcp://127.0.0.1:3306" | ||
// - "tcp://0.0.0.0:3306 | ||
Listen string `yaml:"listen"` | ||
// Roles is the list of roles to request for the tunnel. | ||
// If empty, it defaults to all the bot's roles. | ||
Roles []string `yaml:"roles,omitempty"` | ||
// Service is the service name of the Teleport database. Generally this is | ||
// the name of the Teleport resource. This field is required for all types | ||
// of database. | ||
Service string `yaml:"service"` | ||
// Database is the name of the database to proxy to. | ||
Database string `yaml:"database"` | ||
// Username is the database username to proxy as. | ||
Username string `yaml:"username"` | ||
} | ||
|
||
func (s *DatabaseTunnelService) Type() string { | ||
return DatabaseTunnelServiceType | ||
} | ||
|
||
func (s *DatabaseTunnelService) MarshalYAML() (interface{}, error) { | ||
type raw DatabaseTunnelService | ||
return withTypeHeader((*raw)(s), DatabaseTunnelServiceType) | ||
} | ||
|
||
func (s *DatabaseTunnelService) UnmarshalYAML(node *yaml.Node) error { | ||
// Alias type to remove UnmarshalYAML to avoid recursion | ||
type raw DatabaseTunnelService | ||
if err := node.Decode((*raw)(s)); err != nil { | ||
return trace.Wrap(err) | ||
} | ||
return nil | ||
} | ||
|
||
func (s *DatabaseTunnelService) CheckAndSetDefaults() error { | ||
switch { | ||
case s.Listen == "": | ||
return trace.BadParameter("listen: should not be empty") | ||
case s.Service == "": | ||
return trace.BadParameter("service: should not be empty") | ||
case s.Database == "": | ||
return trace.BadParameter("database: should not be empty") | ||
case s.Username == "": | ||
return trace.BadParameter("username: should not be empty") | ||
} | ||
if _, err := url.Parse(s.Listen); err != nil { | ||
return trace.Wrap(err, "parsing listen") | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
/* | ||
* Teleport | ||
* Copyright (C) 2024 Gravitational, Inc. | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU Affero General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU Affero General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU Affero General Public License | ||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
package config | ||
|
||
import "testing" | ||
|
||
func TestDatabaseTunnelService_YAML(t *testing.T) { | ||
t.Parallel() | ||
|
||
tests := []testYAMLCase[DatabaseTunnelService]{ | ||
{ | ||
name: "full", | ||
in: DatabaseTunnelService{ | ||
Listen: "tcp://0.0.0.0:3621", | ||
Roles: []string{"role1", "role2"}, | ||
Service: "service", | ||
Database: "database", | ||
Username: "username", | ||
}, | ||
}, | ||
} | ||
testYAML(t, tests) | ||
} | ||
|
||
func TestDatabaseTunnelService_CheckAndSetDefaults(t *testing.T) { | ||
t.Parallel() | ||
|
||
tests := []testCheckAndSetDefaultsCase[*DatabaseTunnelService]{ | ||
{ | ||
name: "valid", | ||
in: func() *DatabaseTunnelService { | ||
return &DatabaseTunnelService{ | ||
Listen: "tcp://0.0.0.0:3621", | ||
Roles: []string{"role1", "role2"}, | ||
Service: "service", | ||
Database: "database", | ||
Username: "username", | ||
} | ||
}, | ||
wantErr: "", | ||
}, | ||
{ | ||
name: "missing listen", | ||
in: func() *DatabaseTunnelService { | ||
return &DatabaseTunnelService{ | ||
Roles: []string{"role1", "role2"}, | ||
Service: "service", | ||
Database: "database", | ||
Username: "username", | ||
} | ||
}, | ||
wantErr: "listen: should not be empty", | ||
}, | ||
{ | ||
name: "missing service", | ||
in: func() *DatabaseTunnelService { | ||
return &DatabaseTunnelService{ | ||
Listen: "tcp://0.0.0.0:3621", | ||
Roles: []string{"role1", "role2"}, | ||
Database: "database", | ||
Username: "username", | ||
} | ||
}, | ||
wantErr: "service: should not be empty", | ||
}, | ||
{ | ||
name: "missing database", | ||
in: func() *DatabaseTunnelService { | ||
return &DatabaseTunnelService{ | ||
Listen: "tcp://0.0.0.0:3621", | ||
Roles: []string{"role1", "role2"}, | ||
Service: "service", | ||
Username: "username", | ||
} | ||
}, | ||
wantErr: "database: should not be empty", | ||
}, | ||
{ | ||
name: "missing username", | ||
in: func() *DatabaseTunnelService { | ||
return &DatabaseTunnelService{ | ||
Listen: "tcp://0.0.0.0:3621", | ||
Roles: []string{"role1", "role2"}, | ||
Service: "service", | ||
Database: "database", | ||
} | ||
}, | ||
wantErr: "username: should not be empty", | ||
}, | ||
} | ||
testCheckAndSetDefaults(t, tests) | ||
} |
8 changes: 8 additions & 0 deletions
8
lib/tbot/config/testdata/TestDatabaseTunnelService_YAML/full.golden
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
type: database-tunnel | ||
listen: tcp://0.0.0.0:3621 | ||
roles: | ||
- role1 | ||
- role2 | ||
service: service | ||
database: database | ||
username: username |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
/* | ||
* Teleport | ||
* Copyright (C) 2024 Gravitational, Inc. | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU Affero General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU Affero General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU Affero General Public License | ||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
package tbot | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/gravitational/trace" | ||
"github.com/sirupsen/logrus" | ||
|
||
apiclient "github.com/gravitational/teleport/api/client" | ||
"github.com/gravitational/teleport/api/client/proto" | ||
"github.com/gravitational/teleport/api/defaults" | ||
"github.com/gravitational/teleport/api/types" | ||
"github.com/gravitational/teleport/lib/auth" | ||
libdefaults "github.com/gravitational/teleport/lib/defaults" | ||
) | ||
|
||
func getDatabase(ctx context.Context, clt *auth.Client, name string) (types.Database, error) { | ||
ctx, span := tracer.Start(ctx, "getDatabase") | ||
defer span.End() | ||
|
||
servers, err := apiclient.GetAllResources[types.DatabaseServer](ctx, clt, &proto.ListResourcesRequest{ | ||
Namespace: defaults.Namespace, | ||
ResourceType: types.KindDatabaseServer, | ||
PredicateExpression: makeNameOrDiscoveredNamePredicate(name), | ||
Limit: int32(defaults.DefaultChunkSize), | ||
}) | ||
if err != nil { | ||
return nil, trace.Wrap(err) | ||
} | ||
|
||
var databases []types.Database | ||
for _, server := range servers { | ||
databases = append(databases, server.GetDatabase()) | ||
} | ||
|
||
databases = types.DeduplicateDatabases(databases) | ||
db, err := chooseOneDatabase(databases, name) | ||
return db, trace.Wrap(err) | ||
} | ||
|
||
func getRouteToDatabase( | ||
ctx context.Context, | ||
log logrus.FieldLogger, | ||
client *auth.Client, | ||
service string, | ||
username string, | ||
database string, | ||
) (proto.RouteToDatabase, error) { | ||
ctx, span := tracer.Start(ctx, "getRouteToDatabase") | ||
defer span.End() | ||
|
||
if service == "" { | ||
return proto.RouteToDatabase{}, nil | ||
} | ||
|
||
db, err := getDatabase(ctx, client, service) | ||
if err != nil { | ||
return proto.RouteToDatabase{}, trace.Wrap(err) | ||
} | ||
// make sure the output matches the fully resolved db name, since it may | ||
// have been just a "discovered name". | ||
service = db.GetName() | ||
if db.GetProtocol() == libdefaults.ProtocolMongoDB && username == "" { | ||
// This isn't strictly a runtime error so killing the process seems | ||
// wrong. We'll just loudly warn about it. | ||
log.Errorf("Database `username` field for %q is unset but is required for MongoDB databases.", service) | ||
} else if db.GetProtocol() == libdefaults.ProtocolRedis && username == "" { | ||
// Per tsh's lead, fall back to the default username. | ||
username = libdefaults.DefaultRedisUsername | ||
} | ||
|
||
return proto.RouteToDatabase{ | ||
ServiceName: service, | ||
Protocol: db.GetProtocol(), | ||
Database: database, | ||
Username: username, | ||
}, nil | ||
} |
Oops, something went wrong.