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
24 changes: 17 additions & 7 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 @@ -839,9 +849,9 @@ func (d *DatabaseV3) CheckAndSetDefaults() error {
d.GetName())
}

// Admin user (for automatic user provisioning) is only supported for
// PostgreSQL currently.
if d.GetAdminUser() != "" && !d.SupportsAutoUsers() {
// Admin user should only be specified for databases that support automatic
// user provisioning.
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
3,001 changes: 1,526 additions & 1,475 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 @@ -1648,7 +1648,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
78 changes: 63 additions & 15 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 @@ -116,40 +117,87 @@ func TestAutoUsersMySQL(t *testing.T) {
for name, tc := range map[string]struct {
mode types.CreateDatabaseUserMode
databaseRoles []string
teleportUser string
serverVersion string
expectConnectionErr bool
expectDatabaseUser string
}{
"activate/deactivate users": {
"MySQL activate/deactivate users": {
mode: types.CreateDatabaseUserMode_DB_USER_MODE_KEEP,
databaseRoles: []string{"reader", "writer"},
serverVersion: "8.0.28",
teleportUser: "a.very.long.name@teleport.example.com",
expectConnectionErr: false,
expectDatabaseUser: "tp-ZLhdP1FgxXsUvcVpG8ucVm/PCHg",
},
"activate/delete users": {
"MySQL activate/delete users": {
mode: types.CreateDatabaseUserMode_DB_USER_MODE_BEST_EFFORT_DROP,
databaseRoles: []string{"reader", "writer"},
serverVersion: "8.0.28",
teleportUser: "user1",
expectConnectionErr: false,
expectDatabaseUser: "user1",
},
"disabled": {
"MySQL auto-user off": {
mode: types.CreateDatabaseUserMode_DB_USER_MODE_OFF,
databaseRoles: []string{"reader", "writer"},
serverVersion: "8.0.28",
teleportUser: "a.very.long.name@teleport.example.com",
// Given the "alice" user is not present on the database and
// Teleport won't create it, this should fail with an access denied
// error.
expectConnectionErr: true,
expectDatabaseUser: "user1",
},
"MySQL version not supported": {
mode: types.CreateDatabaseUserMode_DB_USER_MODE_KEEP,
databaseRoles: []string{"reader", "writer"},
serverVersion: "5.7.42",
teleportUser: "user1",
expectConnectionErr: true,
expectDatabaseUser: "user1",
},
"MariaDB activate/deactivate users": {
mode: types.CreateDatabaseUserMode_DB_USER_MODE_KEEP,
databaseRoles: []string{"reader", "writer"},
serverVersion: "5.5.5-10.11.0-MariaDB",
teleportUser: "user1",
expectConnectionErr: false,
expectDatabaseUser: "user1",
},
"MariaDB long name": {
mode: types.CreateDatabaseUserMode_DB_USER_MODE_KEEP,
databaseRoles: []string{"reader", "writer"},
serverVersion: "5.5.5-10.11.0-MariaDB",
teleportUser: strings.Repeat("even-longer-name", 5) + "@teleport.example.com",
expectConnectionErr: false,
expectDatabaseUser: "tp-W+34lSjdNvyLfzOejQLRcbe0Rrs",
},
"MariaDB version not supported ": {
mode: types.CreateDatabaseUserMode_DB_USER_MODE_KEEP,
databaseRoles: []string{"reader", "writer"},
serverVersion: "5.5.5-10.0.0-MariaDB",
teleportUser: "user1",
expectConnectionErr: true,
expectDatabaseUser: "user1",
},
} {
t.Run(name, func(t *testing.T) {
tc := tc
t.Parallel()

testCtx := setupTestContext(ctx, t, withSelfHostedMySQL("mysql", withMySQLAdminUser("admin")))
testCtx := setupTestContext(
ctx,
t,
withSelfHostedMySQL("mysql",
withMySQLAdminUser("admin"),
withMySQLServerVersion(tc.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(), tc.teleportUser, []string{"auto"}, nil)
require.NoError(t, err)
options := role.GetOptions()
options.CreateDatabaseUserMode = tc.mode
Expand All @@ -160,11 +208,11 @@ func TestAutoUsersMySQL(t *testing.T) {
require.NoError(t, err)

// DatabaseUser must match identity.
_, err = testCtx.mysqlClient(teleportUser, "mysql", "user1")
_, err = testCtx.mysqlClient(tc.teleportUser, "mysql", "some-other-username")
require.Error(t, err)

// Try to connect to the database as this user.
mysqlConn, err := testCtx.mysqlClient(teleportUser, "mysql", teleportUser)
mysqlConn, err := testCtx.mysqlClient(tc.teleportUser, "mysql", tc.teleportUser)
if tc.expectConnectionErr {
require.Error(t, err)
return
Expand All @@ -173,8 +221,8 @@ func TestAutoUsersMySQL(t *testing.T) {

select {
case e := <-testCtx.mysql["mysql"].db.UserEventsCh():
require.Equal(t, teleportUser, e.TeleportUser)
require.Equal(t, wantDatabaseUser, e.DatabaseUser)
require.Equal(t, tc.teleportUser, e.TeleportUser)
require.Equal(t, tc.expectDatabaseUser, e.DatabaseUser)
require.Equal(t, []string{"reader", "writer"}, e.Roles)
require.True(t, e.Active)
case <-time.After(5 * time.Second):
Expand All @@ -188,8 +236,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, tc.teleportUser, e.TeleportUser)
require.Equal(t, tc.expectDatabaseUser, 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