Skip to content

Commit

Permalink
Merge ebbb64e into 022627f
Browse files Browse the repository at this point in the history
  • Loading branch information
hmhealey committed Mar 27, 2017
2 parents 022627f + ebbb64e commit 5416861
Show file tree
Hide file tree
Showing 80 changed files with 1,445 additions and 1,229 deletions.
11 changes: 10 additions & 1 deletion api4/user.go
Expand Up @@ -266,6 +266,7 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) {
inTeamId := r.URL.Query().Get("in_team")
inChannelId := r.URL.Query().Get("in_channel")
notInChannelId := r.URL.Query().Get("not_in_channel")
withoutTeam := r.URL.Query().Get("without_team")

if len(notInChannelId) > 0 && len(inTeamId) == 0 {
c.SetInvalidParam("team_id")
Expand All @@ -276,7 +277,15 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) {
var err *model.AppError
etag := ""

if len(notInChannelId) > 0 {
if withoutTeamBool, err := strconv.ParseBool(withoutTeam); err == nil && withoutTeamBool {
// Use a special permission for now
if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_LIST_USERS_WITHOUT_TEAM) {
c.SetPermissionError(model.PERMISSION_LIST_USERS_WITHOUT_TEAM)
return
}

profiles, err = app.GetUsersWithoutTeamPage(c.Params.Page, c.Params.PerPage, c.IsSystemAdmin())
} else if len(notInChannelId) > 0 {
if !app.SessionHasPermissionToChannel(c.Session, notInChannelId, model.PERMISSION_READ_CHANNEL) {
c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return
Expand Down
51 changes: 51 additions & 0 deletions api4/user_test.go
Expand Up @@ -837,6 +837,57 @@ func TestGetUsers(t *testing.T) {
CheckUnauthorizedStatus(t, resp)
}

func TestGetUsersWithoutTeam(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
defer TearDown()
Client := th.Client
SystemAdminClient := th.SystemAdminClient

if _, resp := Client.GetUsersWithoutTeam(0, 100, ""); resp.Error == nil {
t.Fatal("should prevent non-admin user from getting users without a team")
}

// These usernames need to appear in the first 100 users for this to work

user, resp := Client.CreateUser(&model.User{
Username: "a000000000" + model.NewId(),
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Password: "Password1",
})
CheckNoError(t, resp)
LinkUserToTeam(user, th.BasicTeam)
defer app.Srv.Store.User().PermanentDelete(user.Id)

user2, resp := Client.CreateUser(&model.User{
Username: "a000000001" + model.NewId(),
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Password: "Password1",
})
CheckNoError(t, resp)
defer app.Srv.Store.User().PermanentDelete(user2.Id)

if rusers, resp := SystemAdminClient.GetUsersWithoutTeam(0, 100, ""); resp.Error != nil {
t.Fatal(resp.Error)
} else {
found1 := false
found2 := false

for _, u := range rusers {
if u.Id == user.Id {
found1 = true
} else if u.Id == user2.Id {
found2 = true
}
}

if found1 {
t.Fatal("shouldn't have returned user that has a team")
} else if !found2 {
t.Fatal("should've returned user that has no teams")
}
}
}

func TestGetUsersInTeam(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
defer TearDown()
Expand Down
21 changes: 21 additions & 0 deletions app/user.go
Expand Up @@ -541,6 +541,27 @@ func GetUsersNotInChannelPage(teamId string, channelId string, page int, perPage
return users, nil
}

func GetUsersWithoutTeamPage(page int, perPage int, asAdmin bool) ([]*model.User, *model.AppError) {
users, err := GetUsersWithoutTeam(page*perPage, perPage)
if err != nil {
return nil, err
}

for _, user := range users {
SanitizeProfile(user, asAdmin)
}

return users, nil
}

func GetUsersWithoutTeam(offset int, limit int) ([]*model.User, *model.AppError) {
if result := <-Srv.Store.User().GetProfilesWithoutTeam(offset, limit); result.Err != nil {
return nil, result.Err
} else {
return result.Data.([]*model.User), nil
}
}

func GetUsersByIds(userIds []string, asAdmin bool) ([]*model.User, *model.AppError) {
if result := <-Srv.Store.User().GetProfileByIds(userIds, true); result.Err != nil {
return nil, result.Err
Expand Down
7 changes: 7 additions & 0 deletions model/authorization.go
Expand Up @@ -57,6 +57,7 @@ var PERMISSION_CREATE_TEAM *Permission
var PERMISSION_MANAGE_TEAM *Permission
var PERMISSION_IMPORT_TEAM *Permission
var PERMISSION_VIEW_TEAM *Permission
var PERMISSION_LIST_USERS_WITHOUT_TEAM *Permission

// General permission that encompases all system admin functions
// in the future this could be broken up to allow access to some
Expand Down Expand Up @@ -286,6 +287,11 @@ func InitalizePermissions() {
"authentication.permissions.view_team.name",
"authentication.permissions.view_team.description",
}
PERMISSION_LIST_USERS_WITHOUT_TEAM = &Permission{
"list_users_without_team",
"authentication.permisssions.list_users_without_team.name",
"authentication.permisssions.list_users_without_team.description",
}
}

func InitalizeRoles() {
Expand Down Expand Up @@ -400,6 +406,7 @@ func InitalizeRoles() {
PERMISSION_DELETE_OTHERS_POSTS.Id,
PERMISSION_CREATE_TEAM.Id,
PERMISSION_ADD_USER_TO_TEAM.Id,
PERMISSION_LIST_USERS_WITHOUT_TEAM.Id,
},
ROLE_TEAM_USER.Permissions...,
),
Expand Down
11 changes: 11 additions & 0 deletions model/client4.go
Expand Up @@ -489,6 +489,17 @@ func (c *Client4) GetUsersNotInChannel(teamId, channelId string, page int, perPa
}
}

// GetUsersWithoutTeam returns a page of users on the system that aren't on any teams. Page counting starts at 0.
func (c *Client4) GetUsersWithoutTeam(page int, perPage int, etag string) ([]*User, *Response) {
query := fmt.Sprintf("?without_team=1&page=%v&per_page=%v", page, perPage)
if r, err := c.DoApiGet(c.GetUsersRoute()+query, etag); err != nil {
return nil, &Response{StatusCode: r.StatusCode, Error: err}
} else {
defer closeBody(r)
return UserListFromJson(r.Body), BuildResponse(r)
}
}

// GetUsersByIds returns a list of users based on the provided user ids.
func (c *Client4) GetUsersByIds(userIds []string) ([]*User, *Response) {
if r, err := c.DoApiPost(c.GetUsersRoute()+"/ids", ArrayToJson(userIds)); err != nil {
Expand Down
48 changes: 48 additions & 0 deletions store/sql_user_store.go
Expand Up @@ -726,6 +726,54 @@ func (us SqlUserStore) GetProfilesNotInChannel(teamId string, channelId string,
return storeChannel
}

func (us SqlUserStore) GetProfilesWithoutTeam(offset int, limit int) StoreChannel {
storeChannel := make(StoreChannel)

go func() {
result := StoreResult{}

var users []*model.User

query := `
SELECT
*
FROM
Users
WHERE
(SELECT
COUNT(0)
FROM
TeamMembers
WHERE
TeamMembers.UserId = Users.Id
AND TeamMembers.DeleteAt = 0) = 0
ORDER BY
Username ASC
LIMIT
:Limit
OFFSET
:Offset`

if _, err := us.GetReplica().Select(&users, query, map[string]interface{}{"Offset": offset, "Limit": limit}); err != nil {
result.Err = model.NewLocAppError("SqlUserStore.GetProfilesWithoutTeam", "store.sql_user.get_profiles.app_error", nil, err.Error())
} else {

for _, u := range users {
u.Password = ""
u.AuthData = new(string)
*u.AuthData = ""
}

result.Data = users
}

storeChannel <- result
close(storeChannel)
}()

return storeChannel
}

func (us SqlUserStore) GetProfilesByUsernames(usernames []string, teamId string) StoreChannel {
storeChannel := make(StoreChannel)

Expand Down
43 changes: 43 additions & 0 deletions store/sql_user_store_test.go
Expand Up @@ -373,6 +373,49 @@ func TestUserStoreGetProfilesInChannel(t *testing.T) {
}
}

func TestUserStoreGetProfilesWithoutTeam(t *testing.T) {
Setup()

teamId := model.NewId()

// These usernames need to appear in the first 100 users for this to work

u1 := &model.User{}
u1.Username = "a000000000" + model.NewId()
u1.Email = model.NewId()
Must(store.User().Save(u1))
Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}))
defer store.User().PermanentDelete(u1.Id)

u2 := &model.User{}
u2.Username = "a000000001" + model.NewId()
u2.Email = model.NewId()
Must(store.User().Save(u2))
defer store.User().PermanentDelete(u2.Id)

if r1 := <-store.User().GetProfilesWithoutTeam(0, 100); r1.Err != nil {
t.Fatal(r1.Err)
} else {
users := r1.Data.([]*model.User)

found1 := false
found2 := false
for _, u := range users {
if u.Id == u1.Id {
found1 = true
} else if u.Id == u2.Id {
found2 = true
}
}

if found1 {
t.Fatal("shouldn't have returned user on team")
} else if !found2 {
t.Fatal("should've returned user without any teams")
}
}
}

func TestUserStoreGetAllProfilesInChannel(t *testing.T) {
Setup()

Expand Down
1 change: 1 addition & 0 deletions store/store.go
Expand Up @@ -177,6 +177,7 @@ type UserStore interface {
GetProfilesInChannel(channelId string, offset int, limit int) StoreChannel
GetAllProfilesInChannel(channelId string, allowFromCache bool) StoreChannel
GetProfilesNotInChannel(teamId string, channelId string, offset int, limit int) StoreChannel
GetProfilesWithoutTeam(offset int, limit int) StoreChannel
GetProfilesByUsernames(usernames []string, teamId string) StoreChannel
GetAllProfiles(offset int, limit int) StoreChannel
GetProfiles(teamId string, offset int, limit int) StoreChannel
Expand Down
3 changes: 2 additions & 1 deletion webapp/actions/global_actions.jsx
Expand Up @@ -25,6 +25,7 @@ const ActionTypes = Constants.ActionTypes;
import Client from 'client/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import WebSocketClient from 'client/web_websocket_client.jsx';
import {sortTeamsByDisplayName} from 'utils/team_utils.jsx';
import * as Utils from 'utils/utils.jsx';

import en from 'i18n/en.json';
Expand Down Expand Up @@ -594,7 +595,7 @@ export function redirectUserToDefaultTeam() {
}

if (myTeams.length > 0) {
myTeams = myTeams.sort(Utils.sortTeamsByDisplayName);
myTeams = myTeams.sort(sortTeamsByDisplayName);
teamId = myTeams[0].id;
}
}
Expand Down
23 changes: 23 additions & 0 deletions webapp/actions/user_actions.jsx
Expand Up @@ -133,6 +133,29 @@ export function loadTeamMembersForProfilesList(profiles, teamId = TeamStore.getC
loadTeamMembersForProfiles(list, teamId, success, error);
}

export function loadProfilesWithoutTeam(page, perPage, success, error) {
Client.getProfilesWithoutTeam(
page,
perPage,
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_PROFILES_WITHOUT_TEAM,
profiles: data,
page
});

loadStatusesForProfilesMap(data);
},
(err) => {
AsyncClient.dispatchError(err, 'getProfilesWithoutTeam');

if (error) {
error(err);
}
}
);
}

function loadTeamMembersForProfiles(userIds, teamId, success, error) {
Client.getTeamMembersByIds(
teamId,
Expand Down
23 changes: 23 additions & 0 deletions webapp/client/client.jsx
Expand Up @@ -1126,6 +1126,29 @@ export default class Client {
this.trackEvent('api', 'api_profiles_get_not_in_channel', {team_id: this.getTeamId(), channel_id: channelId});
}

getProfilesWithoutTeam(page, perPage, success, error) {
// Super hacky, but this option only exists in api v4
function wrappedSuccess(data, res) {
// Convert the profile list provided by api v4 to a map to match similar v3 calls
const profiles = {};

for (const profile of data) {
profiles[profile.id] = profile;
}

success(profiles, res);
}

request.
get(`${this.url}/api/v4/users?without_team=1&page=${page}&per_page=${perPage}`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'getProfilesWithoutTeam', wrappedSuccess, error));

this.trackEvent('api', 'api_profiles_get_without_team');
}

getProfilesByIds(userIds, success, error) {
request.
post(`${this.getUsersRoute()}/ids`).
Expand Down
2 changes: 1 addition & 1 deletion webapp/components/admin_console/admin_navbar_dropdown.jsx
Expand Up @@ -6,7 +6,7 @@ import ReactDOM from 'react-dom';

import TeamStore from 'stores/team_store.jsx';
import Constants from 'utils/constants.jsx';
import {sortTeamsByDisplayName} from 'utils/utils.jsx';
import {sortTeamsByDisplayName} from 'utils/team_utils.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';

import {FormattedMessage} from 'react-intl';
Expand Down
4 changes: 3 additions & 1 deletion webapp/components/admin_console/admin_settings.jsx
Expand Up @@ -112,7 +112,9 @@ export default class AdminSettings extends React.Component {
render() {
return (
<div className='wrapper--fixed'>
{this.renderTitle()}
<h3 className='admin-console-header'>
{this.renderTitle()}
</h3>
<form
className='form-horizontal'
role='form'
Expand Down

0 comments on commit 5416861

Please sign in to comment.