diff --git a/server/channels/api4/team.go b/server/channels/api4/team.go index bbd5faebd4..2f44c6dc0b 100644 --- a/server/channels/api4/team.go +++ b/server/channels/api4/team.go @@ -604,6 +604,10 @@ func getTeamMember(c *Context, w http.ResponseWriter, r *http.Request) { return } + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeamRoles) { + team.SanitizeRoleData(c.AppContext.Session().UserId) + } + if err := json.NewEncoder(w).Encode(team); err != nil { c.Logger.Warn("Error while writing response", mlog.Err(err)) } @@ -642,6 +646,13 @@ func getTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) { return } + currentUserId := c.AppContext.Session().UserId + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeamRoles) { + for _, m := range members { + m.SanitizeRoleData(currentUserId) + } + } + js, err := json.Marshal(members) if err != nil { c.Err = model.NewAppError("getTeamMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) @@ -681,6 +692,13 @@ func getTeamMembersForUser(c *Context, w http.ResponseWriter, r *http.Request) { return } + currentUserId := c.AppContext.Session().UserId + for _, m := range members { + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), m.TeamId, model.PermissionManageTeamRoles) { + m.SanitizeRoleData(currentUserId) + } + } + js, err := json.Marshal(members) if err != nil { c.Err = model.NewAppError("getTeamMembersForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) @@ -724,6 +742,13 @@ func getTeamMembersByIds(c *Context, w http.ResponseWriter, r *http.Request) { return } + currentUserId := c.AppContext.Session().UserId + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeamRoles) { + for _, m := range members { + m.SanitizeRoleData(currentUserId) + } + } + js, err := json.Marshal(members) if err != nil { c.Err = model.NewAppError("getTeamMembersByIds", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) @@ -830,6 +855,10 @@ func addTeamMember(c *Context, w http.ResponseWriter, r *http.Request) { auditRec.AddEventObjectType("team_member") // TODO verify this is the final state. should it be the team instead? auditRec.Success() + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeamRoles) { + tm.SanitizeRoleData(c.AppContext.Session().UserId) + } + w.WriteHeader(http.StatusCreated) if err := json.NewEncoder(w).Encode(tm); err != nil { c.Logger.Warn("Error while writing response", mlog.Err(err)) @@ -984,6 +1013,15 @@ func addTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) { return } + currentUserId := c.AppContext.Session().UserId + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeamRoles) { + for _, m := range membersWithErrors { + if m.Member != nil { + m.Member.SanitizeRoleData(currentUserId) + } + } + } + var ( js []byte err error diff --git a/server/channels/api4/team_test.go b/server/channels/api4/team_test.go index 35a30e5f5f..db6ed64d47 100644 --- a/server/channels/api4/team_test.go +++ b/server/channels/api4/team_test.go @@ -4574,3 +4574,251 @@ func TestInvalidateAllEmailInvites(t *testing.T) { CheckOKStatus(t, res) }) } + +func setupTeamWithAdminAndMember(t *testing.T, th *TestHelper) *model.Client4 { + t.Helper() + th.UpdateUserToTeamAdmin(th.BasicUser2, th.BasicTeam) + require.Nil(t, th.App.Srv().InvalidateAllCaches()) + teamAdminClient := th.CreateClient() + _, _, err := teamAdminClient.Login(context.Background(), th.BasicUser2.Email, th.BasicUser2.Password) + require.NoError(t, err) + return teamAdminClient +} + +func assertRoleDataSanitized(t *testing.T, m *model.TeamMember) { + t.Helper() + assert.Empty(t, m.Roles) + assert.Empty(t, m.ExplicitRoles) + assert.False(t, m.SchemeAdmin) + assert.False(t, m.SchemeGuest) + assert.False(t, m.SchemeUser) + assert.Equal(t, int64(-1), m.DeleteAt) +} + +func TestGetTeamMembersRoleDataSanitization(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t).InitBasic() + teamAdminClient := setupTeamWithAdminAndMember(t, th) + + t.Run("non-admin cannot see role data of others", func(t *testing.T) { + members, _, err := th.Client.GetTeamMembers(context.Background(), th.BasicTeam.Id, 0, 100, "") + require.NoError(t, err) + + for _, m := range members { + if m.UserId != th.BasicUser.Id { + assertRoleDataSanitized(t, m) + } + } + }) + + t.Run("non-admin sees own role data", func(t *testing.T) { + members, _, err := th.Client.GetTeamMembers(context.Background(), th.BasicTeam.Id, 0, 100, "") + require.NoError(t, err) + + for _, m := range members { + if m.UserId == th.BasicUser.Id { + assert.True(t, m.SchemeUser) + return + } + } + require.Fail(t, "current user not found in members") + }) + + t.Run("team admin sees full role data for other user", func(t *testing.T) { + members, _, err := teamAdminClient.GetTeamMembers(context.Background(), th.BasicTeam.Id, 0, 100, "") + require.NoError(t, err) + + for _, m := range members { + if m.UserId == th.BasicUser.Id { + assert.True(t, m.SchemeUser) + return + } + } + require.Fail(t, "target user not found in members") + }) + + t.Run("system admin sees full role data", func(t *testing.T) { + members, _, err := th.SystemAdminClient.GetTeamMembers(context.Background(), th.BasicTeam.Id, 0, 100, "") + require.NoError(t, err) + + for _, m := range members { + if m.UserId == th.BasicUser2.Id { + assert.True(t, m.SchemeAdmin) + return + } + } + require.Fail(t, "team admin not found in members") + }) +} + +func TestGetTeamMemberRoleDataSanitization(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t).InitBasic() + teamAdminClient := setupTeamWithAdminAndMember(t, th) + + t.Run("non-admin cannot see role data of others", func(t *testing.T) { + member, _, err := th.Client.GetTeamMember(context.Background(), th.BasicTeam.Id, th.BasicUser2.Id, "") + require.NoError(t, err) + assertRoleDataSanitized(t, member) + }) + + t.Run("non-admin sees own role data", func(t *testing.T) { + member, _, err := th.Client.GetTeamMember(context.Background(), th.BasicTeam.Id, th.BasicUser.Id, "") + require.NoError(t, err) + assert.True(t, member.SchemeUser) + }) + + t.Run("team admin sees full role data for other user", func(t *testing.T) { + member, _, err := teamAdminClient.GetTeamMember(context.Background(), th.BasicTeam.Id, th.BasicUser.Id, "") + require.NoError(t, err) + assert.True(t, member.SchemeUser) + }) + + t.Run("system admin sees full role data", func(t *testing.T) { + member, _, err := th.SystemAdminClient.GetTeamMember(context.Background(), th.BasicTeam.Id, th.BasicUser2.Id, "") + require.NoError(t, err) + assert.True(t, member.SchemeAdmin) + }) +} + +func TestGetTeamMembersByIdsRoleDataSanitization(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t).InitBasic() + teamAdminClient := setupTeamWithAdminAndMember(t, th) + + t.Run("non-admin cannot see role data of others", func(t *testing.T) { + members, _, err := th.Client.GetTeamMembersByIds(context.Background(), th.BasicTeam.Id, []string{th.BasicUser2.Id}) + require.NoError(t, err) + require.Len(t, members, 1) + assertRoleDataSanitized(t, members[0]) + }) + + t.Run("non-admin sees own role data", func(t *testing.T) { + members, _, err := th.Client.GetTeamMembersByIds(context.Background(), th.BasicTeam.Id, []string{th.BasicUser.Id}) + require.NoError(t, err) + require.Len(t, members, 1) + assert.True(t, members[0].SchemeUser) + }) + + t.Run("team admin sees full role data for other user", func(t *testing.T) { + members, _, err := teamAdminClient.GetTeamMembersByIds(context.Background(), th.BasicTeam.Id, []string{th.BasicUser.Id}) + require.NoError(t, err) + require.Len(t, members, 1) + assert.True(t, members[0].SchemeUser) + }) + + t.Run("system admin sees full role data", func(t *testing.T) { + members, _, err := th.SystemAdminClient.GetTeamMembersByIds(context.Background(), th.BasicTeam.Id, []string{th.BasicUser2.Id}) + require.NoError(t, err) + require.Len(t, members, 1) + assert.True(t, members[0].SchemeAdmin) + }) +} + +func TestAddTeamMemberRoleDataSanitization(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t).InitBasic() + teamAdminClient := setupTeamWithAdminAndMember(t, th) + + t.Run("team admin adding user sees full role data in response", func(t *testing.T) { + newUser := th.CreateUser() + tm, _, err := teamAdminClient.AddTeamMember(context.Background(), th.BasicTeam.Id, newUser.Id) + require.NoError(t, err) + assert.True(t, tm.SchemeUser) + }) + + t.Run("non-admin adding user sees sanitized role data in response", func(t *testing.T) { + defaultRolePermissions := th.SaveDefaultRolePermissions() + defer th.RestoreDefaultRolePermissions(defaultRolePermissions) + th.AddPermissionToRole(model.PermissionAddUserToTeam.Id, model.TeamUserRoleId) + + newUser := th.CreateUser() + tm, _, err := th.Client.AddTeamMember(context.Background(), th.BasicTeam.Id, newUser.Id) + require.NoError(t, err) + assertRoleDataSanitized(t, tm) + }) +} + +func TestAddTeamMembersRoleDataSanitization(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t).InitBasic() + teamAdminClient := setupTeamWithAdminAndMember(t, th) + + t.Run("team admin adding users sees full role data in response", func(t *testing.T) { + newUser := th.CreateUser() + members, _, err := teamAdminClient.AddTeamMembers(context.Background(), th.BasicTeam.Id, []string{newUser.Id}) + require.NoError(t, err) + require.Len(t, members, 1) + assert.True(t, members[0].SchemeUser) + }) + + t.Run("non-admin adding users sees sanitized role data in response", func(t *testing.T) { + defaultRolePermissions := th.SaveDefaultRolePermissions() + defer th.RestoreDefaultRolePermissions(defaultRolePermissions) + th.AddPermissionToRole(model.PermissionAddUserToTeam.Id, model.TeamUserRoleId) + + newUser := th.CreateUser() + members, _, err := th.Client.AddTeamMembers(context.Background(), th.BasicTeam.Id, []string{newUser.Id}) + require.NoError(t, err) + require.Len(t, members, 1) + assertRoleDataSanitized(t, members[0]) + }) +} + +func TestGetTeamMembersForUserRoleDataSanitization(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t).InitBasic() + teamAdminClient := setupTeamWithAdminAndMember(t, th) + + t.Run("user sees own role data", func(t *testing.T) { + members, _, err := th.Client.GetTeamMembersForUser(context.Background(), th.BasicUser.Id, "") + require.NoError(t, err) + require.NotEmpty(t, members) + for _, m := range members { + assert.True(t, m.SchemeUser) + } + }) + + t.Run("non-admin cannot see role data of another user", func(t *testing.T) { + defaultRolePermissions := th.SaveDefaultRolePermissions() + defer th.RestoreDefaultRolePermissions(defaultRolePermissions) + th.AddPermissionToRole(model.PermissionReadOtherUsersTeams.Id, model.SystemUserRoleId) + + members, _, err := th.Client.GetTeamMembersForUser(context.Background(), th.BasicUser2.Id, "") + require.NoError(t, err) + require.NotEmpty(t, members) + for _, m := range members { + assertRoleDataSanitized(t, m) + } + }) + + t.Run("team admin sees full role data for other user in managed team", func(t *testing.T) { + defaultRolePermissions := th.SaveDefaultRolePermissions() + defer th.RestoreDefaultRolePermissions(defaultRolePermissions) + th.AddPermissionToRole(model.PermissionReadOtherUsersTeams.Id, model.SystemUserRoleId) + + members, _, err := teamAdminClient.GetTeamMembersForUser(context.Background(), th.BasicUser.Id, "") + require.NoError(t, err) + require.NotEmpty(t, members) + for _, m := range members { + if m.TeamId == th.BasicTeam.Id { + assert.True(t, m.SchemeUser) + return + } + } + require.Fail(t, "basic team membership not found") + }) + + t.Run("system admin sees full role data", func(t *testing.T) { + members, _, err := th.SystemAdminClient.GetTeamMembersForUser(context.Background(), th.BasicUser2.Id, "") + require.NoError(t, err) + require.NotEmpty(t, members) + for _, m := range members { + if m.TeamId == th.BasicTeam.Id { + assert.True(t, m.SchemeAdmin) + return + } + } + require.Fail(t, "basic team membership not found") + }) +} diff --git a/server/public/model/team_member.go b/server/public/model/team_member.go index 53cf25f072..59c7393679 100644 --- a/server/public/model/team_member.go +++ b/server/public/model/team_member.go @@ -142,3 +142,14 @@ func (o *TeamMember) PreUpdate() { func (o *TeamMember) GetRoles() []string { return strings.Fields(o.Roles) } + +func (o *TeamMember) SanitizeRoleData(currentUserId string) { + if o.UserId != currentUserId { + o.Roles = "" + o.ExplicitRoles = "" + o.SchemeAdmin = false + o.SchemeGuest = false + o.SchemeUser = false + o.DeleteAt = -1 + } +}