Skip to content

Commit

Permalink
[v14] Database Automatic User Provisioning support for MySQL (#33379)
Browse files Browse the repository at this point in the history
* Database Automatic User Provisioning support for MySQL (#31902)

* RDS MySQL auto user provisioning

* add UT

* let go code manage procedure version

* Add reporting.

* fix lint

* change hash and use prepare stmt

* check same Teleport user

* fix UT

* Compare user roles if active connections

* fix typos

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

* MySQL auto-user provisioning support for Aurora

* fix typo
  • Loading branch information
greedy52 committed Oct 16, 2023
1 parent 5e3c968 commit b2e5431
Show file tree
Hide file tree
Showing 14 changed files with 1,033 additions and 11 deletions.
5 changes: 5 additions & 0 deletions api/types/database.go
Expand Up @@ -314,6 +314,11 @@ func (d *DatabaseV3) SupportsAutoUsers() bool {
case DatabaseTypeSelfHosted, DatabaseTypeRDS:
return true
}
case DatabaseProtocolMySQL:
switch d.GetType() {
case DatabaseTypeSelfHosted, DatabaseTypeRDS:
return true
}
}
return false
}
Expand Down
37 changes: 35 additions & 2 deletions lib/srv/db/access_test.go
Expand Up @@ -2650,13 +2650,41 @@ func withAzurePostgres(name, authToken string) withDatabaseOption {
}
}

func withSelfHostedMySQL(name string, opts ...mysql.TestServerOption) withDatabaseOption {
type selfHostedMySQLOptions struct {
serverOptions []mysql.TestServerOption
databaseOptions []databaseOption
}

type selfHostedMySQLOption func(*selfHostedMySQLOptions)

func withMySQLServerVersion(version string) selfHostedMySQLOption {
return func(opts *selfHostedMySQLOptions) {
opts.serverOptions = append(opts.serverOptions, mysql.WithServerVersion(version))
}
}

func withMySQLAdminUser(username string) selfHostedMySQLOption {
return func(opts *selfHostedMySQLOptions) {
opts.databaseOptions = append(opts.databaseOptions, func(db *types.DatabaseV3) {
db.Spec.AdminUser = &types.DatabaseAdminUser{
Name: username,
}
})
}
}

func withSelfHostedMySQL(name string, applyOpts ...selfHostedMySQLOption) withDatabaseOption {
return func(t testing.TB, ctx context.Context, testCtx *testContext) types.Database {
opts := selfHostedMySQLOptions{}
for _, applyOpt := range applyOpts {
applyOpt(&opts)
}

mysqlServer, err := mysql.NewTestServer(common.TestServerConfig{
Name: name,
AuthClient: testCtx.authClient,
ClientAuth: tls.RequireAndVerifyClientCert,
}, opts...)
}, opts.serverOptions...)
require.NoError(t, err)
go mysqlServer.Serve()
t.Cleanup(func() {
Expand All @@ -2670,6 +2698,11 @@ func withSelfHostedMySQL(name string, opts ...mysql.TestServerOption) withDataba
DynamicLabels: dynamicLabels,
})
require.NoError(t, err)

for _, applyDatabaseOpt := range opts.databaseOptions {
applyDatabaseOpt(database)
}

testCtx.mysql[name] = testMySQL{
db: mysqlServer,
resource: database,
Expand Down
53 changes: 53 additions & 0 deletions lib/srv/db/autousers_test.go
Expand Up @@ -75,3 +75,56 @@ func TestAutoUsersPostgres(t *testing.T) {
t.Fatal("user not deactivated after 5s")
}
}

func TestAutoUsersMySQL(t *testing.T) {
ctx := context.Background()
testCtx := setupTestContext(ctx, t, withSelfHostedMySQL("mysql", withMySQLAdminUser("admin")))
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)
require.NoError(t, err)
options := role.GetOptions()
options.CreateDatabaseUser = types.NewBoolOption(true)
role.SetOptions(options)
role.SetDatabaseRoles(types.Allow, []string{"reader", "writer"})
role.SetDatabaseNames(types.Allow, []string{"*"})
err = testCtx.tlsServer.Auth().UpsertRole(ctx, role)
require.NoError(t, err)

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

// Try to connect to the database as this user.
mysqlConn, err := testCtx.mysqlClient(teleportUser, "mysql", teleportUser)
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, []string{"reader", "writer"}, e.Roles)
require.True(t, e.Active)
case <-time.After(5 * time.Second):
t.Fatal("user not activated after 5s")
}

// Disconnect.
err = mysqlConn.Close()
require.NoError(t, err)

// 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.False(t, e.Active)
case <-time.After(5 * time.Second):
t.Fatal("user not deactivated after 5s")
}
}
7 changes: 7 additions & 0 deletions lib/srv/db/common/autousers.go
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/jonboulle/clockwork"
"github.com/sirupsen/logrus"

"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/utils/retryutils"
"github.com/gravitational/teleport/lib/auth"
Expand Down Expand Up @@ -68,6 +69,9 @@ func (a *UserProvisioner) Activate(ctx context.Context, sessionCtx *Session) (fu
"your Teleport administrator")
}

// Observe.
defer methodCallMetrics("UserProvisioner:Activate", teleport.ComponentDatabase, sessionCtx.Database)()

retryCtx, cancel := context.WithTimeout(ctx, defaults.DatabaseConnectTimeout)
defer cancel()

Expand Down Expand Up @@ -101,6 +105,9 @@ func (a *UserProvisioner) Deactivate(ctx context.Context, sessionCtx *Session) e
return nil
}

// Observe.
defer methodCallMetrics("UserProvisioner:Deactivate", teleport.ComponentDatabase, sessionCtx.Database)()

retryCtx, cancel := context.WithTimeout(ctx, defaults.DatabaseConnectTimeout)
defer cancel()

Expand Down
8 changes: 8 additions & 0 deletions lib/srv/db/common/session.go
Expand Up @@ -84,3 +84,11 @@ func (c *Session) WithUser(user string) *Session {
copy.DatabaseUser = user
return &copy
}

// WithUserAndDatabase returns a shallow copy of the session with overridden
// database user and overridden database name.
func (c *Session) WithUserAndDatabase(user string, defaultDatabase string) *Session {
copy := c.WithUser(user)
copy.DatabaseName = defaultDatabase
return copy
}

0 comments on commit b2e5431

Please sign in to comment.