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

MySQL auto-user provisioning support for Aurora and version check #32685

Merged
merged 2 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
76 changes: 58 additions & 18 deletions lib/srv/db/mysql/autousers.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ import (
"fmt"
"strings"

"github.com/coreos/go-semver/semver"
"github.com/go-mysql-org/go-mysql/client"
"github.com/go-mysql-org/go-mysql/mysql"
"github.com/gravitational/trace"
"github.com/sirupsen/logrus"
"golang.org/x/exp/slices"

"github.com/gravitational/teleport/api/types"
Expand Down Expand Up @@ -79,6 +81,18 @@ func (c *clientConn) executeAndCloseResult(command string, args ...any) error {
return trace.Wrap(err)
}

func (c *clientConn) isMariaDB() bool {
return strings.Contains(strings.ToLower(c.GetServerVersion()), "mariadb")
}

// maxUsernameLength returns the username/role character limit.
func (c *clientConn) maxUsernameLength() int {
if c.isMariaDB() {
return mariadbMaxUsernameLength
}
return mysqlMaxUsernameLength
}

// ActivateUser creates or enables the database user.
func (e *Engine) ActivateUser(ctx context.Context, sessionCtx *common.Session) error {
if sessionCtx.Database.GetAdminUser() == "" {
Expand All @@ -91,6 +105,11 @@ func (e *Engine) ActivateUser(ctx context.Context, sessionCtx *common.Session) e
}
defer conn.Close()

// Ensure version is supported.
if err := checkSupportedVersion(conn); err != nil {
return trace.Wrap(err)
}

// Ensure the roles meet spec.
if err := checkRoles(conn, sessionCtx.DatabaseRoles); err != nil {
return trace.Wrap(err)
Expand All @@ -102,7 +121,7 @@ func (e *Engine) ActivateUser(ctx context.Context, sessionCtx *common.Session) e
}

// Use "tp-<hash>" in case DatabaseUser is over max username length.
sessionCtx.DatabaseUser = maybeHashUsername(sessionCtx.DatabaseUser, maxUsernameLength(conn))
sessionCtx.DatabaseUser = maybeHashUsername(sessionCtx.DatabaseUser, conn.maxUsernameLength())
e.Log.Infof("Activating MySQL user %q with roles %v for %v.", sessionCtx.DatabaseUser, sessionCtx.DatabaseRoles, sessionCtx.Identity.Username)

// Prep JSON.
Expand Down Expand Up @@ -175,7 +194,7 @@ func (e *Engine) setupDatabaseForAutoUsers(conn *clientConn, sessionCtx *common.
// be used to track User -> JSON attribute mapping (protected view with
// row level security can be used in addition so each user can only read
// their own attributes, if needed).
if isMariaDB(conn) {
if conn.isMariaDB() {
return trace.NotImplemented("auto user provisioning is not supported for MariaDB yet")
}

Expand Down Expand Up @@ -257,27 +276,24 @@ func convertActivateError(sessionCtx *common.Session, err error) error {
// This also avoids "No database selected" errors if client doesn't provide
// one.
func defaultSchema(_ *common.Session) string {
// Use "mysql" as the default schema as both MySQL and Mariadb have it.
// Aurora MySQL does not allow procedures on built-in "mysql" database.
// Technically we can use another built-in database like "sys". However,
// AWS (or database admins for self-hosted) may restrict permissions on
// these built-in databases eventually. Well, each built-in database has
// its own purpose.
//
// Thus lets use a teleport-specific database. This database should be
// created when configuring the admin user. The admin user should be
// granted the following permissions for this database:
// GRANT ALTER ROUTINE, CREATE ROUTINE, EXECUTE ON teleport.* TO '<admin-user>'
//
// TODO consider allowing user to specify the default database through database
// definition.
return "mysql"
}

func isMariaDB(conn *clientConn) bool {
return strings.Contains(strings.ToLower(conn.GetServerVersion()), "mariadb")
}

// maxUsernameLength returns the username/role character limit.
func maxUsernameLength(conn *clientConn) int {
if isMariaDB(conn) {
return mariadbMaxUsernameLength
}
return mysqlMaxUsernameLength
// definition/labels.
return "teleport"
}

func checkRoles(conn *clientConn, roles []string) error {
maxRoleLength := maxUsernameLength(conn)
maxRoleLength := conn.maxUsernameLength()
for _, role := range roles {
if len(role) > maxRoleLength {
return trace.BadParameter("role %q exceeds maximum length limit of %d", role, maxRoleLength)
Expand All @@ -286,6 +302,30 @@ func checkRoles(conn *clientConn, roles []string) error {
return nil
}

func checkSupportedVersion(conn *clientConn) error {
if conn.isMariaDB() {
return trace.NotImplemented("auto user provisioning is not supported for MariaDB yet")
}
return trace.Wrap(checkMySQLSupportedVersion(conn.GetServerVersion()))
}

func checkMySQLSupportedVersion(serverVersion string) error {
ver, err := semver.NewVersion(serverVersion)
switch {
case err != nil:
logrus.Debugf("Invalid MySQL server version %q. Assuming role management is supported.", serverVersion)
return nil

// Reference:
// https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-0.html#mysqld-8-0-0-account-management
case ver.Major < 8:
return trace.BadParameter("role management is not supproted for MySQL servers older than 8.0")
greedy52 marked this conversation as resolved.
Show resolved Hide resolved

default:
return nil
}
}

func maybeHashUsername(teleportUser string, maxUsernameLength int) string {
if len(teleportUser) <= maxUsernameLength {
return teleportUser
Expand Down
29 changes: 29 additions & 0 deletions lib/srv/db/mysql/autousers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,32 @@ func Test_convertActivateError(t *testing.T) {
})
}
}

func Test_checkMySQLSupportedVersion(t *testing.T) {
tests := []struct {
input string
checkError require.ErrorAssertionFunc
}{
{
input: "invalid-server-version",
checkError: require.NoError,
},
{
input: "8.0.28",
checkError: require.NoError,
},
{
input: "9.0.0",
checkError: require.NoError,
},
{
input: "5.7.42",
checkError: require.Error,
},
}
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
test.checkError(t, checkMySQLSupportedVersion(test.input))
})
}
}