diff --git a/lib/client/profile.go b/lib/client/profile.go index 991daa9a06126..bdb6c667a5d5c 100644 --- a/lib/client/profile.go +++ b/lib/client/profile.go @@ -577,6 +577,7 @@ func ProfileNameFromProxyAddress(store ProfileStore, proxyAddr string) (string, // AccessInfo returns the complete services.AccessInfo for this profile. func (p *ProfileStatus) AccessInfo() *services.AccessInfo { return &services.AccessInfo{ + Username: p.Username, Roles: p.Roles, Traits: p.Traits, AllowedResourceIDs: p.AllowedResourceIDs, diff --git a/lib/client/profile_test.go b/lib/client/profile_test.go index 3897e2c5e5902..5c0ba1cf22b42 100644 --- a/lib/client/profile_test.go +++ b/lib/client/profile_test.go @@ -22,6 +22,9 @@ import ( "github.com/stretchr/testify/require" "github.com/gravitational/teleport/api/profile" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/types/wrappers" + "github.com/gravitational/teleport/lib/services" ) func newTestFSProfileStore(t *testing.T) *FSProfileStore { @@ -110,3 +113,31 @@ func TestProfileNameFromProxyAddress(t *testing.T) { require.Error(t, err) }) } + +func TestProfileStatusAccessInfo(t *testing.T) { + allowedResourceIDs := []types.ResourceID{{ + ClusterName: "cluster", + Kind: types.KindNode, + Name: "uuid", + }} + traits := wrappers.Traits{ + "trait1": {"value1", "value2"}, + "trait2": {"value3", "value4"}, + } + + wantAccessInfo := &services.AccessInfo{ + Username: "alice", + Roles: []string{"role1", "role2"}, + Traits: traits, + AllowedResourceIDs: allowedResourceIDs, + } + + profileStatus := ProfileStatus{ + Username: "alice", + Roles: []string{"role1", "role2"}, + Traits: traits, + AllowedResourceIDs: allowedResourceIDs, + } + + require.Equal(t, wantAccessInfo, profileStatus.AccessInfo()) +} diff --git a/lib/services/access_checker.go b/lib/services/access_checker.go index 2b34a322e75dd..5b0713b068fb0 100644 --- a/lib/services/access_checker.go +++ b/lib/services/access_checker.go @@ -236,7 +236,7 @@ type AccessChecker interface { EnumerateEntities(resource AccessCheckable, listFn roleEntitiesListFn, newMatcher roleMatcherFactoryFn, extraEntities ...string) EnumerationResult // EnumerateDatabaseUsers specializes EnumerateEntities to enumerate db_users. - EnumerateDatabaseUsers(database types.Database, extraUsers ...string) EnumerationResult + EnumerateDatabaseUsers(database types.Database, extraUsers ...string) (EnumerationResult, error) // EnumerateDatabaseNames specializes EnumerateEntities to enumerate db_names. EnumerateDatabaseNames(database types.Database, extraNames ...string) EnumerationResult @@ -265,6 +265,8 @@ type AccessInfo struct { // access restrictions should be applied. Used for search-based access // requests. AllowedResourceIDs []types.ResourceID + // Username is the Teleport username. + Username string } // accessChecker implements the AccessChecker interface. @@ -340,7 +342,8 @@ func NewAccessCheckerForRemoteCluster(ctx context.Context, localAccessInfo *Acce } remoteAccessInfo := &AccessInfo{ - Traits: remoteUser.GetTraits(), + Username: localAccessInfo.Username, + Traits: remoteUser.GetTraits(), // Will fill this in with the names of the remote/mapped roles we got // from GetCurrentUserRoles. Roles: make([]string, 0, len(remoteRoles)), @@ -563,14 +566,26 @@ func (a *accessChecker) CheckDatabaseRoles(database types.Database) (mode types. } // EnumerateDatabaseUsers specializes EnumerateEntities to enumerate db_users. -func (a *accessChecker) EnumerateDatabaseUsers(database types.Database, extraUsers ...string) EnumerationResult { +func (a *accessChecker) EnumerateDatabaseUsers(database types.Database, extraUsers ...string) (EnumerationResult, error) { + // When auto-user provisioning is enabled, only Teleport username is allowed. + if database.SupportsAutoUsers() && database.GetAdminUser().Name != "" { + result := NewEnumerationResult() + autoUser, _, err := a.CheckDatabaseRoles(database) + if err != nil { + return result, trace.Wrap(err) + } else if autoUser.IsEnabled() { + result.allowedDeniedMap[a.info.Username] = true + return result, nil + } + } + listFn := func(role types.Role, condition types.RoleConditionType) []string { return role.GetDatabaseUsers(condition) } newMatcher := func(user string) RoleMatcher { return NewDatabaseUserMatcher(database, user) } - return a.EnumerateEntities(database, listFn, newMatcher, extraUsers...) + return a.EnumerateEntities(database, listFn, newMatcher, extraUsers...), nil } // EnumerateDatabaseNames specializes EnumerateEntities to enumerate db_names. @@ -999,6 +1014,7 @@ func AccessInfoFromLocalCertificate(cert *ssh.Certificate) (*AccessInfo, error) } return &AccessInfo{ + Username: cert.KeyId, Roles: roles, Traits: traits, AllowedResourceIDs: allowedResourceIDs, @@ -1045,6 +1061,7 @@ func AccessInfoFromRemoteCertificate(cert *ssh.Certificate, roleMap types.RoleMa } return &AccessInfo{ + Username: cert.KeyId, Roles: roles, Traits: traits, AllowedResourceIDs: allowedResourceIDs, @@ -1078,6 +1095,7 @@ func AccessInfoFromLocalIdentity(identity tlsca.Identity, access UserGetter) (*A } return &AccessInfo{ + Username: identity.Username, Roles: roles, Traits: traits, AllowedResourceIDs: allowedResourceIDs, @@ -1127,6 +1145,7 @@ func AccessInfoFromRemoteIdentity(identity tlsca.Identity, roleMap types.RoleMap allowedResourceIDs := identity.AllowedResourceIDs return &AccessInfo{ + Username: identity.Username, Roles: roles, Traits: traits, AllowedResourceIDs: allowedResourceIDs, @@ -1171,7 +1190,8 @@ func AccessInfoFromUserState(user UserState) *AccessInfo { roles := user.GetRoles() traits := user.GetTraits() return &AccessInfo{ - Roles: roles, - Traits: traits, + Username: user.GetName(), + Roles: roles, + Traits: traits, } } diff --git a/lib/services/role_test.go b/lib/services/role_test.go index 564e7fabaa8d4..2087f8664b932 100644 --- a/lib/services/role_test.go +++ b/lib/services/role_test.go @@ -2082,6 +2082,7 @@ func makeAccessCheckerWithRoleSet(roleSet RoleSet) AccessChecker { roleNames[i] = role.GetName() } accessInfo := &AccessInfo{ + Username: "alice", Roles: roleNames, Traits: nil, AllowedResourceIDs: nil, @@ -3913,6 +3914,17 @@ func TestRoleSetEnumerateDatabaseUsersAndNames(t *testing.T) { URI: "uri", }) require.NoError(t, err) + dbAutoUser, err := types.NewDatabaseV3(types.Metadata{ + Name: "auto-user", + Labels: map[string]string{"env": "prod"}, + }, types.DatabaseSpecV3{ + Protocol: "postgres", + URI: "localhost:5432", + AdminUser: &types.DatabaseAdminUser{ + Name: "teleport-admin", + }, + }) + require.NoError(t, err) roleDevStage := &types.RoleV6{ Metadata: types.Metadata{Name: "dev-stage", Namespace: apidefaults.Namespace}, Spec: types.RoleSpecV6{ @@ -3968,17 +3980,39 @@ func TestRoleSetEnumerateDatabaseUsersAndNames(t *testing.T) { }, } + roleAutoUser := &types.RoleV6{ + Metadata: types.Metadata{Name: "auto-user", Namespace: apidefaults.Namespace}, + Spec: types.RoleSpecV6{ + Options: types.RoleOptions{ + CreateDatabaseUserMode: types.CreateDatabaseUserMode_DB_USER_MODE_KEEP, + }, + Allow: types.RoleConditions{ + Namespaces: []string{apidefaults.Namespace}, + DatabaseLabels: types.Labels{"env": []string{"prod"}}, + DatabaseRoles: []string{"dev"}, + DatabaseNames: []string{"*"}, + DatabaseUsers: []string{types.Wildcard}, + }, + }, + } + testCases := []struct { - name string - roles RoleSet - server types.Database - enumResult EnumerationResult + name string + roles RoleSet + server types.Database + enumDBUserResult EnumerationResult + enumDBNameResult EnumerationResult }{ { name: "deny overrides allow", roles: RoleSet{roleAllowDenySame}, server: dbStage, - enumResult: EnumerationResult{ + enumDBUserResult: EnumerationResult{ + allowedDeniedMap: map[string]bool{"root": false}, + wildcardAllowed: false, + wildcardDenied: false, + }, + enumDBNameResult: EnumerationResult{ allowedDeniedMap: map[string]bool{"root": false}, wildcardAllowed: false, wildcardDenied: false, @@ -3988,7 +4022,12 @@ func TestRoleSetEnumerateDatabaseUsersAndNames(t *testing.T) { name: "developer allowed any username in stage database except root", roles: RoleSet{roleDevStage, roleDevProd}, server: dbStage, - enumResult: EnumerationResult{ + enumDBUserResult: EnumerationResult{ + allowedDeniedMap: map[string]bool{"dev": true, "root": false}, + wildcardAllowed: true, + wildcardDenied: false, + }, + enumDBNameResult: EnumerationResult{ allowedDeniedMap: map[string]bool{"dev": true, "root": false}, wildcardAllowed: true, wildcardDenied: false, @@ -3998,7 +4037,12 @@ func TestRoleSetEnumerateDatabaseUsersAndNames(t *testing.T) { name: "developer allowed only specific username/database in prod database", roles: RoleSet{roleDevStage, roleDevProd}, server: dbProd, - enumResult: EnumerationResult{ + enumDBUserResult: EnumerationResult{ + allowedDeniedMap: map[string]bool{"dev": true, "root": false}, + wildcardAllowed: false, + wildcardDenied: false, + }, + enumDBNameResult: EnumerationResult{ allowedDeniedMap: map[string]bool{"dev": true, "root": false}, wildcardAllowed: false, wildcardDenied: false, @@ -4008,20 +4052,41 @@ func TestRoleSetEnumerateDatabaseUsersAndNames(t *testing.T) { name: "there may be users disallowed from all users", roles: RoleSet{roleDevStage, roleDevProd, roleNoDBAccess}, server: dbProd, - enumResult: EnumerationResult{ + enumDBUserResult: EnumerationResult{ allowedDeniedMap: map[string]bool{"dev": false, "root": false}, wildcardAllowed: false, wildcardDenied: true, }, + enumDBNameResult: EnumerationResult{ + allowedDeniedMap: map[string]bool{"dev": false, "root": false}, + wildcardAllowed: false, + wildcardDenied: true, + }, + }, + { + name: "auto-user provisioning enabled", + roles: RoleSet{roleAutoUser}, + server: dbAutoUser, + enumDBUserResult: EnumerationResult{ + allowedDeniedMap: map[string]bool{"alice": true}, + wildcardAllowed: false, + wildcardDenied: false, + }, + enumDBNameResult: EnumerationResult{ + allowedDeniedMap: map[string]bool{}, + wildcardAllowed: true, + wildcardDenied: false, + }, }, } for _, tc := range testCases { accessChecker := makeAccessCheckerWithRoleSet(tc.roles) t.Run(tc.name, func(t *testing.T) { - enumResult := accessChecker.EnumerateDatabaseUsers(tc.server) - require.Equal(t, tc.enumResult, enumResult) + enumResult, err := accessChecker.EnumerateDatabaseUsers(tc.server) + require.NoError(t, err) + require.Equal(t, tc.enumDBUserResult, enumResult) enumResult = accessChecker.EnumerateDatabaseNames(tc.server) - require.Equal(t, tc.enumResult, enumResult) + require.Equal(t, tc.enumDBNameResult, enumResult) }) } } @@ -7604,6 +7669,7 @@ func TestNewAccessCheckerForRemoteCluster(t *testing.T) { } accessInfo := AccessInfoFromUserState(user) + require.Equal(t, "mockCurrentUser", accessInfo.Username) accessChecker, err := NewAccessCheckerForRemoteCluster(context.Background(), accessInfo, "clustername", currentUserRoleGetter) require.NoError(t, err) diff --git a/lib/teleterm/clusters/cluster_databases.go b/lib/teleterm/clusters/cluster_databases.go index 55ac925c7012d..2527ebe2991c6 100644 --- a/lib/teleterm/clusters/cluster_databases.go +++ b/lib/teleterm/clusters/cluster_databases.go @@ -238,7 +238,10 @@ func (c *Cluster) GetAllowedDatabaseUsers(ctx context.Context, dbURI string) ([] return nil, trace.Wrap(err) } - dbUsers := accessChecker.EnumerateDatabaseUsers(db) + dbUsers, err := accessChecker.EnumerateDatabaseUsers(db) + if err != nil { + return nil, trace.Wrap(err) + } return dbUsers.Allowed(), nil } diff --git a/tool/tsh/common/db.go b/tool/tsh/common/db.go index 8d8d80f462a05..0247bb6d4cd10 100644 --- a/tool/tsh/common/db.go +++ b/tool/tsh/common/db.go @@ -1122,7 +1122,10 @@ func getDefaultDBUser(db types.Database, checker services.AccessChecker) (string // ref: https://redis.io/commands/auth extraUsers = append(extraUsers, defaults.DefaultRedisUsername) } - dbUsers := checker.EnumerateDatabaseUsers(db, extraUsers...) + dbUsers, err := checker.EnumerateDatabaseUsers(db, extraUsers...) + if err != nil { + return "", trace.Wrap(err) + } allowed := dbUsers.Allowed() if len(allowed) == 1 { return allowed[0], nil diff --git a/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go index 963bbeaafacb2..77ff4deb49439 100644 --- a/tool/tsh/common/tsh.go +++ b/tool/tsh/common/tsh.go @@ -2811,7 +2811,11 @@ type databaseWithUsers struct { } func getDBUsers(db types.Database, accessChecker services.AccessChecker) *dbUsers { - users := accessChecker.EnumerateDatabaseUsers(db) + users, err := accessChecker.EnumerateDatabaseUsers(db) + if err != nil { + log.Warnf("Failed to EnumerateDatabaseUsers for database %v: %v.", db.GetName(), err) + return &dbUsers{} + } var denied []string allowed := users.Allowed() if users.WildcardAllowed() { @@ -2865,7 +2869,7 @@ func serializeDatabasesAllClusters(dbListings []databaseListing, format string) return string(out), trace.Wrap(err) } -func formatUsersForDB(database types.Database, accessChecker services.AccessChecker) string { +func formatUsersForDB(database types.Database, accessChecker services.AccessChecker) (users string) { // may happen if fetching the role set failed for any reason. if accessChecker == nil { return "(unknown)" @@ -2876,6 +2880,18 @@ func formatUsersForDB(database types.Database, accessChecker services.AccessChec return "(none)" } + // Add a note for auto-provisioned user. + if database.SupportsAutoUsers() && database.GetAdminUser().Name != "" { + autoUser, _, err := accessChecker.CheckDatabaseRoles(database) + if err != nil { + log.Warnf("Failed to CheckDatabaseRoles for database %v: %v.", database.GetName(), err) + } else if autoUser.IsEnabled() { + defer func() { + users = users + " (Auto-provisioned)" + }() + } + } + if len(dbUsers.Denied) == 0 { return fmt.Sprintf("%v", dbUsers.Allowed) } diff --git a/tool/tsh/common/tsh_test.go b/tool/tsh/common/tsh_test.go index 495247c73b69f..ac92194ca2f5d 100644 --- a/tool/tsh/common/tsh_test.go +++ b/tool/tsh/common/tsh_test.go @@ -4514,6 +4514,18 @@ func TestListDatabasesWithUsers(t *testing.T) { }) require.NoError(t, err) + dbWithAutoUser, err := types.NewDatabaseV3(types.Metadata{ + Name: "auto-user", + Labels: map[string]string{"env": "prod"}, + }, types.DatabaseSpecV3{ + Protocol: "postgres", + URI: "localhost:5432", + AdminUser: &types.DatabaseAdminUser{ + Name: "teleport-admin", + }, + }) + require.NoError(t, err) + roleDevStage := &types.RoleV6{ Metadata: types.Metadata{Name: "dev-stage", Namespace: apidefaults.Namespace}, Spec: types.RoleSpecV6{ @@ -4538,6 +4550,21 @@ func TestListDatabasesWithUsers(t *testing.T) { }, }, } + roleAutoUser := &types.RoleV6{ + Metadata: types.Metadata{Name: "auto-user", Namespace: apidefaults.Namespace}, + Spec: types.RoleSpecV6{ + Options: types.RoleOptions{ + CreateDatabaseUserMode: types.CreateDatabaseUserMode_DB_USER_MODE_KEEP, + }, + Allow: types.RoleConditions{ + Namespaces: []string{apidefaults.Namespace}, + DatabaseLabels: types.Labels{"env": []string{"prod"}}, + DatabaseRoles: []string{"dev"}, + DatabaseNames: []string{"*"}, + DatabaseUsers: []string{types.Wildcard}, + }, + }, + } tests := []struct { name string @@ -4598,6 +4625,33 @@ func TestListDatabasesWithUsers(t *testing.T) { }, wantText: "[dev]", }, + { + name: "db with admin user and role with auto-user", + database: dbWithAutoUser, + roles: services.RoleSet{roleAutoUser}, + wantUsers: &dbUsers{ + Allowed: []string{"alice"}, + }, + wantText: "[alice] (Auto-provisioned)", + }, + { + name: "db with admin user but role without auto-user", + database: dbWithAutoUser, + roles: services.RoleSet{roleDevProd}, + wantUsers: &dbUsers{ + Allowed: []string{"dev"}, + }, + wantText: "[dev]", + }, + { + name: "db without admin user but role with auto-user", + database: dbProd, + roles: services.RoleSet{roleAutoUser}, + wantUsers: &dbUsers{ + Allowed: []string{"*"}, + }, + wantText: "[*]", + }, } for _, tt := range tests { @@ -4605,7 +4659,9 @@ func TestListDatabasesWithUsers(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - accessChecker := services.NewAccessCheckerWithRoleSet(&services.AccessInfo{}, "clustername", tt.roles) + accessChecker := services.NewAccessCheckerWithRoleSet(&services.AccessInfo{ + Username: "alice", + }, "clustername", tt.roles) gotUsers := getDBUsers(tt.database, accessChecker) require.Equal(t, tt.wantUsers, gotUsers)