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

Database Automatic User Provisioning support for MariaDB #33018

Merged
merged 10 commits into from
Oct 23, 2023
6 changes: 6 additions & 0 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,12 @@ message DatabaseSpecV3 {
message DatabaseAdminUser {
// Name is the username of the privileged database user.
string Name = 1 [(gogoproto.jsontag) = "name"];
// DefaultDatabase is the database that the privileged database user logs
// into by default.
//
// Depending on the database type, this database may be used to store
// procedures or data for managing database users.
string DefaultDatabase = 2 [(gogoproto.jsontag) = "default_database"];
}

// OracleOptions contains information about privileged database user used
Expand Down
4 changes: 4 additions & 0 deletions api/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,10 @@ const (
// discovered databases.
DatabaseAdminLabel = TeleportNamespace + "/db-admin"

// DatabaseAdminDefaultDatabaseLabel is used to identify the database that
// the admin user logs into by default.
DatabaseAdminDefaultDatabaseLabel = TeleportNamespace + "/db-admin-default-database"

// cloudKubeClusterNameOverrideLabel is a cloud agnostic label key for
// overriding kubernetes cluster name in discovered cloud kube clusters.
// It's used for AWS, GCP, and Azure, but not exported to decouple the
Expand Down
20 changes: 15 additions & 5 deletions api/types/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ type Database interface {
// Copy returns a copy of this database resource.
Copy() *DatabaseV3
// GetAdminUser returns database privileged user information.
GetAdminUser() string
GetAdminUser() DatabaseAdminUser
// SupportsAutoUsers returns true if this database supports automatic
// user provisioning.
SupportsAutoUsers() bool
Expand Down Expand Up @@ -291,13 +291,23 @@ func (d *DatabaseV3) SetURI(uri string) {
}

// GetAdminUser returns database privileged user information.
func (d *DatabaseV3) GetAdminUser() string {
func (d *DatabaseV3) GetAdminUser() (ret DatabaseAdminUser) {
// First check the spec.
if d.Spec.AdminUser != nil {
return d.Spec.AdminUser.Name
ret = *d.Spec.AdminUser
}

// If it's not in the spec, check labels (for auto-discovered databases).
return d.Metadata.Labels[DatabaseAdminLabel]
// TODO Azure will require different labels.
if d.Origin() == OriginCloud {
if ret.Name == "" {
ret.Name = d.Metadata.Labels[DatabaseAdminLabel]
}
if ret.DefaultDatabase == "" {
ret.DefaultDatabase = d.Metadata.Labels[DatabaseAdminDefaultDatabaseLabel]
}
}
return
}

// GetOracle returns the Oracle options from spec.
Expand Down Expand Up @@ -841,7 +851,7 @@ func (d *DatabaseV3) CheckAndSetDefaults() error {

// Admin user (for automatic user provisioning) is only supported for
// PostgreSQL currently.
greedy52 marked this conversation as resolved.
Show resolved Hide resolved
if d.GetAdminUser() != "" && !d.SupportsAutoUsers() {
if d.GetAdminUser().Name != "" && !d.SupportsAutoUsers() {
return trace.BadParameter("cannot set admin user on database %q: %v/%v databases don't support automatic user provisioning yet",
d.GetName(), d.GetProtocol(), d.GetType())
}
Expand Down
2,768 changes: 1,409 additions & 1,359 deletions api/types/types.pb.go

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion lib/config/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -1617,7 +1617,8 @@ func applyDatabasesConfig(fc *FileConfig, cfg *servicecfg.Config) error {
Mode: servicecfg.TLSMode(database.TLS.Mode),
},
AdminUser: servicecfg.DatabaseAdminUser{
Name: database.AdminUser.Name,
Name: database.AdminUser.Name,
DefaultDatabase: database.AdminUser.DefaultDatabase,
},
Oracle: convOracleOptions(database.Oracle),
AWS: servicecfg.DatabaseAWS{
Expand Down
6 changes: 6 additions & 0 deletions lib/config/fileconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -1695,6 +1695,12 @@ type Database struct {
type DatabaseAdminUser struct {
// Name is the database admin username (e.g. "postgres").
Name string `yaml:"name"`
// DefaultDatabase is the database that the admin user logs into by
// default.
//
// Depending on the database type, this database may be used to store
// procedures or data for managing database users.
DefaultDatabase string `yaml:"default_database"`
}

// DatabaseAD contains database Active Directory configuration.
Expand Down
9 changes: 8 additions & 1 deletion lib/service/servicecfg/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ type Database struct {
type DatabaseAdminUser struct {
// Name is the database admin username (e.g. "postgres").
Name string
// DefaultDatabase is the database that the admin user logs into by
// default.
//
// Depending on the database type, this database may be used to store
// procedures or data for managing database users.
DefaultDatabase string
}

// OracleOptions are additional Oracle options.
Expand Down Expand Up @@ -158,7 +164,8 @@ func (d *Database) ToDatabase() (types.Database, error) {
ServerVersion: d.MySQL.ServerVersion,
},
AdminUser: &types.DatabaseAdminUser{
Name: d.AdminUser.Name,
Name: d.AdminUser.Name,
DefaultDatabase: d.AdminUser.DefaultDatabase,
},
Oracle: convOracleOptions(d.Oracle),
AWS: types.AWS{
Expand Down
83 changes: 71 additions & 12 deletions lib/srv/db/autousers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package db

import (
"context"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -77,16 +78,70 @@ func TestAutoUsersPostgres(t *testing.T) {
}

func TestAutoUsersMySQL(t *testing.T) {
tests := []mysqlAutoUserTest{
{
name: "MySQL with long name",
serverVersion: "8.0.28",
teleportUser: "a.very.long.name@teleport.example.com",
wantDatabaseUser: "tp-ZLhdP1FgxXsUvcVpG8ucVm/PCHg",
},
{
name: "MySQL not supported",
serverVersion: "5.7.42",
teleportUser: "a.very.long.name@teleport.example.com",
wantClientError: true,
},
{
name: "MariaDB",
serverVersion: "5.5.5-10.11.0-MariaDB",
teleportUser: "a.very.long.name@teleport.example.com",
// MariaDB max username length is 80 (MySQL is 32).
wantDatabaseUser: "a.very.long.name@teleport.example.com",
},
{
name: "MariaDB with long name",
serverVersion: "5.5.5-10.11.0-MariaDB",
teleportUser: strings.Repeat("even-longer-name", 5) + "@teleport.example.com",
wantDatabaseUser: "tp-W+34lSjdNvyLfzOejQLRcbe0Rrs",
},
{
name: "MariaDB not supported",
serverVersion: "5.5.5-10.0.0-MariaDB",
teleportUser: "a.very.long.name@teleport.example.com",
wantClientError: true,
},
}

for _, test := range tests {
test := test
t.Run(test.name, test.Run)
}
}

type mysqlAutoUserTest struct {
name string
serverVersion string
teleportUser string
wantDatabaseUser string
wantClientError bool
}

func (m *mysqlAutoUserTest) Run(t *testing.T) {
t.Parallel()

ctx := context.Background()
testCtx := setupTestContext(ctx, t, withSelfHostedMySQL("mysql", withMySQLAdminUser("admin")))
testCtx := setupTestContext(
ctx,
t,
withSelfHostedMySQL("mysql",
withMySQLAdminUser("admin"),
withMySQLServerVersion(m.serverVersion),
),
)
go testCtx.startHandlingConnections()

// Use a long name to test hashed name is used in database.
teleportUser := "a.very.long.name@teleport.example.com"
wantDatabaseUser := "tp-ZLhdP1FgxXsUvcVpG8ucVm/PCHg"

// Create user with role that allows user provisioning.
_, role, err := auth.CreateUserAndRole(testCtx.tlsServer.Auth(), teleportUser, []string{"auto"}, nil)
_, role, err := auth.CreateUserAndRole(testCtx.tlsServer.Auth(), m.teleportUser, []string{"auto"}, nil)
require.NoError(t, err)
options := role.GetOptions()
options.CreateDatabaseUser = types.NewBoolOption(true)
Expand All @@ -97,17 +152,21 @@ func TestAutoUsersMySQL(t *testing.T) {
require.NoError(t, err)

// DatabaseUser must match identity.
_, err = testCtx.mysqlClient(teleportUser, "mysql", "user1")
_, err = testCtx.mysqlClient(m.teleportUser, "mysql", "user1")
require.Error(t, err)

// Try to connect to the database as this user.
mysqlConn, err := testCtx.mysqlClient(teleportUser, "mysql", teleportUser)
mysqlConn, err := testCtx.mysqlClient(m.teleportUser, "mysql", m.teleportUser)
if m.wantClientError {
require.Error(t, err)
return
}
require.NoError(t, err)

select {
case e := <-testCtx.mysql["mysql"].db.UserEventsCh():
require.Equal(t, teleportUser, e.TeleportUser)
require.Equal(t, wantDatabaseUser, e.DatabaseUser)
require.Equal(t, m.teleportUser, e.TeleportUser)
require.Equal(t, m.wantDatabaseUser, e.DatabaseUser)
require.Equal(t, []string{"reader", "writer"}, e.Roles)
require.True(t, e.Active)
case <-time.After(5 * time.Second):
Expand All @@ -121,8 +180,8 @@ func TestAutoUsersMySQL(t *testing.T) {
// Verify user was deactivated.
select {
case e := <-testCtx.mysql["mysql"].db.UserEventsCh():
require.Equal(t, teleportUser, e.TeleportUser)
require.Equal(t, wantDatabaseUser, e.DatabaseUser)
require.Equal(t, m.teleportUser, e.TeleportUser)
require.Equal(t, m.wantDatabaseUser, e.DatabaseUser)
require.False(t, e.Active)
case <-time.After(5 * time.Second):
t.Fatal("user not deactivated after 5s")
Expand Down
2 changes: 1 addition & 1 deletion lib/srv/db/common/autousers.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (a *UserProvisioner) Activate(ctx context.Context, sessionCtx *Session) (fu
"administrator")
}

if sessionCtx.Database.GetAdminUser() == "" {
if sessionCtx.Database.GetAdminUser().Name == "" {
return nil, trace.BadParameter(
"your Teleport role requires automatic database user provisioning " +
"but this database doesn't have admin user configured, contact " +
Expand Down