Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v15] Machine ID: Database Tunnel service #40151

Merged
merged 13 commits into from Apr 3, 2024
6 changes: 6 additions & 0 deletions lib/tbot/config/config.go
Expand Up @@ -454,6 +454,12 @@ func (o *ServiceConfigs) UnmarshalYAML(node *yaml.Node) error {
return trace.Wrap(err)
}
out = append(out, v)
case DatabaseTunnelServiceType:
v := &DatabaseTunnelService{}
if err := node.Decode(v); err != nil {
return trace.Wrap(err)
}
out = append(out, v)
default:
return trace.BadParameter("unrecognized service type (%s)", header.Type)
}
Expand Down
82 changes: 82 additions & 0 deletions lib/tbot/config/service_database_tunnel.go
@@ -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
}
108 changes: 108 additions & 0 deletions lib/tbot/config/service_database_tunnel_test.go
@@ -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)
}
@@ -0,0 +1,8 @@
type: database-tunnel
listen: tcp://0.0.0.0:3621
roles:
- role1
- role2
service: service
database: database
username: username
96 changes: 96 additions & 0 deletions lib/tbot/database_access.go
@@ -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
}