MM-48186: Add a new API endpoint to add a user to their default GroupChannels and GroupTeams. (#21591)

* MM-48186: Add a new API endpoint to add a user to their default GroupChannels and GroupTeams.

* MM-48186: Removed unrelated lint fixes.

* MM-48186: Removed variable from previous iteration.

* MM-48186: Adds translation.

* MM-48186: Not upgrading golang.org/x/text in this pr.

* MM-48186: Validate user ID and auth service.

* MM-48186: Use user id from struct.

* MM-48186: Added basic client test.

* MM-48186: Adds empty translation.

* MM-48186: Added translations.

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
Martin Kraft
2022-11-21 13:36:41 -05:00
коммит произвёл GitHub
родитель 25bb1d0ebd
Коммит fcd45de73b
9 изменённых файлов: 227 добавлений и 20 удалений

Просмотреть файл

@@ -40,6 +40,7 @@ func (api *API) InitLdap() {
api.BaseRoutes.LDAP.Handle("/certificate/public", api.APISessionRequired(removeLdapPublicCertificate)).Methods("DELETE")
api.BaseRoutes.LDAP.Handle("/certificate/private", api.APISessionRequired(removeLdapPrivateCertificate)).Methods("DELETE")
api.BaseRoutes.LDAP.Handle("/users/{user_id}/group_sync_memberships", api.APISessionRequired(addUserToGroupSyncables)).Methods("POST")
}
func syncLdap(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -419,3 +420,36 @@ func removeLdapPrivateCertificate(c *Context, w http.ResponseWriter, r *http.Req
auditRec.Success()
ReturnStatusOK(w)
}
// addUserToGroupSyncables creates memberships—for the given user—to all of their group syncables (i.e. channels or teams).
// For each group the user is a member of, for each channel and/or team that group is associated with, the user will be added.
func addUserToGroupSyncables(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementGroups)
return
}
user, appErr := c.App.GetUser(c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
if user.AuthService != model.UserAuthServiceLdap {
c.Err = model.NewAppError("addUserToGroupSyncables", "api.user.add_user_to_group_syncables.not_ldap_user.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("addUserToGroupSyncables", audit.Fail)
defer c.LogAuditRec(auditRec)
params := model.CreateDefaultMembershipParams{Since: 0, ReAddRemovedMembers: true, ScopedUserID: &user.Id}
err := c.App.CreateDefaultMemberships(c.AppContext, params)
if err != nil {
c.Err = model.NewAppError("addUserToGroupSyncables", "api.admin.syncables_error", nil, err.Error(), http.StatusBadRequest)
return
}
auditRec.Success()
ReturnStatusOK(w)
}

Просмотреть файл

@@ -277,3 +277,34 @@ func TestUploadPrivateCertificate(t *testing.T) {
require.NoErrorf(t, err, "Should have passed. System Admin privileges %v", err)
})
}
func TestAddUserToGroupSyncables(t *testing.T) {
th := Setup(t)
defer th.TearDown()
resp, err := th.Client.AddUserToGroupSyncables(th.BasicUser.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
resp, err = th.SystemAdminClient.AddUserToGroupSyncables("invalid-user-id")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
resp, err = th.SystemAdminClient.AddUserToGroupSyncables(th.BasicUser.Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
id := model.NewId()
user := &model.User{
Email: "test@localhost",
Username: model.NewId(),
AuthData: &id,
AuthService: model.UserAuthServiceLdap,
}
user, err = th.App.Srv().Store().User().Save(user)
require.NoError(t, err)
resp, err = th.SystemAdminClient.AddUserToGroupSyncables(user.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
}

Просмотреть файл

@@ -93,7 +93,7 @@ type AppIface interface {
// are configured to sync with teams and channels for group members on or after the given timestamp.
// If includeRemovedMembers is true, then members who left or were removed from a team/channel will
// be re-added; otherwise, they will not be re-added.
CreateDefaultMemberships(c *request.Context, since int64, includeRemovedMembers bool) error
CreateDefaultMemberships(c *request.Context, params model.CreateDefaultMembershipParams) error
// CreateGuest creates a guest and sets several fields of the returned User struct to
// their zero values.
CreateGuest(c request.CTX, user *model.User) (*model.User, *model.AppError)

Просмотреть файл

@@ -2007,7 +2007,7 @@ func (a *OpenTracingAppLayer) CreateCommandWebhook(commandID string, args *model
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateDefaultMemberships(c *request.Context, since int64, includeRemovedMembers bool) error {
func (a *OpenTracingAppLayer) CreateDefaultMemberships(c *request.Context, params model.CreateDefaultMembershipParams) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateDefaultMemberships")
@@ -2019,7 +2019,7 @@ func (a *OpenTracingAppLayer) CreateDefaultMemberships(c *request.Context, since
}()
defer span.Finish()
resultVar0 := a.app.CreateDefaultMemberships(c, since, includeRemovedMembers)
resultVar0 := a.app.CreateDefaultMemberships(c, params)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))

Просмотреть файл

@@ -18,13 +18,16 @@ import (
// only that channel's members are created. If channelID is nil all channel memberships are created.
// If includeRemovedMembers is true, then channel members who left or were removed from the channel will
// be re-added; otherwise, they will not be re-added.
func (a *App) createDefaultChannelMemberships(c request.CTX, since int64, channelID *string, includeRemovedMembers bool) error {
channelMembers, appErr := a.ChannelMembersToAdd(since, channelID, includeRemovedMembers)
func (a *App) createDefaultChannelMemberships(c request.CTX, params model.CreateDefaultMembershipParams) error {
channelMembers, appErr := a.ChannelMembersToAdd(params.Since, params.ScopedChannelID, params.ReAddRemovedMembers)
if appErr != nil {
return appErr
}
for _, userChannel := range channelMembers {
if params.ScopedUserID != nil && *params.ScopedUserID != userChannel.UserID {
continue
}
channel, err := a.GetChannel(c, userChannel.ChannelID)
if err != nil {
return err
@@ -83,13 +86,16 @@ func (a *App) createDefaultChannelMemberships(c request.CTX, since int64, channe
// only that team's members are created. If teamID is nil all team memberships are created.
// If includeRemovedMembers is true, then team members who left or were removed from the team will
// be re-added; otherwise, they will not be re-added.
func (a *App) createDefaultTeamMemberships(c request.CTX, since int64, teamID *string, includeRemovedMembers bool) error {
teamMembers, appErr := a.TeamMembersToAdd(since, teamID, includeRemovedMembers)
func (a *App) createDefaultTeamMemberships(c request.CTX, params model.CreateDefaultMembershipParams) error {
teamMembers, appErr := a.TeamMembersToAdd(params.Since, params.ScopedTeamID, params.ReAddRemovedMembers)
if appErr != nil {
return appErr
}
for _, userTeam := range teamMembers {
if params.ScopedUserID != nil && *params.ScopedUserID != userTeam.UserID {
continue
}
_, err := a.AddTeamMember(c, userTeam.TeamID, userTeam.UserID)
if err != nil {
if err.Id == "api.team.join_user_to_team.allowed_domains.app_error" {
@@ -115,13 +121,13 @@ func (a *App) createDefaultTeamMemberships(c request.CTX, since int64, teamID *s
// are configured to sync with teams and channels for group members on or after the given timestamp.
// If includeRemovedMembers is true, then members who left or were removed from a team/channel will
// be re-added; otherwise, they will not be re-added.
func (a *App) CreateDefaultMemberships(c *request.Context, since int64, includeRemovedMembers bool) error {
err := a.createDefaultTeamMemberships(c, since, nil, includeRemovedMembers)
func (a *App) CreateDefaultMemberships(c *request.Context, params model.CreateDefaultMembershipParams) error {
err := a.createDefaultTeamMemberships(c, params)
if err != nil {
return err
}
err = a.createDefaultChannelMemberships(c, since, nil, includeRemovedMembers)
err = a.createDefaultChannelMemberships(c, params)
if err != nil {
return err
}
@@ -242,15 +248,19 @@ func (a *App) SyncRolesAndMembership(c request.CTX, syncableID string, syncableT
since = lastJob.StartAt
}
params := model.CreateDefaultMembershipParams{Since: since, ReAddRemovedMembers: includeRemovedMembers}
switch syncableType {
case model.GroupSyncableTypeTeam:
a.createDefaultTeamMemberships(c, since, &syncableID, includeRemovedMembers)
params.ScopedTeamID = &syncableID
a.createDefaultTeamMemberships(c, params)
a.deleteGroupConstrainedTeamMemberships(c, &syncableID)
if err := a.ClearTeamMembersCache(syncableID); err != nil {
c.Logger().Warn("Error clearing team members cache", mlog.Err(err))
}
case model.GroupSyncableTypeChannel:
a.createDefaultChannelMemberships(c, since, &syncableID, includeRemovedMembers)
params.ScopedChannelID = &syncableID
a.createDefaultChannelMemberships(c, params)
a.deleteGroupConstrainedChannelMemberships(c, &syncableID)
if err := a.ClearChannelMembersCache(c, syncableID); err != nil {
c.Logger().Warn("Error clearing channel members cache", mlog.Err(err))

Просмотреть файл

@@ -103,7 +103,7 @@ func TestCreateDefaultMemberships(t *testing.T) {
t.Errorf("test groupmember not created: %s", err.Error())
}
pErr := th.App.CreateDefaultMemberships(th.Context, 0, false)
pErr := th.App.CreateDefaultMemberships(th.Context, model.CreateDefaultMembershipParams{Since: 0, ReAddRemovedMembers: false})
if pErr != nil {
t.Errorf("faild to populate syncables: %s", pErr.Error())
}
@@ -173,7 +173,7 @@ func TestCreateDefaultMemberships(t *testing.T) {
}
// Sync everything after syncable was created (proving that team updates trigger re-sync)
pErr = th.App.CreateDefaultMemberships(th.Context, scientistGroupMember.CreateAt+1, false)
pErr = th.App.CreateDefaultMemberships(th.Context, model.CreateDefaultMembershipParams{Since: scientistGroupMember.CreateAt + 1, ReAddRemovedMembers: false})
if pErr != nil {
t.Errorf("faild to populate syncables: %s", pErr.Error())
}
@@ -216,7 +216,7 @@ func TestCreateDefaultMemberships(t *testing.T) {
}
// Sync everything after syncable was created (proving that channel updates trigger re-sync)
pErr = th.App.CreateDefaultMemberships(th.Context, scientistGroupMember.CreateAt+1, false)
pErr = th.App.CreateDefaultMemberships(th.Context, model.CreateDefaultMembershipParams{Since: scientistGroupMember.CreateAt + 1, ReAddRemovedMembers: false})
if pErr != nil {
t.Errorf("faild to populate syncables: %s", pErr.Error())
}
@@ -241,7 +241,7 @@ func TestCreateDefaultMemberships(t *testing.T) {
}
// Even re-syncing from the beginning doesn't re-add to channel or team
pErr = th.App.CreateDefaultMemberships(th.Context, 0, false)
pErr = th.App.CreateDefaultMemberships(th.Context, model.CreateDefaultMembershipParams{Since: 0, ReAddRemovedMembers: false})
if pErr != nil {
t.Errorf("faild to populate syncables: %s", pErr.Error())
}
@@ -282,7 +282,7 @@ func TestCreateDefaultMemberships(t *testing.T) {
t.Errorf("error updating group syncable: %s", err.Error())
}
pErr = th.App.CreateDefaultMemberships(th.Context, 0, false)
pErr = th.App.CreateDefaultMemberships(th.Context, model.CreateDefaultMembershipParams{Since: 0, ReAddRemovedMembers: false})
if pErr != nil {
t.Errorf("faild to populate syncables: %s", pErr.Error())
}
@@ -305,7 +305,7 @@ func TestCreateDefaultMemberships(t *testing.T) {
}
require.Equal(t, int64(1), deletedCount)
pErr = th.App.CreateDefaultMemberships(th.Context, scienceChannelGroupSyncable.UpdateAt, false)
pErr = th.App.CreateDefaultMemberships(th.Context, model.CreateDefaultMembershipParams{Since: scienceChannelGroupSyncable.UpdateAt, ReAddRemovedMembers: false})
if pErr != nil {
t.Errorf("failed to populate syncables: %s", pErr.Error())
}
@@ -323,7 +323,7 @@ func TestCreateDefaultMemberships(t *testing.T) {
}
require.Equal(t, int64(1), deletedCount)
pErr = th.App.CreateDefaultMemberships(th.Context, scienceChannelGroupSyncable.UpdateAt, false)
pErr = th.App.CreateDefaultMemberships(th.Context, model.CreateDefaultMembershipParams{Since: scienceChannelGroupSyncable.UpdateAt, ReAddRemovedMembers: false})
if pErr != nil {
t.Errorf("failed to populate syncables: %s", pErr.Error())
}
@@ -363,7 +363,7 @@ func TestCreateDefaultMemberships(t *testing.T) {
_, err = th.App.UpsertGroupSyncable(model.NewGroupChannel(scienceGroup.Id, restrictedChannel.Id, true))
require.Nil(t, err)
pErr = th.App.CreateDefaultMemberships(th.Context, 0, false)
pErr = th.App.CreateDefaultMemberships(th.Context, model.CreateDefaultMembershipParams{Since: 0, ReAddRemovedMembers: false})
require.NoError(t, pErr)
// Ensure only the restricted user was added to both the team and channel
@@ -375,6 +375,113 @@ func TestCreateDefaultMemberships(t *testing.T) {
require.Len(t, tmembers, 1)
require.Equal(t, tmembers[0].UserId, restrictedUser.Id)
})
t.Run("scoped to a single user", func(t *testing.T) {
team1, err := th.App.CreateTeam(th.Context, &model.Team{
DisplayName: "Team 1",
Name: "zz" + model.NewId(),
Email: "team1admin@test.com",
Type: model.TeamOpen,
})
if err != nil {
t.Errorf("test team not created: %s", err.Error())
}
team1Channel1, err := th.App.CreateChannel(th.Context, &model.Channel{
TeamId: team1.Id,
DisplayName: "Team 1 Channel 1",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}, false)
if err != nil {
t.Errorf("test channel not created: %s", err.Error())
}
group1, err := th.App.CreateGroup(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "Group 1",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
})
if err != nil {
t.Errorf("test group not created: %s", err.Error())
}
_, err = th.App.UpsertGroupSyncable(model.NewGroupTeam(group1.Id, team1.Id, true))
if err != nil {
t.Errorf("test groupchannel not created: %s", err.Error())
}
_, err = th.App.UpsertGroupSyncable(model.NewGroupChannel(group1.Id, team1Channel1.Id, true))
if err != nil {
t.Errorf("test groupchannel not created: %s", err.Error())
}
user1 := th.BasicUser
user2 := th.BasicUser2
_, err = th.App.UpsertGroupMember(group1.Id, user1.Id)
if err != nil {
t.Errorf("test groupmember not created: %s", err.Error())
}
_, err = th.App.UpsertGroupMember(group1.Id, user2.Id)
if err != nil {
t.Errorf("test groupmember not created: %s", err.Error())
}
params := model.CreateDefaultMembershipParams{Since: 0, ReAddRemovedMembers: false, ScopedUserID: &user1.Id}
pErr = th.App.CreateDefaultMemberships(th.Context, params)
if pErr != nil {
t.Errorf("failed to populate syncables: %s", pErr.Error())
}
// test that only user 1 is successfully added to the team and channel.
team1Members, err := th.App.GetTeamMembers(team1.Id, 0, 100, nil)
if err != nil {
t.Errorf("failed to get team members: %s", err.Error())
}
if len(team1Members) != 1 {
t.Errorf("expected 1 team member on team1, got %d", len(team1Members))
}
if team1Members[0].UserId != user1.Id {
t.Errorf("expected user1 to be a team member on team1, got %s", team1Members[0].UserId)
}
team1Channel1Members, err := th.App.GetChannelMembersPage(th.Context, team1Channel1.Id, 0, 100)
if err != nil {
t.Errorf("failed to get channel members: %s", err.Error())
}
if len(team1Channel1Members) != 1 {
t.Errorf("expected 1 channel member on team1Channel1, got %d", len(team1Channel1Members))
}
if team1Channel1Members[0].UserId != user1.Id {
t.Errorf("expected user1 to be a channel member on team1Channel1, got %s", team1Channel1Members[0].UserId)
}
// unscoped should add user2 to the team and channel
params = model.CreateDefaultMembershipParams{Since: 0, ReAddRemovedMembers: false}
pErr = th.App.CreateDefaultMemberships(th.Context, params)
if pErr != nil {
t.Errorf("failed to populate syncables: %s", pErr.Error())
}
team1Members, err = th.App.GetTeamMembers(team1.Id, 0, 100, nil)
if err != nil {
t.Errorf("failed to get team members: %s", err.Error())
}
if len(team1Members) != 2 {
t.Errorf("expected 2 team member on team1, got %d", len(team1Members))
}
team1Channel1Members, err = th.App.GetChannelMembersPage(th.Context, team1Channel1.Id, 0, 100)
if err != nil {
t.Errorf("failed to get channel members: %s", err.Error())
}
if len(team1Channel1Members) != 2 {
t.Errorf("expected 2 channel member on team1Channel1, got %d", len(team1Channel1Members))
}
})
}
func TestDeleteGroupMemberships(t *testing.T) {

Просмотреть файл

@@ -147,6 +147,10 @@
"id": "api.admin.saml.set_certificate_from_metadata.missing_content_type.app_error",
"translation": "Missing content type."
},
{
"id": "api.admin.syncables_error",
"translation": "failed to add user to group-teams and group-channels"
},
{
"id": "api.admin.test_email.body",
"translation": "It appears your Mattermost email is setup correctly!"
@@ -3931,6 +3935,10 @@
"id": "api.user.add_direct_channels_and_forget.failed.error",
"translation": "Failed to add direct channel preferences for user user_id={{.UserId}}, team_id={{.TeamId}}, err={{.Error}}"
},
{
"id": "api.user.add_user_to_group_syncables.not_ldap_user.app_error",
"translation": "not an ldap user"
},
{
"id": "api.user.authorize_oauth_user.bad_response.app_error",
"translation": "Bad response from token request."

Просмотреть файл

@@ -8458,3 +8458,12 @@ func (c *Client4) GetNewTeamMembersSince(teamID string, timeRange string, page i
}
return newTeamMembersList, BuildResponse(r), nil
}
func (c *Client4) AddUserToGroupSyncables(userID string) (*Response, error) {
r, err := c.DoAPIPost(c.ldapRoute()+"/users/"+userID+"/group_sync_memberships", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}

Просмотреть файл

@@ -247,3 +247,11 @@ type GroupsWithCount struct {
Groups []*Group `json:"groups"`
TotalCount int64 `json:"total_count"`
}
type CreateDefaultMembershipParams struct {
Since int64
ReAddRemovedMembers bool
ScopedUserID *string
ScopedTeamID *string
ScopedChannelID *string
}