diff --git a/lib/client/profile.go b/lib/client/profile.go index ea8460137a153..16ed61c589049 100644 --- a/lib/client/profile.go +++ b/lib/client/profile.go @@ -563,6 +563,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 11593181a930e..87d5e5a745d94 100644 --- a/lib/services/access_checker.go +++ b/lib/services/access_checker.go @@ -224,7 +224,7 @@ type AccessChecker interface { GetKubeResources(cluster types.KubeCluster) (allowed, denied []types.KubernetesResource) // EnumerateDatabaseUsers specializes EnumerateEntities to enumerate db_users. - EnumerateDatabaseUsers(database types.Database, extraUsers ...string) EnumerationResult + EnumerateDatabaseUsers(database types.Database, extraUsers ...string) (EnumerationResult, error) // GetAllowedLoginsForResource returns all of the allowed logins for the passed resource. // @@ -250,6 +250,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. @@ -325,7 +327,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)), @@ -534,14 +537,26 @@ func (a *accessChecker) CheckDatabaseRoles(database types.Database) (create bool } // 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() != "" { + result := NewEnumerationResult() + createAutoUser, _, err := a.CheckDatabaseRoles(database) + if err != nil { + return result, trace.Wrap(err) + } else if createAutoUser { + 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. @@ -968,6 +983,7 @@ func AccessInfoFromLocalCertificate(cert *ssh.Certificate) (*AccessInfo, error) } return &AccessInfo{ + Username: cert.KeyId, Roles: roles, Traits: traits, AllowedResourceIDs: allowedResourceIDs, @@ -1014,6 +1030,7 @@ func AccessInfoFromRemoteCertificate(cert *ssh.Certificate, roleMap types.RoleMa } return &AccessInfo{ + Username: cert.KeyId, Roles: roles, Traits: traits, AllowedResourceIDs: allowedResourceIDs, @@ -1047,6 +1064,7 @@ func AccessInfoFromLocalIdentity(identity tlsca.Identity, access UserGetter) (*A } return &AccessInfo{ + Username: identity.Username, Roles: roles, Traits: traits, AllowedResourceIDs: allowedResourceIDs, @@ -1096,6 +1114,7 @@ func AccessInfoFromRemoteIdentity(identity tlsca.Identity, roleMap types.RoleMap allowedResourceIDs := identity.AllowedResourceIDs return &AccessInfo{ + Username: identity.Username, Roles: roles, Traits: traits, AllowedResourceIDs: allowedResourceIDs, @@ -1140,7 +1159,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 1543db63a3fb8..45a93b1a5db73 100644 --- a/lib/services/role_test.go +++ b/lib/services/role_test.go @@ -1990,6 +1990,7 @@ func makeAccessCheckerWithRoleSet(roleSet RoleSet) AccessChecker { roleNames[i] = role.GetName() } accessInfo := &AccessInfo{ + Username: "alice", Roles: roleNames, Traits: nil, AllowedResourceIDs: nil, @@ -3820,6 +3821,17 @@ func TestRoleSetEnumerateDatabaseUsers(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{ @@ -3870,6 +3882,22 @@ func TestRoleSetEnumerateDatabaseUsers(t *testing.T) { }, } + roleAutoUser := &types.RoleV6{ + Metadata: types.Metadata{Name: "auto-user", Namespace: apidefaults.Namespace}, + Spec: types.RoleSpecV6{ + Options: types.RoleOptions{ + CreateDatabaseUser: types.NewBoolOption(true), + }, + 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 @@ -3916,11 +3944,22 @@ func TestRoleSetEnumerateDatabaseUsers(t *testing.T) { wildcardDenied: true, }, }, + { + name: "auto-user provisioning enabled", + roles: RoleSet{roleAutoUser}, + server: dbAutoUser, + enumResult: EnumerationResult{ + allowedDeniedMap: map[string]bool{"alice": true}, + wildcardAllowed: false, + wildcardDenied: false, + }, + }, } for _, tc := range testCases { accessChecker := makeAccessCheckerWithRoleSet(tc.roles) t.Run(tc.name, func(t *testing.T) { - enumResult := accessChecker.EnumerateDatabaseUsers(tc.server) + enumResult, err := accessChecker.EnumerateDatabaseUsers(tc.server) + require.NoError(t, err) require.Equal(t, tc.enumResult, enumResult) }) } @@ -7384,6 +7423,10 @@ func (u mockCurrentUser) GetTraits() map[string][]string { return u.traits } +func (u mockCurrentUser) GetName() string { + return "mockCurrentUser" +} + func TestNewAccessCheckerForRemoteCluster(t *testing.T) { user := mockCurrentUser{ roles: []string{"dev", "admin"}, @@ -7409,6 +7452,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 7283812a7b8e3..7b45131be506e 100644 --- a/lib/teleterm/clusters/cluster_databases.go +++ b/lib/teleterm/clusters/cluster_databases.go @@ -233,7 +233,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/tsh.go b/tool/tsh/tsh.go index 0ed01e845627c..46161dcc34999 100644 --- a/tool/tsh/tsh.go +++ b/tool/tsh/tsh.go @@ -2776,7 +2776,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() { @@ -2830,7 +2834,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)" @@ -2841,6 +2845,18 @@ func formatUsersForDB(database types.Database, accessChecker services.AccessChec return "(none)" } + // Add a note for auto-provisioned user. + if database.SupportsAutoUsers() && database.GetAdminUser() != "" { + createAutoUser, _, err := accessChecker.CheckDatabaseRoles(database) + if err != nil { + log.Warnf("Failed to CheckDatabaseRoles for database %v: %v.", database.GetName(), err) + } else if createAutoUser { + defer func() { + users = users + " (Auto-provisioned)" + }() + } + } + if len(dbUsers.Denied) == 0 { return fmt.Sprintf("%v", dbUsers.Allowed) } diff --git a/tool/tsh/tsh_test.go b/tool/tsh/tsh_test.go index 91dc0a57af36b..04ce3e4874f39 100644 --- a/tool/tsh/tsh_test.go +++ b/tool/tsh/tsh_test.go @@ -4379,6 +4379,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{ @@ -4403,6 +4415,21 @@ func TestListDatabasesWithUsers(t *testing.T) { }, }, } + roleAutoUser := &types.RoleV6{ + Metadata: types.Metadata{Name: "auto-user", Namespace: apidefaults.Namespace}, + Spec: types.RoleSpecV6{ + Options: types.RoleOptions{ + CreateDatabaseUser: types.NewBoolOption(true), + }, + 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 @@ -4463,6 +4490,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 { @@ -4470,7 +4524,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)