* add new pluginapi methods

* SAML login hook

* set ReAddRemovedMembers to true for plugin groups

* change to DoLogin signature for SAML
Этот коммит содержится в:
Ben Cooke
2025-03-13 12:00:15 -04:00
коммит произвёл GitHub
родитель 0e0e54446d
Коммит ccd8a60168
44 изменённых файлов: 2119 добавлений и 213 удалений

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

@@ -1040,6 +1040,59 @@
schema:
type: boolean
default: false
- name: include_member_count
in: query
description: Boolean which adds a `member_count` field to each group object.
schema:
type: boolean
default: false
- name: include_timezones
in: query
description: Boolean which adds timezone information for group members.
schema:
type: boolean
default: false
- name: include_total_count
in: query
description: Boolean which adds total count of groups in the response.
schema:
type: boolean
default: false
- name: include_archived
in: query
description: Boolean which includes archived groups in the response.
schema:
type: boolean
default: false
- name: filter_archived
in: query
description: Boolean which filters out archived groups from the response.
schema:
type: boolean
default: false
- name: filter_parent_team_permitted
in: query
description: Boolean which filters groups based on parent team permissions.
schema:
type: boolean
default: false
- name: filter_has_member
in: query
description: User ID to filter groups that have this member.
schema:
type: string
- name: include_member_ids
in: query
description: Boolean which adds member IDs to the group objects.
schema:
type: boolean
default: false
- name: only_syncable_sources
in: query
description: Boolean which includes groups from syncable sources.
schema:
type: boolean
default: false
responses:
"200":
description: Group list retrieval successful

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

@@ -394,7 +394,7 @@ func linkGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec.AddEventObjectType("group_syncable")
c.App.Srv().Go(func() {
c.App.SyncRolesAndMembership(c.AppContext, syncableID, syncableType, false)
c.App.SyncRolesAndMembership(c.AppContext, syncableID, syncableType, c.Params.GroupId)
})
w.WriteHeader(http.StatusCreated)
@@ -579,7 +579,7 @@ func patchGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec.AddEventObjectType("group_syncable")
c.App.Srv().Go(func() {
c.App.SyncRolesAndMembership(c.AppContext, syncableID, syncableType, false)
c.App.SyncRolesAndMembership(c.AppContext, syncableID, syncableType, c.Params.GroupId)
})
b, err := json.Marshal(groupSyncable)
@@ -641,7 +641,7 @@ func unlinkGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
}
c.App.Srv().Go(func() {
c.App.SyncRolesAndMembership(c.AppContext, syncableID, syncableType, false)
c.App.SyncRolesAndMembership(c.AppContext, syncableID, syncableType, c.Params.GroupId)
})
auditRec.Success()
@@ -655,7 +655,7 @@ func verifyLinkUnlinkPermission(c *Context, syncableType model.GroupSyncableType
return appErr
}
if group.Source != model.GroupSourceLdap {
if !group.IsSyncable() {
return model.NewAppError("Api4.linkGroupSyncable", "app.group.crud_permission", nil, "", http.StatusBadRequest)
}
@@ -1023,6 +1023,8 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
source := c.Params.GroupSource
onlySyncableSources := r.URL.Query().Get("only_syncable_sources") == "true"
if id := c.Params.NotAssociatedToTeam; model.IsValidId(id) {
teamID = id
}
@@ -1042,9 +1044,9 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
// If they don't specify a source and custom groups are disabled, ensure they only get ldap groups in the response
// If they don't specify a source and custom groups are disabled, ensure they only get the other sources
if !*c.App.Config().ServiceSettings.EnableCustomGroups {
source = model.GroupSourceLdap
onlySyncableSources = true
}
includeTimezones := r.URL.Query().Get("include_timezones") == "true"
@@ -1063,6 +1065,7 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
IncludeTimezones: includeTimezones,
IncludeMemberIDs: c.Params.IncludeMemberIDs,
IncludeArchived: includeArchived,
OnlySyncableSources: onlySyncableSources,
}
if teamID != "" {
@@ -1461,6 +1464,10 @@ func licensedAndConfiguredForGroupBySource(app *app.App, source model.GroupSourc
return model.NewAppError("", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
}
if strings.HasPrefix(string(source), string(model.GroupSourcePluginPrefix)) && !*lic.Features.LDAPGroups {
return model.NewAppError("", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
}
if source == model.GroupSourceCustom && lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise {
return model.NewAppError("", "api.custom_groups.license_error", nil, "", http.StatusBadRequest)
}

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

@@ -7,6 +7,7 @@ import (
"context"
"fmt"
"net/http"
"strings"
"testing"
"time"
@@ -1543,7 +1544,7 @@ func TestGetGroups(t *testing.T) {
})
assert.Nil(t, appErr)
opts := model.GroupSearchOpts{
baseOpts := model.GroupSearchOpts{
Source: model.GroupSourceLdap,
PageOpts: &model.PageOpts{
Page: 0,
@@ -1551,142 +1552,222 @@ func TestGetGroups(t *testing.T) {
},
}
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional))
_, _, err := th.SystemAdminClient.GetGroups(context.Background(), opts)
require.NoError(t, err)
_, err = th.SystemAdminClient.UpdateChannelRoles(context.Background(), th.BasicChannel.Id, th.BasicUser.Id, "")
require.NoError(t, err)
opts.NotAssociatedToChannel = th.BasicChannel.Id
_, err = th.SystemAdminClient.UpdateChannelRoles(context.Background(), th.BasicChannel.Id, th.BasicUser.Id, "channel_user channel_admin")
require.NoError(t, err)
groups, _, err := th.SystemAdminClient.GetGroups(context.Background(), opts)
assert.NoError(t, err)
assert.ElementsMatch(t, []*model.Group{group, th.Group}, groups)
assert.Nil(t, groups[0].MemberCount)
opts.IncludeMemberCount = true
groups, _, _ = th.SystemAdminClient.GetGroups(context.Background(), opts)
assert.NotNil(t, groups[0].MemberCount)
opts.IncludeMemberCount = false
opts.Q = "-fOo"
groups, _, _ = th.SystemAdminClient.GetGroups(context.Background(), opts)
assert.Len(t, groups, 1)
opts.Q = ""
_, err = th.SystemAdminClient.UpdateTeamMemberRoles(context.Background(), th.BasicTeam.Id, th.BasicUser.Id, "")
require.NoError(t, err)
opts.NotAssociatedToTeam = th.BasicTeam.Id
_, err = th.SystemAdminClient.UpdateTeamMemberRoles(context.Background(), th.BasicTeam.Id, th.BasicUser.Id, "team_user team_admin")
require.NoError(t, err)
_, _, err = th.Client.GetGroups(context.Background(), opts)
assert.NoError(t, err)
// test "since", should only return group created in this test, not th.Group
opts.Since = start
groups, _, err = th.Client.GetGroups(context.Background(), opts)
assert.NoError(t, err)
assert.Len(t, groups, 1)
// test correct group returned
assert.Equal(t, groups[0].Id, group.Id)
// delete group, should still return
_, appErr = th.App.DeleteGroup(group.Id)
require.Nil(t, appErr)
groups, _, err = th.Client.GetGroups(context.Background(), opts)
assert.NoError(t, err)
assert.Len(t, groups, 1)
assert.Equal(t, groups[0].Id, group.Id)
// test with current since value, return none
opts.Since = model.GetMillis()
groups, _, err = th.Client.GetGroups(context.Background(), opts)
assert.NoError(t, err)
assert.Empty(t, groups)
// make sure delete group is not returned without Since
opts.Since = 0
groups, _, err = th.Client.GetGroups(context.Background(), opts)
assert.NoError(t, err)
//'Normal getGroups should not return delete groups
assert.Len(t, groups, 1)
// make sure it returned th.Group,not group
assert.Equal(t, groups[0].Id, th.Group.Id)
// Test include_archived parameter
opts.IncludeArchived = true
groups, _, err = th.Client.GetGroups(context.Background(), opts)
assert.NoError(t, err)
assert.Len(t, groups, 2)
opts.IncludeArchived = false
// Test returning only archived groups
opts.FilterArchived = true
groups, _, err = th.Client.GetGroups(context.Background(), opts)
assert.NoError(t, err)
assert.Len(t, groups, 1)
assert.Equal(t, groups[0].Id, group.Id)
opts.FilterArchived = false
opts.Source = model.GroupSourceCustom
groups, _, err = th.Client.GetGroups(context.Background(), opts)
assert.NoError(t, err)
assert.Len(t, groups, 1)
assert.Equal(t, groups[0].Id, group2.Id)
// Test IncludeChannelMemberCount url param is working
opts.IncludeChannelMemberCount = th.BasicChannel.Id
opts.IncludeTimezones = true
opts.Q = "-fOo"
opts.IncludeMemberCount = true
groups, _, _ = th.SystemAdminClient.GetGroups(context.Background(), opts)
assert.Equal(t, *groups[0].MemberCount, int(0))
assert.Equal(t, *groups[0].ChannelMemberCount, int(0))
_, appErr = th.App.UpsertGroupMember(group2.Id, th.BasicUser.Id)
assert.Nil(t, appErr)
groups, _, _ = th.SystemAdminClient.GetGroups(context.Background(), opts)
assert.NotNil(t, groups[0].MemberCount)
assert.Equal(t, *groups[0].ChannelMemberCount, int(1))
opts.IncludeChannelMemberCount = ""
opts.IncludeTimezones = false
opts.Q = ""
opts.IncludeMemberCount = false
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableCustomGroups = false
t.Run("without license", func(t *testing.T) {
opts := baseOpts
th.App.Srv().SetLicense(nil)
_, response, err := th.SystemAdminClient.GetGroups(context.Background(), opts)
require.Error(t, err)
CheckNotImplementedStatus(t, response)
})
// Specify custom groups source when feature is disabled
opts.Source = model.GroupSourceCustom
_, response, err := th.Client.GetGroups(context.Background(), opts)
require.Error(t, err)
CheckBadRequestStatus(t, response)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional))
// Specify ldap groups source when custom groups feature is disabled
opts.Source = model.GroupSourceLdap
groups, _, err = th.Client.GetGroups(context.Background(), opts)
assert.NoError(t, err)
assert.Len(t, groups, 1)
assert.Equal(t, groups[0].Source, model.GroupSourceLdap)
t.Run("basic search", func(t *testing.T) {
opts := baseOpts
groups, _, err := th.SystemAdminClient.GetGroups(context.Background(), opts)
require.NoError(t, err)
assert.ElementsMatch(t, []*model.Group{group, th.Group}, groups)
assert.Nil(t, groups[0].MemberCount)
})
// don't include source and should only get ldap groups in response
opts.Source = ""
groups, _, err = th.Client.GetGroups(context.Background(), opts)
assert.NoError(t, err)
assert.Len(t, groups, 1)
assert.Equal(t, groups[0].Source, model.GroupSourceLdap)
t.Run("include member count", func(t *testing.T) {
opts := baseOpts
opts.IncludeMemberCount = true
groups, _, err := th.SystemAdminClient.GetGroups(context.Background(), opts)
require.NoError(t, err)
assert.NotNil(t, groups[0].MemberCount)
})
t.Run("search with Q parameter", func(t *testing.T) {
opts := baseOpts
opts.Q = "-fOo"
groups, _, err := th.SystemAdminClient.GetGroups(context.Background(), opts)
require.NoError(t, err)
assert.Len(t, groups, 1)
})
t.Run("not associated to channel", func(t *testing.T) {
opts := baseOpts
_, err := th.SystemAdminClient.UpdateChannelRoles(context.Background(), th.BasicChannel.Id, th.BasicUser.Id, "")
require.NoError(t, err)
opts.NotAssociatedToChannel = th.BasicChannel.Id
_, err = th.SystemAdminClient.UpdateChannelRoles(context.Background(), th.BasicChannel.Id, th.BasicUser.Id, "channel_user channel_admin")
require.NoError(t, err)
groups, _, err := th.SystemAdminClient.GetGroups(context.Background(), opts)
require.NoError(t, err)
assert.ElementsMatch(t, []*model.Group{group, th.Group}, groups)
})
t.Run("not associated to team", func(t *testing.T) {
opts := baseOpts
_, err := th.SystemAdminClient.UpdateTeamMemberRoles(context.Background(), th.BasicTeam.Id, th.BasicUser.Id, "")
require.NoError(t, err)
opts.NotAssociatedToTeam = th.BasicTeam.Id
_, err = th.SystemAdminClient.UpdateTeamMemberRoles(context.Background(), th.BasicTeam.Id, th.BasicUser.Id, "team_user team_admin")
require.NoError(t, err)
_, _, err = th.Client.GetGroups(context.Background(), opts)
require.NoError(t, err)
})
t.Run("since parameter", func(t *testing.T) {
opts := baseOpts
opts.Since = start
groups, _, err := th.Client.GetGroups(context.Background(), opts)
require.NoError(t, err)
assert.Len(t, groups, 1)
assert.Equal(t, groups[0].Id, group.Id)
opts.Since = model.GetMillis()
groups, _, err = th.Client.GetGroups(context.Background(), opts)
require.NoError(t, err)
assert.Empty(t, groups)
})
t.Run("archived groups", func(t *testing.T) {
opts := baseOpts
_, appErr = th.App.DeleteGroup(group.Id)
require.Nil(t, appErr)
// Test include_archived parameter
opts.IncludeArchived = true
groups, _, err := th.Client.GetGroups(context.Background(), opts)
require.NoError(t, err)
assert.Len(t, groups, 2)
// Test returning only archived groups
opts.FilterArchived = true
groups, _, err = th.Client.GetGroups(context.Background(), opts)
require.NoError(t, err)
assert.Len(t, groups, 1)
assert.Equal(t, groups[0].Id, group.Id)
})
t.Run("group source filtering", func(t *testing.T) {
opts := baseOpts
opts.Source = model.GroupSourceCustom
groups, _, err := th.Client.GetGroups(context.Background(), opts)
require.NoError(t, err)
assert.Len(t, groups, 1)
assert.Equal(t, groups[0].Id, group2.Id)
})
t.Run("channel member counts", func(t *testing.T) {
opts := baseOpts
opts.IncludeChannelMemberCount = th.BasicChannel.Id
opts.IncludeTimezones = true
opts.Q = "-fOo"
opts.IncludeMemberCount = true
opts.Source = model.GroupSourceCustom // Switch to custom source to get group2
groups, _, err := th.SystemAdminClient.GetGroups(context.Background(), opts)
require.NoError(t, err)
require.Len(t, groups, 1)
assert.Equal(t, *groups[0].MemberCount, int(0))
assert.Equal(t, *groups[0].ChannelMemberCount, int(0))
_, appErr = th.App.UpsertGroupMember(group2.Id, th.BasicUser.Id)
require.Nil(t, appErr)
groups, _, err = th.SystemAdminClient.GetGroups(context.Background(), opts)
require.NoError(t, err)
require.Len(t, groups, 1)
assert.Equal(t, *groups[0].MemberCount, int(1))
assert.Equal(t, *groups[0].ChannelMemberCount, int(1))
})
t.Run("custom groups disabled", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableCustomGroups = false
})
t.Run("custom source not allowed", func(t *testing.T) {
opts := baseOpts
opts.Source = model.GroupSourceCustom
_, response, err := th.Client.GetGroups(context.Background(), opts)
require.Error(t, err)
CheckBadRequestStatus(t, response)
})
t.Run("ldap source allowed", func(t *testing.T) {
opts := baseOpts
opts.Source = model.GroupSourceLdap
groups, _, err := th.Client.GetGroups(context.Background(), opts)
require.NoError(t, err)
assert.Len(t, groups, 1)
assert.Equal(t, groups[0].Source, model.GroupSourceLdap)
})
t.Run("no source specified", func(t *testing.T) {
opts := baseOpts
opts.Source = ""
groups, _, err := th.Client.GetGroups(context.Background(), opts)
require.NoError(t, err)
assert.Len(t, groups, 1)
assert.Equal(t, groups[0].Source, model.GroupSourceLdap)
})
})
t.Run("only_syncable_sources parameter", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableCustomGroups = true
})
// Create a syncable group with the plugin prefix
id := model.NewId()
_, appErr := th.App.CreateGroup(&model.Group{
DisplayName: "dn-foo_" + id,
Name: model.NewPointer("name" + id),
Source: model.GroupSourcePluginPrefix + "keycloak",
Description: "description_" + id,
RemoteId: model.NewPointer(model.NewId()),
})
require.Nil(t, appErr)
// First test without only_syncable_sources
opts := model.GroupSearchOpts{
PageOpts: &model.PageOpts{
Page: 0,
PerPage: 60,
},
}
groups, _, err := th.SystemAdminClient.GetGroups(context.Background(), opts)
require.NoError(t, err)
// Should return all groups regardless of source when not specified
assert.Len(t, groups, 3) // group, and group2
// Test with custom groups disabled and only_syncable_sources=true
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableCustomGroups = false
})
groups, _, err = th.SystemAdminClient.GetGroups(context.Background(), opts)
require.NoError(t, err)
// Should still only return LDAP groups
assert.Len(t, groups, 2)
for _, g := range groups {
assert.True(t, g.Source == model.GroupSourceLdap || strings.HasPrefix(string(g.Source), string(model.GroupSourcePluginPrefix)))
}
// Reset config
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableCustomGroups = true
})
// Test with only_syncable_sources=true
opts.OnlySyncableSources = true
groups, _, err = th.SystemAdminClient.GetGroups(context.Background(), opts)
require.NoError(t, err)
// Should only return groups from syncable sources (LDAP and plugin_ groups)
assert.Len(t, groups, 2)
for _, g := range groups {
assert.True(t, g.Source == model.GroupSourceLdap || strings.HasPrefix(string(g.Source), string(model.GroupSourcePluginPrefix)))
}
})
}
func TestGetGroupsByUserId(t *testing.T) {

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

@@ -39,6 +39,14 @@ func NewPluginAPI(a *App, c request.CTX, manifest *model.Manifest) *PluginAPI {
}
}
func (api *PluginAPI) checkLDAPLicense() error {
license := api.GetLicense()
if license == nil || !*license.Features.LDAPGroups {
return fmt.Errorf("license does not support LDAP groups")
}
return nil
}
func (api *PluginAPI) LoadPluginConfiguration(dest any) error {
finalConfig := make(map[string]any)
@@ -656,6 +664,97 @@ func (api *PluginAPI) GetGroupsForUser(userID string) ([]*model.Group, *model.Ap
return api.app.GetGroupsByUserId(userID)
}
func (api *PluginAPI) UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("UpsertGroupMember", "app.group.license_error", nil, err.Error(), http.StatusForbidden)
}
return api.app.UpsertGroupMember(groupID, userID)
}
func (api *PluginAPI) UpsertGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("UpsertGroupMembers", "app.group.license_error", nil, err.Error(), http.StatusForbidden)
}
return api.app.UpsertGroupMembers(groupID, userIDs)
}
func (api *PluginAPI) GetGroupByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("GetGroupByRemoteID", "app.group.license_error", nil, err.Error(), http.StatusForbidden)
}
return api.app.GetGroupByRemoteID(remoteID, groupSource)
}
func (api *PluginAPI) CreateGroup(group *model.Group) (*model.Group, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("CreateGroup", "app.group.license_error", nil, err.Error(), http.StatusForbidden)
}
return api.app.CreateGroup(group)
}
func (api *PluginAPI) UpdateGroup(group *model.Group) (*model.Group, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("UpdateGroup", "app.group.license_error", nil, err.Error(), http.StatusForbidden)
}
return api.app.UpdateGroup(group)
}
func (api *PluginAPI) DeleteGroup(groupID string) (*model.Group, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("DeleteGroup", "app.group.license_error", nil, err.Error(), http.StatusForbidden)
}
return api.app.DeleteGroup(groupID)
}
func (api *PluginAPI) RestoreGroup(groupID string) (*model.Group, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("RestoreGroup", "app.group.license_error", nil, err.Error(), http.StatusForbidden)
}
return api.app.RestoreGroup(groupID)
}
func (api *PluginAPI) DeleteGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("DeleteGroupMember", "app.group.license_error", nil, err.Error(), http.StatusForbidden)
}
return api.app.DeleteGroupMember(groupID, userID)
}
func (api *PluginAPI) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("GetGroupSyncable", "app.group.license_error", nil, err.Error(), http.StatusForbidden)
}
return api.app.GetGroupSyncable(groupID, syncableID, syncableType)
}
func (api *PluginAPI) GetGroupSyncables(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("GetGroupSyncables", "app.group.license_error", nil, err.Error(), http.StatusForbidden)
}
return api.app.GetGroupSyncables(groupID, syncableType)
}
func (api *PluginAPI) UpsertGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("UpsertGroupSyncable", "app.group.license_error", nil, err.Error(), http.StatusForbidden)
}
return api.app.UpsertGroupSyncable(groupSyncable)
}
func (api *PluginAPI) UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("UpdateGroupSyncable", "app.group.license_error", nil, err.Error(), http.StatusForbidden)
}
return api.app.UpdateGroupSyncable(groupSyncable)
}
func (api *PluginAPI) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("DeleteGroupSyncable", "app.group.license_error", nil, err.Error(), http.StatusForbidden)
}
return api.app.DeleteGroupSyncable(groupID, syncableID, syncableType)
}
func (api *PluginAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
post.AddProp("from_plugin", "true")
@@ -1347,3 +1446,10 @@ func (api *PluginAPI) UninviteRemoteFromChannel(channelID string, remoteID strin
func (api *PluginAPI) GetPluginID() string {
return api.id
}
func (api *PluginAPI) GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("GetGroups", "app.group.license_error", nil, err.Error(), http.StatusForbidden)
}
return api.app.GetGroups(page, perPage, opts, viewRestrictions)
}

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

@@ -270,16 +270,27 @@ func (a *App) SyncSyncableRoles(rctx request.CTX, syncableID string, syncableTyp
// SyncRolesAndMembership updates the SchemeAdmin status and membership of all of the members of the given
// syncable.
func (a *App) SyncRolesAndMembership(rctx request.CTX, syncableID string, syncableType model.GroupSyncableType, includeRemovedMembers bool) {
appErr := a.SyncSyncableRoles(rctx, syncableID, syncableType)
func (a *App) SyncRolesAndMembership(rctx request.CTX, syncableID string, syncableType model.GroupSyncableType, groupID string) {
group, appErr := a.GetGroup(groupID, nil, nil)
if appErr != nil {
rctx.Logger().Warn("Error getting group", mlog.Err(appErr))
return
}
appErr = a.SyncSyncableRoles(rctx, syncableID, syncableType)
if appErr != nil {
rctx.Logger().Warn("Error syncing syncable roles", mlog.Err(appErr))
}
lastJob, _ := a.Srv().Store().Job().GetNewestJobByStatusAndType(model.JobStatusSuccess, model.JobTypeLdapSync)
var since int64
if lastJob != nil {
since = lastJob.StartAt
includeRemovedMembers := true
if group.Source == model.GroupSourceLdap {
lastJob, _ := a.Srv().Store().Job().GetNewestJobByStatusAndType(model.JobStatusSuccess, model.JobTypeLdapSync)
if lastJob != nil {
since = lastJob.StartAt
}
includeRemovedMembers = false
}
params := model.CreateDefaultMembershipParams{Since: since, ReAddRemovedMembers: includeRemovedMembers}

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

@@ -1620,6 +1620,18 @@ func (s *SqlGroupStore) GetGroups(page, perPage int, opts model.GroupSearchOpts,
if opts.Source != "" {
groupsQuery = groupsQuery.Where("g.Source = ?", opts.Source)
} else if opts.OnlySyncableSources {
sources := model.GetSyncableGroupSources()
sourcePrefixes := model.GetSyncableGroupSourcePrefixes()
orClauses := sq.Or{}
if len(sources) > 0 {
orClauses = append(orClauses, sq.Eq{"g.Source": sources})
}
for _, prefix := range sourcePrefixes {
orClauses = append(orClauses, sq.Like{"g.Source": string(prefix) + "%"})
}
groupsQuery = groupsQuery.Where(orClauses)
}
queryString, args, err := groupsQuery.ToSql()

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

@@ -3963,6 +3963,36 @@ func testGetGroups(t *testing.T, rctx request.CTX, ss store.Store) {
},
Restrictions: nil,
},
{
Name: "Include syncable sources only",
Opts: model.GroupSearchOpts{OnlySyncableSources: true},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
for _, g := range groups {
if g.Source != model.GroupSourceLdap && !strings.HasPrefix(string(g.Source), "plugin_") {
return false
}
}
return true
},
Restrictions: nil,
},
{
Name: "Include syncable sources with specific source",
Opts: model.GroupSearchOpts{OnlySyncableSources: true, Source: model.GroupSourceLdap},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
for _, g := range groups {
if g.Source != model.GroupSourceLdap {
return false
}
}
return true
},
Restrictions: nil,
},
{
Name: "Include archived groups",
Opts: model.GroupSearchOpts{IncludeArchived: true, Q: "group-deleted"},

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

@@ -12,6 +12,7 @@ import (
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/v8/channels/audit"
"github.com/mattermost/mattermost/server/v8/channels/utils"
)
@@ -135,7 +136,7 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
user, err := samlInterface.DoLogin(c.AppContext, encodedXML, relayProps)
user, assertion, err := samlInterface.DoLogin(c.AppContext, encodedXML, relayProps)
if err != nil {
c.LogAudit("fail")
handleError(err)
@@ -172,6 +173,18 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
})
}
pluginContext := &plugin.Context{
RequestId: c.AppContext.RequestId(),
SessionId: c.AppContext.Session().Id,
IPAddress: c.AppContext.IPAddress(),
AcceptLanguage: c.AppContext.AcceptLanguage(),
UserAgent: c.AppContext.UserAgent(),
}
c.App.Channels().RunMultiHook(func(hooks plugin.Hooks, manifest *model.Manifest) bool {
err := hooks.OnSAMLLogin(pluginContext, user, assertion)
return err == nil
}, plugin.OnSAMLLoginID)
auditRec.AddMeta("obtained_user_id", user.Id)
c.LogAuditWithUserId(user.Id, "obtained user")

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

@@ -8,6 +8,8 @@ import (
model "github.com/mattermost/mattermost/server/public/model"
request "github.com/mattermost/mattermost/server/public/shared/request"
mock "github.com/stretchr/testify/mock"
saml2 "github.com/mattermost/gosaml2"
)
// SamlInterface is an autogenerated mock type for the SamlInterface type
@@ -84,7 +86,7 @@ func (_m *SamlInterface) ConfigureSP(c request.CTX) error {
}
// DoLogin provides a mock function with given fields: c, encodedXML, relayState
func (_m *SamlInterface) DoLogin(c request.CTX, encodedXML string, relayState map[string]string) (*model.User, *model.AppError) {
func (_m *SamlInterface) DoLogin(c request.CTX, encodedXML string, relayState map[string]string) (*model.User, *saml2.AssertionInfo, *model.AppError) {
ret := _m.Called(c, encodedXML, relayState)
if len(ret) == 0 {
@@ -92,8 +94,9 @@ func (_m *SamlInterface) DoLogin(c request.CTX, encodedXML string, relayState ma
}
var r0 *model.User
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, string, map[string]string) (*model.User, *model.AppError)); ok {
var r1 *saml2.AssertionInfo
var r2 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, string, map[string]string) (*model.User, *saml2.AssertionInfo, *model.AppError)); ok {
return rf(c, encodedXML, relayState)
}
if rf, ok := ret.Get(0).(func(request.CTX, string, map[string]string) *model.User); ok {
@@ -104,15 +107,23 @@ func (_m *SamlInterface) DoLogin(c request.CTX, encodedXML string, relayState ma
}
}
if rf, ok := ret.Get(1).(func(request.CTX, string, map[string]string) *model.AppError); ok {
if rf, ok := ret.Get(1).(func(request.CTX, string, map[string]string) *saml2.AssertionInfo); ok {
r1 = rf(c, encodedXML, relayState)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
r1 = ret.Get(1).(*saml2.AssertionInfo)
}
}
return r0, r1
if rf, ok := ret.Get(2).(func(request.CTX, string, map[string]string) *model.AppError); ok {
r2 = rf(c, encodedXML, relayState)
} else {
if ret.Get(2) != nil {
r2 = ret.Get(2).(*model.AppError)
}
}
return r0, r1, r2
}
// GetMetadata provides a mock function with given fields: c

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

@@ -4,6 +4,7 @@
package einterfaces
import (
saml2 "github.com/mattermost/gosaml2"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
)
@@ -11,7 +12,7 @@ import (
type SamlInterface interface {
ConfigureSP(c request.CTX) error
BuildRequest(c request.CTX, relayState string) (*model.SamlAuthRequest, *model.AppError)
DoLogin(c request.CTX, encodedXML string, relayState map[string]string) (*model.User, *model.AppError)
DoLogin(c request.CTX, encodedXML string, relayState map[string]string) (*model.User, *saml2.AssertionInfo, *model.AppError)
GetMetadata(c request.CTX) (string, *model.AppError)
CheckProviderAttributes(c request.CTX, SS *model.SamlSettings, ouser *model.User, patch *model.UserPatch) string
}

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

@@ -5314,6 +5314,10 @@
"id": "app.group.id.app_error",
"translation": "invalid id property for group."
},
{
"id": "app.group.license_error",
"translation": "LDAP license required."
},
{
"id": "app.group.no_rows",
"translation": "no matching group found"

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

@@ -17,6 +17,7 @@ require (
github.com/hashicorp/go-plugin v1.6.3
github.com/lib/pq v1.10.9
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404
github.com/mattermost/gosaml2 v0.8.0
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956
github.com/mattermost/logr/v2 v2.0.21
github.com/nicksnyder/go-i18n/v2 v2.5.0
@@ -37,6 +38,7 @@ require (
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/beevik/etree v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
@@ -44,7 +46,9 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/oklog/run v1.1.0 // indirect
@@ -52,6 +56,7 @@ require (
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/russellhaering/goxmldsig v1.2.0 // indirect
github.com/segmentio/backo-go v1.1.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
@@ -67,7 +72,6 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 // indirect
google.golang.org/grpc v1.70.0 // indirect
google.golang.org/protobuf v1.36.4 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

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

@@ -13,6 +13,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
@@ -93,11 +95,14 @@ github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -111,10 +116,14 @@ github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 h1:Khvh6waxG1cHc4Cz5ef9n3XVCxRWpAKUtqg9PJl5+y8=
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404/go.mod h1:RyS7FDNQlzF1PsjbJWHRI35exqaKGSO9qD4iv8QjE34=
github.com/mattermost/gosaml2 v0.8.0 h1:nkYiByawqwJ7KncK1LDWKwTx5aRarBTQsmH+XcCVsWQ=
github.com/mattermost/gosaml2 v0.8.0/go.mod h1:1nMAdE2Psxaz+pj79Oytayi+hC3aZUi3SmJQlIe+sLM=
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 h1:Y1Tu/swM31pVwwb2BTCsOdamENjjWCI6qmfHLbk6OZI=
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956/go.mod h1:SRl30Lb7/QoYyohYeVBuqYvvmXSZJxZgiV3Zf6VbxjI=
github.com/mattermost/logr/v2 v2.0.21 h1:CMHsP+nrbRlEC4g7BwOk1GAnMtHkniFhlSQPXy52be4=
github.com/mattermost/logr/v2 v2.0.21/go.mod h1:kZkB/zqKL9e+RY5gB3vGpsyenC+TpuiOenjMkvJJbzc=
github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
@@ -152,11 +161,15 @@ github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rudderlabs/analytics-go v3.3.3+incompatible h1:OG0XlKoXfr539e2t1dXtTB+Gr89uFW+OUNQBVhHIIBY=
github.com/rudderlabs/analytics-go v3.3.3+incompatible/go.mod h1:LF8/ty9kUX4PTY3l5c97K3nZZaX5Hwsvt+NBaRL/f30=
github.com/russellhaering/goxmldsig v1.2.0 h1:Y6GTTc9Un5hCxSzVz4UIWQ/zuVwDvzJk80guqzwx6Vg=
github.com/russellhaering/goxmldsig v1.2.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/segmentio/backo-go v1.1.0 h1:cJIfHQUdmLsd8t9IXqf5J8SdrOMn9vMa7cIvOavHAhc=
github.com/segmentio/backo-go v1.1.0/go.mod h1:ckenwdf+v/qbyhVdNPWHnqh2YdJBED1O9cidYyM5J18=
@@ -339,8 +352,10 @@ google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40Rmc
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
@@ -349,6 +364,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=

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

@@ -5862,7 +5862,7 @@ func (c *Client4) GetGroupsAssociatedToChannelsByTeam(ctx context.Context, teamI
// GetGroups retrieves Mattermost Groups
func (c *Client4) GetGroups(ctx context.Context, opts GroupSearchOpts) ([]*Group, *Response, error) {
path := fmt.Sprintf(
"%s?include_member_count=%v&not_associated_to_team=%v&not_associated_to_channel=%v&filter_allow_reference=%v&q=%v&filter_parent_team_permitted=%v&group_source=%v&include_channel_member_count=%v&include_timezones=%v&include_archived=%v&filter_archived=%v",
"%s?include_member_count=%v&not_associated_to_team=%v&not_associated_to_channel=%v&filter_allow_reference=%v&q=%v&filter_parent_team_permitted=%v&group_source=%v&include_channel_member_count=%v&include_timezones=%v&include_archived=%v&filter_archived=%v&only_syncable_sources=%v",
c.groupsRoute(),
opts.IncludeMemberCount,
opts.NotAssociatedToTeam,
@@ -5875,6 +5875,7 @@ func (c *Client4) GetGroups(ctx context.Context, opts GroupSearchOpts) ([]*Group
opts.IncludeTimezones,
opts.IncludeArchived,
opts.FilterArchived,
opts.OnlySyncableSources,
)
if opts.Since > 0 {
path = fmt.Sprintf("%s&since=%v", path, opts.Since)

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

@@ -6,12 +6,16 @@ package model
import (
"net/http"
"regexp"
"strings"
)
const (
GroupSourceLdap GroupSource = "ldap"
GroupSourceCustom GroupSource = "custom"
// plugin groups must prefix their source with this
GroupSourcePluginPrefix GroupSource = "plugin_"
GroupNameMaxLength = 64
GroupSourceMaxLength = 64
GroupDisplayNameMaxLength = 128
@@ -21,15 +25,6 @@ const (
type GroupSource string
var allGroupSources = []GroupSource{
GroupSourceLdap,
GroupSourceCustom,
}
var groupSourcesRequiringRemoteID = []GroupSource{
GroupSourceLdap,
}
type Group struct {
Id string `json:"id"`
Name *string `json:"name,omitempty"`
@@ -157,6 +152,9 @@ type GroupSearchOpts struct {
// Only return archived groups
FilterArchived bool
// OnlySyncableSources filters the groups to only those that are syncable
OnlySyncableSources bool
}
type GetGroupOpts struct {
@@ -214,12 +212,12 @@ func (group *Group) IsValidForCreate() *AppError {
}
isValidSource := false
for _, groupSource := range allGroupSources {
if group.Source == groupSource {
isValidSource = true
break
}
if group.Source == GroupSourceLdap ||
group.Source == GroupSourceCustom ||
strings.HasPrefix(string(group.Source), string(GroupSourcePluginPrefix)) {
isValidSource = true
}
if !isValidSource {
return NewAppError("Group.IsValidForCreate", "model.group.source.app_error", nil, "", http.StatusBadRequest)
}
@@ -232,12 +230,19 @@ func (group *Group) IsValidForCreate() *AppError {
}
func (group *Group) requiresRemoteId() bool {
for _, groupSource := range groupSourcesRequiringRemoteID {
if groupSource == group.Source {
return true
}
}
return false
return group.Source == GroupSourceLdap || strings.HasPrefix(string(group.Source), string(GroupSourcePluginPrefix))
}
func GetSyncableGroupSources() []GroupSource {
return []GroupSource{GroupSourceLdap}
}
func GetSyncableGroupSourcePrefixes() []GroupSource {
return []GroupSource{GroupSourcePluginPrefix}
}
func (group *Group) IsSyncable() bool {
return group.Source == GroupSourceLdap || strings.HasPrefix(string(group.Source), string(GroupSourcePluginPrefix))
}
func (group *Group) IsValidForUpdate() *AppError {

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

@@ -1300,6 +1300,87 @@ type API interface {
// Minimum server version: 9.5
UninviteRemoteFromChannel(channelID string, remoteID string) error
// UpsertGroupMember adds a user to a group or updates their existing membership.
//
// @tag Group
// @tag User
// Minimum server version: 10.7
UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError)
// UpsertGroupMembers adds multiple users to a group or updates their existing memberships.
//
// @tag Group
// @tag User
// Minimum server version: 10.7
UpsertGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError)
// GetGroupByRemoteID gets a group by its remote ID.
//
// @tag Group
// Minimum server version: 10.7
GetGroupByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, *model.AppError)
// CreateGroup creates a new group.
//
// @tag Group
// Minimum server version: 10.7
CreateGroup(group *model.Group) (*model.Group, *model.AppError)
// UpdateGroup updates a group.
//
// @tag Group
// Minimum server version: 10.7
UpdateGroup(group *model.Group) (*model.Group, *model.AppError)
// DeleteGroup soft deletes a group.
//
// @tag Group
// Minimum server version: 10.7
DeleteGroup(groupID string) (*model.Group, *model.AppError)
// RestoreGroup restores a soft deleted group.
//
// @tag Group
// Minimum server version: 10.7
RestoreGroup(groupID string) (*model.Group, *model.AppError)
// DeleteGroupMember removes a user from a group.
//
// @tag Group
// @tag User
// Minimum server version: 10.7
DeleteGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError)
// GetGroupSyncable gets a group syncable.
//
// @tag Group
// Minimum server version: 10.7
GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError)
// GetGroupSyncables gets all group syncables for the given group.
//
// @tag Group
// Minimum server version: 10.7
GetGroupSyncables(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, *model.AppError)
// UpsertGroupSyncable creates or updates a group syncable.
//
// @tag Group
// Minimum server version: 10.7
UpsertGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError)
// UpdateGroupSyncable updates a group syncable.
//
// @tag Group
// Minimum server version: 10.7
UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError)
// DeleteGroupSyncable deletes a group syncable.
//
// @tag Group
// Minimum server version: 10.7
DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError)
// UpdateUserRoles updates the role for a user.
//
// @tag Team
@@ -1312,6 +1393,12 @@ type API interface {
// @tag Plugin
// Minimum server version: 10.1
GetPluginID() string
// GetGroups returns a list of all groups with the given options and restrictions.
//
// @tag Group
// Minimum server version: 10.7
GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, *model.AppError)
}
var handshake = plugin.HandshakeConfig{

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

@@ -1372,6 +1372,97 @@ func (api *apiTimerLayer) UninviteRemoteFromChannel(channelID string, remoteID s
return _returnsA
}
func (api *apiTimerLayer) UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.UpsertGroupMember(groupID, userID)
api.recordTime(startTime, "UpsertGroupMember", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) UpsertGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.UpsertGroupMembers(groupID, userIDs)
api.recordTime(startTime, "UpsertGroupMembers", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetGroupByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetGroupByRemoteID(remoteID, groupSource)
api.recordTime(startTime, "GetGroupByRemoteID", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) CreateGroup(group *model.Group) (*model.Group, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.CreateGroup(group)
api.recordTime(startTime, "CreateGroup", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) UpdateGroup(group *model.Group) (*model.Group, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.UpdateGroup(group)
api.recordTime(startTime, "UpdateGroup", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) DeleteGroup(groupID string) (*model.Group, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.DeleteGroup(groupID)
api.recordTime(startTime, "DeleteGroup", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) RestoreGroup(groupID string) (*model.Group, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.RestoreGroup(groupID)
api.recordTime(startTime, "RestoreGroup", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) DeleteGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.DeleteGroupMember(groupID, userID)
api.recordTime(startTime, "DeleteGroupMember", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetGroupSyncable(groupID, syncableID, syncableType)
api.recordTime(startTime, "GetGroupSyncable", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetGroupSyncables(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetGroupSyncables(groupID, syncableType)
api.recordTime(startTime, "GetGroupSyncables", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) UpsertGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.UpsertGroupSyncable(groupSyncable)
api.recordTime(startTime, "UpsertGroupSyncable", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.UpdateGroupSyncable(groupSyncable)
api.recordTime(startTime, "UpdateGroupSyncable", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.DeleteGroupSyncable(groupID, syncableID, syncableType)
api.recordTime(startTime, "DeleteGroupSyncable", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) UpdateUserRoles(userID, newRoles string) (*model.User, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.UpdateUserRoles(userID, newRoles)
@@ -1385,3 +1476,10 @@ func (api *apiTimerLayer) GetPluginID() string {
api.recordTime(startTime, "GetPluginID", true)
return _returnsA
}
func (api *apiTimerLayer) GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetGroups(page, perPage, opts, viewRestrictions)
api.recordTime(startTime, "GetGroups", _returnsB == nil)
return _returnsA, _returnsB
}

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

@@ -10,6 +10,7 @@ import (
"fmt"
"log"
saml2 "github.com/mattermost/gosaml2"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
)
@@ -1161,6 +1162,43 @@ func (s *hooksRPCServer) GenerateSupportData(args *Z_GenerateSupportDataArgs, re
return nil
}
func init() {
hookNameToId["OnSAMLLogin"] = OnSAMLLoginID
}
type Z_OnSAMLLoginArgs struct {
A *Context
B *model.User
C *saml2.AssertionInfo
}
type Z_OnSAMLLoginReturns struct {
A error
}
func (g *hooksRPCClient) OnSAMLLogin(c *Context, user *model.User, assertion *saml2.AssertionInfo) error {
_args := &Z_OnSAMLLoginArgs{c, user, assertion}
_returns := &Z_OnSAMLLoginReturns{}
if g.implemented[OnSAMLLoginID] {
if err := g.client.Call("Plugin.OnSAMLLogin", _args, _returns); err != nil {
g.log.Error("RPC call OnSAMLLogin to plugin failed.", mlog.Err(err))
}
}
return _returns.A
}
func (s *hooksRPCServer) OnSAMLLogin(args *Z_OnSAMLLoginArgs, returns *Z_OnSAMLLoginReturns) error {
if hook, ok := s.impl.(interface {
OnSAMLLogin(c *Context, user *model.User, assertion *saml2.AssertionInfo) error
}); ok {
returns.A = hook.OnSAMLLogin(args.A, args.B, args.C)
returns.A = encodableError(returns.A)
} else {
return encodableError(fmt.Errorf("Hook OnSAMLLogin called but not implemented."))
}
return nil
}
type Z_RegisterCommandArgs struct {
A *model.Command
}
@@ -6601,6 +6639,392 @@ func (s *apiRPCServer) UninviteRemoteFromChannel(args *Z_UninviteRemoteFromChann
return nil
}
type Z_UpsertGroupMemberArgs struct {
A string
B string
}
type Z_UpsertGroupMemberReturns struct {
A *model.GroupMember
B *model.AppError
}
func (g *apiRPCClient) UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) {
_args := &Z_UpsertGroupMemberArgs{groupID, userID}
_returns := &Z_UpsertGroupMemberReturns{}
if err := g.client.Call("Plugin.UpsertGroupMember", _args, _returns); err != nil {
log.Printf("RPC call to UpsertGroupMember API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) UpsertGroupMember(args *Z_UpsertGroupMemberArgs, returns *Z_UpsertGroupMemberReturns) error {
if hook, ok := s.impl.(interface {
UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError)
}); ok {
returns.A, returns.B = hook.UpsertGroupMember(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API UpsertGroupMember called but not implemented."))
}
return nil
}
type Z_UpsertGroupMembersArgs struct {
A string
B []string
}
type Z_UpsertGroupMembersReturns struct {
A []*model.GroupMember
B *model.AppError
}
func (g *apiRPCClient) UpsertGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) {
_args := &Z_UpsertGroupMembersArgs{groupID, userIDs}
_returns := &Z_UpsertGroupMembersReturns{}
if err := g.client.Call("Plugin.UpsertGroupMembers", _args, _returns); err != nil {
log.Printf("RPC call to UpsertGroupMembers API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) UpsertGroupMembers(args *Z_UpsertGroupMembersArgs, returns *Z_UpsertGroupMembersReturns) error {
if hook, ok := s.impl.(interface {
UpsertGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError)
}); ok {
returns.A, returns.B = hook.UpsertGroupMembers(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API UpsertGroupMembers called but not implemented."))
}
return nil
}
type Z_GetGroupByRemoteIDArgs struct {
A string
B model.GroupSource
}
type Z_GetGroupByRemoteIDReturns struct {
A *model.Group
B *model.AppError
}
func (g *apiRPCClient) GetGroupByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, *model.AppError) {
_args := &Z_GetGroupByRemoteIDArgs{remoteID, groupSource}
_returns := &Z_GetGroupByRemoteIDReturns{}
if err := g.client.Call("Plugin.GetGroupByRemoteID", _args, _returns); err != nil {
log.Printf("RPC call to GetGroupByRemoteID API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetGroupByRemoteID(args *Z_GetGroupByRemoteIDArgs, returns *Z_GetGroupByRemoteIDReturns) error {
if hook, ok := s.impl.(interface {
GetGroupByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetGroupByRemoteID(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API GetGroupByRemoteID called but not implemented."))
}
return nil
}
type Z_CreateGroupArgs struct {
A *model.Group
}
type Z_CreateGroupReturns struct {
A *model.Group
B *model.AppError
}
func (g *apiRPCClient) CreateGroup(group *model.Group) (*model.Group, *model.AppError) {
_args := &Z_CreateGroupArgs{group}
_returns := &Z_CreateGroupReturns{}
if err := g.client.Call("Plugin.CreateGroup", _args, _returns); err != nil {
log.Printf("RPC call to CreateGroup API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) CreateGroup(args *Z_CreateGroupArgs, returns *Z_CreateGroupReturns) error {
if hook, ok := s.impl.(interface {
CreateGroup(group *model.Group) (*model.Group, *model.AppError)
}); ok {
returns.A, returns.B = hook.CreateGroup(args.A)
} else {
return encodableError(fmt.Errorf("API CreateGroup called but not implemented."))
}
return nil
}
type Z_UpdateGroupArgs struct {
A *model.Group
}
type Z_UpdateGroupReturns struct {
A *model.Group
B *model.AppError
}
func (g *apiRPCClient) UpdateGroup(group *model.Group) (*model.Group, *model.AppError) {
_args := &Z_UpdateGroupArgs{group}
_returns := &Z_UpdateGroupReturns{}
if err := g.client.Call("Plugin.UpdateGroup", _args, _returns); err != nil {
log.Printf("RPC call to UpdateGroup API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) UpdateGroup(args *Z_UpdateGroupArgs, returns *Z_UpdateGroupReturns) error {
if hook, ok := s.impl.(interface {
UpdateGroup(group *model.Group) (*model.Group, *model.AppError)
}); ok {
returns.A, returns.B = hook.UpdateGroup(args.A)
} else {
return encodableError(fmt.Errorf("API UpdateGroup called but not implemented."))
}
return nil
}
type Z_DeleteGroupArgs struct {
A string
}
type Z_DeleteGroupReturns struct {
A *model.Group
B *model.AppError
}
func (g *apiRPCClient) DeleteGroup(groupID string) (*model.Group, *model.AppError) {
_args := &Z_DeleteGroupArgs{groupID}
_returns := &Z_DeleteGroupReturns{}
if err := g.client.Call("Plugin.DeleteGroup", _args, _returns); err != nil {
log.Printf("RPC call to DeleteGroup API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) DeleteGroup(args *Z_DeleteGroupArgs, returns *Z_DeleteGroupReturns) error {
if hook, ok := s.impl.(interface {
DeleteGroup(groupID string) (*model.Group, *model.AppError)
}); ok {
returns.A, returns.B = hook.DeleteGroup(args.A)
} else {
return encodableError(fmt.Errorf("API DeleteGroup called but not implemented."))
}
return nil
}
type Z_RestoreGroupArgs struct {
A string
}
type Z_RestoreGroupReturns struct {
A *model.Group
B *model.AppError
}
func (g *apiRPCClient) RestoreGroup(groupID string) (*model.Group, *model.AppError) {
_args := &Z_RestoreGroupArgs{groupID}
_returns := &Z_RestoreGroupReturns{}
if err := g.client.Call("Plugin.RestoreGroup", _args, _returns); err != nil {
log.Printf("RPC call to RestoreGroup API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) RestoreGroup(args *Z_RestoreGroupArgs, returns *Z_RestoreGroupReturns) error {
if hook, ok := s.impl.(interface {
RestoreGroup(groupID string) (*model.Group, *model.AppError)
}); ok {
returns.A, returns.B = hook.RestoreGroup(args.A)
} else {
return encodableError(fmt.Errorf("API RestoreGroup called but not implemented."))
}
return nil
}
type Z_DeleteGroupMemberArgs struct {
A string
B string
}
type Z_DeleteGroupMemberReturns struct {
A *model.GroupMember
B *model.AppError
}
func (g *apiRPCClient) DeleteGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) {
_args := &Z_DeleteGroupMemberArgs{groupID, userID}
_returns := &Z_DeleteGroupMemberReturns{}
if err := g.client.Call("Plugin.DeleteGroupMember", _args, _returns); err != nil {
log.Printf("RPC call to DeleteGroupMember API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) DeleteGroupMember(args *Z_DeleteGroupMemberArgs, returns *Z_DeleteGroupMemberReturns) error {
if hook, ok := s.impl.(interface {
DeleteGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError)
}); ok {
returns.A, returns.B = hook.DeleteGroupMember(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API DeleteGroupMember called but not implemented."))
}
return nil
}
type Z_GetGroupSyncableArgs struct {
A string
B string
C model.GroupSyncableType
}
type Z_GetGroupSyncableReturns struct {
A *model.GroupSyncable
B *model.AppError
}
func (g *apiRPCClient) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) {
_args := &Z_GetGroupSyncableArgs{groupID, syncableID, syncableType}
_returns := &Z_GetGroupSyncableReturns{}
if err := g.client.Call("Plugin.GetGroupSyncable", _args, _returns); err != nil {
log.Printf("RPC call to GetGroupSyncable API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetGroupSyncable(args *Z_GetGroupSyncableArgs, returns *Z_GetGroupSyncableReturns) error {
if hook, ok := s.impl.(interface {
GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetGroupSyncable(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API GetGroupSyncable called but not implemented."))
}
return nil
}
type Z_GetGroupSyncablesArgs struct {
A string
B model.GroupSyncableType
}
type Z_GetGroupSyncablesReturns struct {
A []*model.GroupSyncable
B *model.AppError
}
func (g *apiRPCClient) GetGroupSyncables(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, *model.AppError) {
_args := &Z_GetGroupSyncablesArgs{groupID, syncableType}
_returns := &Z_GetGroupSyncablesReturns{}
if err := g.client.Call("Plugin.GetGroupSyncables", _args, _returns); err != nil {
log.Printf("RPC call to GetGroupSyncables API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetGroupSyncables(args *Z_GetGroupSyncablesArgs, returns *Z_GetGroupSyncablesReturns) error {
if hook, ok := s.impl.(interface {
GetGroupSyncables(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetGroupSyncables(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API GetGroupSyncables called but not implemented."))
}
return nil
}
type Z_UpsertGroupSyncableArgs struct {
A *model.GroupSyncable
}
type Z_UpsertGroupSyncableReturns struct {
A *model.GroupSyncable
B *model.AppError
}
func (g *apiRPCClient) UpsertGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) {
_args := &Z_UpsertGroupSyncableArgs{groupSyncable}
_returns := &Z_UpsertGroupSyncableReturns{}
if err := g.client.Call("Plugin.UpsertGroupSyncable", _args, _returns); err != nil {
log.Printf("RPC call to UpsertGroupSyncable API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) UpsertGroupSyncable(args *Z_UpsertGroupSyncableArgs, returns *Z_UpsertGroupSyncableReturns) error {
if hook, ok := s.impl.(interface {
UpsertGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError)
}); ok {
returns.A, returns.B = hook.UpsertGroupSyncable(args.A)
} else {
return encodableError(fmt.Errorf("API UpsertGroupSyncable called but not implemented."))
}
return nil
}
type Z_UpdateGroupSyncableArgs struct {
A *model.GroupSyncable
}
type Z_UpdateGroupSyncableReturns struct {
A *model.GroupSyncable
B *model.AppError
}
func (g *apiRPCClient) UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) {
_args := &Z_UpdateGroupSyncableArgs{groupSyncable}
_returns := &Z_UpdateGroupSyncableReturns{}
if err := g.client.Call("Plugin.UpdateGroupSyncable", _args, _returns); err != nil {
log.Printf("RPC call to UpdateGroupSyncable API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) UpdateGroupSyncable(args *Z_UpdateGroupSyncableArgs, returns *Z_UpdateGroupSyncableReturns) error {
if hook, ok := s.impl.(interface {
UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError)
}); ok {
returns.A, returns.B = hook.UpdateGroupSyncable(args.A)
} else {
return encodableError(fmt.Errorf("API UpdateGroupSyncable called but not implemented."))
}
return nil
}
type Z_DeleteGroupSyncableArgs struct {
A string
B string
C model.GroupSyncableType
}
type Z_DeleteGroupSyncableReturns struct {
A *model.GroupSyncable
B *model.AppError
}
func (g *apiRPCClient) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) {
_args := &Z_DeleteGroupSyncableArgs{groupID, syncableID, syncableType}
_returns := &Z_DeleteGroupSyncableReturns{}
if err := g.client.Call("Plugin.DeleteGroupSyncable", _args, _returns); err != nil {
log.Printf("RPC call to DeleteGroupSyncable API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) DeleteGroupSyncable(args *Z_DeleteGroupSyncableArgs, returns *Z_DeleteGroupSyncableReturns) error {
if hook, ok := s.impl.(interface {
DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError)
}); ok {
returns.A, returns.B = hook.DeleteGroupSyncable(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API DeleteGroupSyncable called but not implemented."))
}
return nil
}
type Z_UpdateUserRolesArgs struct {
A string
B string
@@ -6657,3 +7081,35 @@ func (s *apiRPCServer) GetPluginID(args *Z_GetPluginIDArgs, returns *Z_GetPlugin
}
return nil
}
type Z_GetGroupsArgs struct {
A int
B int
C model.GroupSearchOpts
D *model.ViewUsersRestrictions
}
type Z_GetGroupsReturns struct {
A []*model.Group
B *model.AppError
}
func (g *apiRPCClient) GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, *model.AppError) {
_args := &Z_GetGroupsArgs{page, perPage, opts, viewRestrictions}
_returns := &Z_GetGroupsReturns{}
if err := g.client.Call("Plugin.GetGroups", _args, _returns); err != nil {
log.Printf("RPC call to GetGroups API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetGroups(args *Z_GetGroupsArgs, returns *Z_GetGroupsReturns) error {
if hook, ok := s.impl.(interface {
GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetGroups(args.A, args.B, args.C, args.D)
} else {
return encodableError(fmt.Errorf("API GetGroups called but not implemented."))
}
return nil
}

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

@@ -7,6 +7,7 @@ import (
"io"
"net/http"
saml2 "github.com/mattermost/gosaml2"
"github.com/mattermost/mattermost/server/public/model"
)
@@ -61,6 +62,7 @@ const (
OnSharedChannelsAttachmentSyncMsgID = 43
OnSharedChannelsProfileImageSyncMsgID = 44
GenerateSupportDataID = 45
OnSAMLLoginID = 46
TotalHooksID = iota
)
@@ -395,4 +397,9 @@ type Hooks interface {
//
// Minimum server version: 9.8
GenerateSupportData(c *Context) ([]*model.FileData, error)
// OnSAMLLogin is invoked after a successful SAML login.
//
// Minimum server version: 10.7
OnSAMLLogin(c *Context, user *model.User, assertion *saml2.AssertionInfo) error
}

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

@@ -11,6 +11,7 @@ import (
"net/http"
timePkg "time"
saml2 "github.com/mattermost/gosaml2"
"github.com/mattermost/mattermost/server/public/model"
)
@@ -291,3 +292,10 @@ func (hooks *hooksTimerLayer) GenerateSupportData(c *Context) ([]*model.FileData
hooks.recordTime(startTime, "GenerateSupportData", _returnsB == nil)
return _returnsA, _returnsB
}
func (hooks *hooksTimerLayer) OnSAMLLogin(c *Context, user *model.User, assertion *saml2.AssertionInfo) error {
startTime := timePkg.Now()
_returnsA := hooks.hooksImpl.OnSAMLLogin(c, user, assertion)
hooks.recordTime(startTime, "OnSAMLLogin", _returnsA == nil)
return _returnsA
}

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

@@ -307,6 +307,15 @@ var hooksTemplate = `// Copyright (c) 2015-present Mattermost, Inc. All Rights R
package plugin
import (
"fmt"
"log"
saml2 "github.com/mattermost/gosaml2"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
)
{{range .HooksMethods}}
func init() {
@@ -432,6 +441,7 @@ import (
"net/http"
timePkg "time"
saml2 "github.com/mattermost/gosaml2"
"github.com/mattermost/mattermost/server/public/model"
)

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

@@ -272,6 +272,38 @@ func (_m *API) CreateCommand(cmd *model.Command) (*model.Command, error) {
return r0, r1
}
// CreateGroup provides a mock function with given fields: group
func (_m *API) CreateGroup(group *model.Group) (*model.Group, *model.AppError) {
ret := _m.Called(group)
if len(ret) == 0 {
panic("no return value specified for CreateGroup")
}
var r0 *model.Group
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(*model.Group) (*model.Group, *model.AppError)); ok {
return rf(group)
}
if rf, ok := ret.Get(0).(func(*model.Group) *model.Group); ok {
r0 = rf(group)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Group)
}
}
if rf, ok := ret.Get(1).(func(*model.Group) *model.AppError); ok {
r1 = rf(group)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// CreateOAuthApp provides a mock function with given fields: app
func (_m *API) CreateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
ret := _m.Called(app)
@@ -653,6 +685,102 @@ func (_m *API) DeleteEphemeralPost(userID string, postId string) {
_m.Called(userID, postId)
}
// DeleteGroup provides a mock function with given fields: groupID
func (_m *API) DeleteGroup(groupID string) (*model.Group, *model.AppError) {
ret := _m.Called(groupID)
if len(ret) == 0 {
panic("no return value specified for DeleteGroup")
}
var r0 *model.Group
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(string) (*model.Group, *model.AppError)); ok {
return rf(groupID)
}
if rf, ok := ret.Get(0).(func(string) *model.Group); ok {
r0 = rf(groupID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Group)
}
}
if rf, ok := ret.Get(1).(func(string) *model.AppError); ok {
r1 = rf(groupID)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// DeleteGroupMember provides a mock function with given fields: groupID, userID
func (_m *API) DeleteGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) {
ret := _m.Called(groupID, userID)
if len(ret) == 0 {
panic("no return value specified for DeleteGroupMember")
}
var r0 *model.GroupMember
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(string, string) (*model.GroupMember, *model.AppError)); ok {
return rf(groupID, userID)
}
if rf, ok := ret.Get(0).(func(string, string) *model.GroupMember); ok {
r0 = rf(groupID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.GroupMember)
}
}
if rf, ok := ret.Get(1).(func(string, string) *model.AppError); ok {
r1 = rf(groupID, userID)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// DeleteGroupSyncable provides a mock function with given fields: groupID, syncableID, syncableType
func (_m *API) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) {
ret := _m.Called(groupID, syncableID, syncableType)
if len(ret) == 0 {
panic("no return value specified for DeleteGroupSyncable")
}
var r0 *model.GroupSyncable
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(string, string, model.GroupSyncableType) (*model.GroupSyncable, *model.AppError)); ok {
return rf(groupID, syncableID, syncableType)
}
if rf, ok := ret.Get(0).(func(string, string, model.GroupSyncableType) *model.GroupSyncable); ok {
r0 = rf(groupID, syncableID, syncableType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.GroupSyncable)
}
}
if rf, ok := ret.Get(1).(func(string, string, model.GroupSyncableType) *model.AppError); ok {
r1 = rf(groupID, syncableID, syncableType)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// DeleteOAuthApp provides a mock function with given fields: appID
func (_m *API) DeleteOAuthApp(appID string) *model.AppError {
ret := _m.Called(appID)
@@ -1758,6 +1886,38 @@ func (_m *API) GetGroupByName(name string) (*model.Group, *model.AppError) {
return r0, r1
}
// GetGroupByRemoteID provides a mock function with given fields: remoteID, groupSource
func (_m *API) GetGroupByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, *model.AppError) {
ret := _m.Called(remoteID, groupSource)
if len(ret) == 0 {
panic("no return value specified for GetGroupByRemoteID")
}
var r0 *model.Group
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(string, model.GroupSource) (*model.Group, *model.AppError)); ok {
return rf(remoteID, groupSource)
}
if rf, ok := ret.Get(0).(func(string, model.GroupSource) *model.Group); ok {
r0 = rf(remoteID, groupSource)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Group)
}
}
if rf, ok := ret.Get(1).(func(string, model.GroupSource) *model.AppError); ok {
r1 = rf(remoteID, groupSource)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetGroupChannel provides a mock function with given fields: userIds
func (_m *API) GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) {
ret := _m.Called(userIds)
@@ -1822,6 +1982,102 @@ func (_m *API) GetGroupMemberUsers(groupID string, page int, perPage int) ([]*mo
return r0, r1
}
// GetGroupSyncable provides a mock function with given fields: groupID, syncableID, syncableType
func (_m *API) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) {
ret := _m.Called(groupID, syncableID, syncableType)
if len(ret) == 0 {
panic("no return value specified for GetGroupSyncable")
}
var r0 *model.GroupSyncable
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(string, string, model.GroupSyncableType) (*model.GroupSyncable, *model.AppError)); ok {
return rf(groupID, syncableID, syncableType)
}
if rf, ok := ret.Get(0).(func(string, string, model.GroupSyncableType) *model.GroupSyncable); ok {
r0 = rf(groupID, syncableID, syncableType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.GroupSyncable)
}
}
if rf, ok := ret.Get(1).(func(string, string, model.GroupSyncableType) *model.AppError); ok {
r1 = rf(groupID, syncableID, syncableType)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetGroupSyncables provides a mock function with given fields: groupID, syncableType
func (_m *API) GetGroupSyncables(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, *model.AppError) {
ret := _m.Called(groupID, syncableType)
if len(ret) == 0 {
panic("no return value specified for GetGroupSyncables")
}
var r0 []*model.GroupSyncable
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(string, model.GroupSyncableType) ([]*model.GroupSyncable, *model.AppError)); ok {
return rf(groupID, syncableType)
}
if rf, ok := ret.Get(0).(func(string, model.GroupSyncableType) []*model.GroupSyncable); ok {
r0 = rf(groupID, syncableType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.GroupSyncable)
}
}
if rf, ok := ret.Get(1).(func(string, model.GroupSyncableType) *model.AppError); ok {
r1 = rf(groupID, syncableType)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetGroups provides a mock function with given fields: page, perPage, opts, viewRestrictions
func (_m *API) GetGroups(page int, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, *model.AppError) {
ret := _m.Called(page, perPage, opts, viewRestrictions)
if len(ret) == 0 {
panic("no return value specified for GetGroups")
}
var r0 []*model.Group
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(int, int, model.GroupSearchOpts, *model.ViewUsersRestrictions) ([]*model.Group, *model.AppError)); ok {
return rf(page, perPage, opts, viewRestrictions)
}
if rf, ok := ret.Get(0).(func(int, int, model.GroupSearchOpts, *model.ViewUsersRestrictions) []*model.Group); ok {
r0 = rf(page, perPage, opts, viewRestrictions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Group)
}
}
if rf, ok := ret.Get(1).(func(int, int, model.GroupSearchOpts, *model.ViewUsersRestrictions) *model.AppError); ok {
r1 = rf(page, perPage, opts, viewRestrictions)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetGroupsBySource provides a mock function with given fields: groupSource
func (_m *API) GetGroupsBySource(groupSource model.GroupSource) ([]*model.Group, *model.AppError) {
ret := _m.Called(groupSource)
@@ -4087,6 +4343,38 @@ func (_m *API) RequestTrialLicense(requesterID string, users int, termsAccepted
return r0
}
// RestoreGroup provides a mock function with given fields: groupID
func (_m *API) RestoreGroup(groupID string) (*model.Group, *model.AppError) {
ret := _m.Called(groupID)
if len(ret) == 0 {
panic("no return value specified for RestoreGroup")
}
var r0 *model.Group
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(string) (*model.Group, *model.AppError)); ok {
return rf(groupID)
}
if rf, ok := ret.Get(0).(func(string) *model.Group); ok {
r0 = rf(groupID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Group)
}
}
if rf, ok := ret.Get(1).(func(string) *model.AppError); ok {
r1 = rf(groupID)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// RevokeSession provides a mock function with given fields: sessionID
func (_m *API) RevokeSession(sessionID string) *model.AppError {
ret := _m.Called(sessionID)
@@ -4837,6 +5125,70 @@ func (_m *API) UpdateEphemeralPost(userID string, post *model.Post) *model.Post
return r0
}
// UpdateGroup provides a mock function with given fields: group
func (_m *API) UpdateGroup(group *model.Group) (*model.Group, *model.AppError) {
ret := _m.Called(group)
if len(ret) == 0 {
panic("no return value specified for UpdateGroup")
}
var r0 *model.Group
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(*model.Group) (*model.Group, *model.AppError)); ok {
return rf(group)
}
if rf, ok := ret.Get(0).(func(*model.Group) *model.Group); ok {
r0 = rf(group)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Group)
}
}
if rf, ok := ret.Get(1).(func(*model.Group) *model.AppError); ok {
r1 = rf(group)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// UpdateGroupSyncable provides a mock function with given fields: groupSyncable
func (_m *API) UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) {
ret := _m.Called(groupSyncable)
if len(ret) == 0 {
panic("no return value specified for UpdateGroupSyncable")
}
var r0 *model.GroupSyncable
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(*model.GroupSyncable) (*model.GroupSyncable, *model.AppError)); ok {
return rf(groupSyncable)
}
if rf, ok := ret.Get(0).(func(*model.GroupSyncable) *model.GroupSyncable); ok {
r0 = rf(groupSyncable)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.GroupSyncable)
}
}
if rf, ok := ret.Get(1).(func(*model.GroupSyncable) *model.AppError); ok {
r1 = rf(groupSyncable)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// UpdateOAuthApp provides a mock function with given fields: app
func (_m *API) UpdateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
ret := _m.Called(app)
@@ -5263,6 +5615,102 @@ func (_m *API) UploadFile(data []byte, channelId string, filename string) (*mode
return r0, r1
}
// UpsertGroupMember provides a mock function with given fields: groupID, userID
func (_m *API) UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) {
ret := _m.Called(groupID, userID)
if len(ret) == 0 {
panic("no return value specified for UpsertGroupMember")
}
var r0 *model.GroupMember
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(string, string) (*model.GroupMember, *model.AppError)); ok {
return rf(groupID, userID)
}
if rf, ok := ret.Get(0).(func(string, string) *model.GroupMember); ok {
r0 = rf(groupID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.GroupMember)
}
}
if rf, ok := ret.Get(1).(func(string, string) *model.AppError); ok {
r1 = rf(groupID, userID)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// UpsertGroupMembers provides a mock function with given fields: groupID, userIDs
func (_m *API) UpsertGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) {
ret := _m.Called(groupID, userIDs)
if len(ret) == 0 {
panic("no return value specified for UpsertGroupMembers")
}
var r0 []*model.GroupMember
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(string, []string) ([]*model.GroupMember, *model.AppError)); ok {
return rf(groupID, userIDs)
}
if rf, ok := ret.Get(0).(func(string, []string) []*model.GroupMember); ok {
r0 = rf(groupID, userIDs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.GroupMember)
}
}
if rf, ok := ret.Get(1).(func(string, []string) *model.AppError); ok {
r1 = rf(groupID, userIDs)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// UpsertGroupSyncable provides a mock function with given fields: groupSyncable
func (_m *API) UpsertGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) {
ret := _m.Called(groupSyncable)
if len(ret) == 0 {
panic("no return value specified for UpsertGroupSyncable")
}
var r0 *model.GroupSyncable
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(*model.GroupSyncable) (*model.GroupSyncable, *model.AppError)); ok {
return rf(groupSyncable)
}
if rf, ok := ret.Get(0).(func(*model.GroupSyncable) *model.GroupSyncable); ok {
r0 = rf(groupSyncable)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.GroupSyncable)
}
}
if rf, ok := ret.Get(1).(func(*model.GroupSyncable) *model.AppError); ok {
r1 = rf(groupSyncable)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// NewAPI creates a new instance of API. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewAPI(t interface {

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

@@ -13,6 +13,8 @@ import (
model "github.com/mattermost/mattermost/server/public/model"
plugin "github.com/mattermost/mattermost/server/public/plugin"
saml2 "github.com/mattermost/gosaml2"
)
// Hooks is an autogenerated mock type for the Hooks type
@@ -384,6 +386,24 @@ func (_m *Hooks) OnPluginClusterEvent(c *plugin.Context, ev model.PluginClusterE
_m.Called(c, ev)
}
// OnSAMLLogin provides a mock function with given fields: c, user, assertion
func (_m *Hooks) OnSAMLLogin(c *plugin.Context, user *model.User, assertion *saml2.AssertionInfo) error {
ret := _m.Called(c, user, assertion)
if len(ret) == 0 {
panic("no return value specified for OnSAMLLogin")
}
var r0 error
if rf, ok := ret.Get(0).(func(*plugin.Context, *model.User, *saml2.AssertionInfo) error); ok {
r0 = rf(c, user, assertion)
} else {
r0 = ret.Error(0)
}
return r0
}
// OnSendDailyTelemetry provides a mock function with given fields:
func (_m *Hooks) OnSendDailyTelemetry() {
_m.Called()

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

@@ -55,3 +55,115 @@ func (g *GroupService) ListForUser(userID string) ([]*model.Group, error) {
return groups, normalizeAppErr(appErr)
}
// UpsertMember adds a user to a group or updates their existing membership.
//
// Minimum server version: 10.7
func (g *GroupService) UpsertMember(groupID string, userID string) (*model.GroupMember, error) {
member, appErr := g.api.UpsertGroupMember(groupID, userID)
return member, normalizeAppErr(appErr)
}
// UpsertMembers adds multiple users to a group or updates their existing memberships.
//
// Minimum server version: 10.7
func (g *GroupService) UpsertMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
members, appErr := g.api.UpsertGroupMembers(groupID, userIDs)
return members, normalizeAppErr(appErr)
}
// GetByRemoteID gets a group by its remote ID.
//
// Minimum server version: 10.7
func (g *GroupService) GetByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, error) {
group, appErr := g.api.GetGroupByRemoteID(remoteID, groupSource)
return group, normalizeAppErr(appErr)
}
// Create creates a new group.
//
// Minimum server version: 10.7
func (g *GroupService) Create(group *model.Group) (*model.Group, error) {
group, appErr := g.api.CreateGroup(group)
return group, normalizeAppErr(appErr)
}
// Update updates a group.
//
// Minimum server version: 10.7
func (g *GroupService) Update(group *model.Group) (*model.Group, error) {
group, appErr := g.api.UpdateGroup(group)
return group, normalizeAppErr(appErr)
}
// Delete soft deletes a group.
//
// Minimum server version: 10.7
func (g *GroupService) Delete(groupID string) (*model.Group, error) {
group, appErr := g.api.DeleteGroup(groupID)
return group, normalizeAppErr(appErr)
}
// Restore restores a soft deleted group.
//
// Minimum server version: 10.7
func (g *GroupService) Restore(groupID string) (*model.Group, error) {
group, appErr := g.api.RestoreGroup(groupID)
return group, normalizeAppErr(appErr)
}
// DeleteMember removes a user from a group.
//
// Minimum server version: 10.7
func (g *GroupService) DeleteMember(groupID string, userID string) (*model.GroupMember, error) {
member, appErr := g.api.DeleteGroupMember(groupID, userID)
return member, normalizeAppErr(appErr)
}
// GetSyncable gets a group syncable.
//
// Minimum server version: 10.7
func (g *GroupService) GetSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
syncable, appErr := g.api.GetGroupSyncable(groupID, syncableID, syncableType)
return syncable, normalizeAppErr(appErr)
}
// GetSyncables gets all group syncables for the given group.
//
// Minimum server version: 10.7
func (g *GroupService) GetSyncables(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, error) {
syncables, appErr := g.api.GetGroupSyncables(groupID, syncableType)
return syncables, normalizeAppErr(appErr)
}
// UpsertSyncable creates or updates a group syncable.
//
// Minimum server version: 10.7
func (g *GroupService) UpsertSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
syncable, appErr := g.api.UpsertGroupSyncable(groupSyncable)
return syncable, normalizeAppErr(appErr)
}
// UpdateSyncable updates a group syncable.
//
// Minimum server version: 10.7
func (g *GroupService) UpdateSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
syncable, appErr := g.api.UpdateGroupSyncable(groupSyncable)
return syncable, normalizeAppErr(appErr)
}
// DeleteSyncable deletes a group syncable.
//
// Minimum server version: 10.7
func (g *GroupService) DeleteSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
syncable, appErr := g.api.DeleteGroupSyncable(groupID, syncableID, syncableType)
return syncable, normalizeAppErr(appErr)
}
// GetGroups returns a list of all groups with the given options and restrictions.
//
// Minimum server version: 10.7
func (g *GroupService) GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error) {
groups, appErr := g.api.GetGroups(page, perPage, opts, viewRestrictions)
return groups, normalizeAppErr(appErr)
}

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

@@ -7,7 +7,7 @@ import type {IntlShape} from 'react-intl';
import {injectIntl, FormattedMessage, defineMessage} from 'react-intl';
import type {ServerError} from '@mattermost/types/errors';
import type {Group, SyncablePatch} from '@mattermost/types/groups';
import type {Group, GroupSource, SyncablePatch} from '@mattermost/types/groups';
import {SyncableType} from '@mattermost/types/groups';
import type {ActionResult} from 'mattermost-redux/types/actions';
@@ -37,7 +37,7 @@ export type Props = {
onAddCallback?: (groupIDs: string[]) => void;
actions: {
getGroupsNotAssociatedToChannel: (channelID: string, q?: string, page?: number, perPage?: number, filterParentTeamPermitted?: boolean) => Promise<ActionResult>;
getGroupsNotAssociatedToChannel: (channelID: string, q?: string, page?: number, perPage?: number, filterParentTeamPermitted?: boolean, source?: GroupSource | string, onlySyncableSources?: boolean) => Promise<ActionResult>;
setModalSearchTerm: (term: string) => void;
linkGroupSyncable: (groupID: string, syncableID: string, syncableType: SyncableType, patch: Partial<SyncablePatch>) => Promise<ActionResult>;
getAllGroupsAssociatedToChannel: (channelID: string, filterAllowReference: boolean, includeMemberCount: boolean) => Promise<ActionResult>;
@@ -80,7 +80,7 @@ export class AddGroupsToChannelModal extends React.PureComponent<Props, State> {
Promise.all([
this.props.actions.getTeam(this.props.teamID),
this.props.actions.getAllGroupsAssociatedToTeam(this.props.teamID, false, true),
this.props.actions.getGroupsNotAssociatedToChannel(this.props.currentChannelId, '', 0, GROUPS_PER_PAGE + 1, true),
this.props.actions.getGroupsNotAssociatedToChannel(this.props.currentChannelId, '', 0, GROUPS_PER_PAGE + 1, true, '', true),
this.props.actions.getAllGroupsAssociatedToChannel(this.props.currentChannelId, false, true),
]).then(() => {
this.setGroupsLoadingState(false);
@@ -99,7 +99,7 @@ export class AddGroupsToChannelModal extends React.PureComponent<Props, State> {
this.searchTimeoutId = window.setTimeout(
async () => {
this.setGroupsLoadingState(true);
await this.props.actions.getGroupsNotAssociatedToChannel(this.props.currentChannelId, searchTerm, undefined, undefined, true);
await this.props.actions.getGroupsNotAssociatedToChannel(this.props.currentChannelId, searchTerm, undefined, undefined, true, '', true);
this.setGroupsLoadingState(false);
},
Constants.SEARCH_TIMEOUT_MILLISECONDS,
@@ -168,7 +168,7 @@ export class AddGroupsToChannelModal extends React.PureComponent<Props, State> {
handlePageChange = (page: number, prevPage: number) => {
if (page > prevPage) {
this.setGroupsLoadingState(true);
this.props.actions.getGroupsNotAssociatedToChannel(this.props.currentChannelId, this.props.searchTerm, page, GROUPS_PER_PAGE + 1, true).then(() => {
this.props.actions.getGroupsNotAssociatedToChannel(this.props.currentChannelId, this.props.searchTerm, page, GROUPS_PER_PAGE + 1, true, '', true).then(() => {
this.setGroupsLoadingState(false);
});
}

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

@@ -7,7 +7,7 @@ import {Modal} from 'react-bootstrap';
import type {IntlShape} from 'react-intl';
import {injectIntl, FormattedMessage, defineMessage} from 'react-intl';
import type {Group, SyncablePatch} from '@mattermost/types/groups';
import type {Group, SyncablePatch, GroupSource} from '@mattermost/types/groups';
import {SyncableType} from '@mattermost/types/groups';
import type {ActionResult} from 'mattermost-redux/types/actions';
@@ -41,7 +41,7 @@ type Props = {
}
export type Actions = {
getGroupsNotAssociatedToTeam: (teamID: string, q?: string, page?: number, perPage?: number) => Promise<ActionResult>;
getGroupsNotAssociatedToTeam: (teamID: string, q?: string, page?: number, perPage?: number, source?: GroupSource | string, onlySyncableSources?: boolean) => Promise<ActionResult>;
setModalSearchTerm: (term: string) => void;
linkGroupSyncable: (groupID: string, syncableID: string, syncableType: SyncableType, patch: SyncablePatch) => Promise<ActionResult>;
getAllGroupsAssociatedToTeam: (teamID: string, filterAllowReference: boolean, includeMemberCount: boolean) => Promise<ActionResult>;
@@ -79,7 +79,7 @@ export class AddGroupsToTeamModal extends React.PureComponent<Props, State> {
public componentDidMount() {
Promise.all([
this.props.actions.getGroupsNotAssociatedToTeam(this.props.currentTeamId, '', 0, GROUPS_PER_PAGE + 1),
this.props.actions.getGroupsNotAssociatedToTeam(this.props.currentTeamId, '', 0, GROUPS_PER_PAGE + 1, '', true),
this.props.actions.getAllGroupsAssociatedToTeam(this.props.currentTeamId, false, true),
]).then(() => {
this.setGroupsLoadingState(false);
@@ -98,7 +98,7 @@ export class AddGroupsToTeamModal extends React.PureComponent<Props, State> {
this.searchTimeoutId = window.setTimeout(
async () => {
this.setGroupsLoadingState(true);
await this.props.actions.getGroupsNotAssociatedToTeam(this.props.currentTeamId, searchTerm);
await this.props.actions.getGroupsNotAssociatedToTeam(this.props.currentTeamId, searchTerm, 0, GROUPS_PER_PAGE + 1, '', true);
this.setGroupsLoadingState(false);
},
Constants.SEARCH_TIMEOUT_MILLISECONDS,
@@ -176,7 +176,7 @@ export class AddGroupsToTeamModal extends React.PureComponent<Props, State> {
public handlePageChange = (page: number, prevPage: number): void => {
if (page > prevPage) {
this.setGroupsLoadingState(true);
this.props.actions.getGroupsNotAssociatedToTeam(this.props.currentTeamId, this.props.searchTerm, page, GROUPS_PER_PAGE + 1).then(() => {
this.props.actions.getGroupsNotAssociatedToTeam(this.props.currentTeamId, this.props.searchTerm, page, GROUPS_PER_PAGE + 1, '', true).then(() => {
this.setGroupsLoadingState(false);
});
}

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

@@ -56,6 +56,9 @@ For more information on Groups, please see <link>documentation</link>."
>
<Connect(GroupsList) />
</AdminPanel>
<Pluggable
pluggableName="SystemConsoleGroupTable"
/>
</div>
</div>
</div>

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

@@ -1399,3 +1399,224 @@ exports[`components/admin_console/group_settings/group_details/GroupUsers should
/>
</div>
`;
exports[`components/admin_console/group_settings/group_details/GroupUsers should match snapshot, plugin group 1`] = `
<div
className="group-users"
>
<div
className="group-users--header"
>
<MemoizedFormattedMessage
defaultMessage="This group is managed by a plugin."
id="admin.group_settings.group_profile.group_users.pluginGroupText"
/>
</div>
<div
className="group-users--body"
>
<div
className="group-users-loading active"
>
<i
className="fa fa-spinner fa-pulse fa-2x"
/>
</div>
<Memo(GroupUsersRow)
displayName="Name0 Surname0"
email="test0@test.com"
key="id0"
lastPictureUpdate={0}
userId="id0"
username="username0"
/>
<Memo(GroupUsersRow)
displayName="Name1 Surname1"
email="test1@test.com"
key="id1"
lastPictureUpdate={1}
userId="id1"
username="username1"
/>
<Memo(GroupUsersRow)
displayName="Name2 Surname2"
email="test2@test.com"
key="id2"
lastPictureUpdate={2}
userId="id2"
username="username2"
/>
<Memo(GroupUsersRow)
displayName="Name3 Surname3"
email="test3@test.com"
key="id3"
lastPictureUpdate={3}
userId="id3"
username="username3"
/>
<Memo(GroupUsersRow)
displayName="Name4 Surname4"
email="test4@test.com"
key="id4"
lastPictureUpdate={4}
userId="id4"
username="username4"
/>
<Memo(GroupUsersRow)
displayName="Name5 Surname5"
email="test5@test.com"
key="id5"
lastPictureUpdate={5}
userId="id5"
username="username5"
/>
<Memo(GroupUsersRow)
displayName="Name6 Surname6"
email="test6@test.com"
key="id6"
lastPictureUpdate={6}
userId="id6"
username="username6"
/>
<Memo(GroupUsersRow)
displayName="Name7 Surname7"
email="test7@test.com"
key="id7"
lastPictureUpdate={7}
userId="id7"
username="username7"
/>
<Memo(GroupUsersRow)
displayName="Name8 Surname8"
email="test8@test.com"
key="id8"
lastPictureUpdate={8}
userId="id8"
username="username8"
/>
<Memo(GroupUsersRow)
displayName="Name9 Surname9"
email="test9@test.com"
key="id9"
lastPictureUpdate={9}
userId="id9"
username="username9"
/>
<Memo(GroupUsersRow)
displayName="Name10 Surname10"
email="test10@test.com"
key="id10"
lastPictureUpdate={10}
userId="id10"
username="username10"
/>
<Memo(GroupUsersRow)
displayName="Name11 Surname11"
email="test11@test.com"
key="id11"
lastPictureUpdate={11}
userId="id11"
username="username11"
/>
<Memo(GroupUsersRow)
displayName="Name12 Surname12"
email="test12@test.com"
key="id12"
lastPictureUpdate={12}
userId="id12"
username="username12"
/>
<Memo(GroupUsersRow)
displayName="Name13 Surname13"
email="test13@test.com"
key="id13"
lastPictureUpdate={13}
userId="id13"
username="username13"
/>
<Memo(GroupUsersRow)
displayName="Name14 Surname14"
email="test14@test.com"
key="id14"
lastPictureUpdate={14}
userId="id14"
username="username14"
/>
<Memo(GroupUsersRow)
displayName="Name15 Surname15"
email="test15@test.com"
key="id15"
lastPictureUpdate={15}
userId="id15"
username="username15"
/>
<Memo(GroupUsersRow)
displayName="Name16 Surname16"
email="test16@test.com"
key="id16"
lastPictureUpdate={16}
userId="id16"
username="username16"
/>
<Memo(GroupUsersRow)
displayName="Name17 Surname17"
email="test17@test.com"
key="id17"
lastPictureUpdate={17}
userId="id17"
username="username17"
/>
<Memo(GroupUsersRow)
displayName="Name18 Surname18"
email="test18@test.com"
key="id18"
lastPictureUpdate={18}
userId="id18"
username="username18"
/>
<Memo(GroupUsersRow)
displayName="Name19 Surname19"
email="test19@test.com"
key="id19"
lastPictureUpdate={19}
userId="id19"
username="username19"
/>
</div>
<div
className="group-users--footer"
>
<div
className="counter"
>
<MemoizedFormattedMessage
defaultMessage="{startCount, number} - {endCount, number} of {total, number}"
id="admin.group_settings.groups_list.paginatorCount"
values={
Object {
"endCount": 20,
"startCount": 1,
"total": 20,
}
}
/>
</div>
<button
className="btn btn-tertiary prev disabled"
disabled={true}
onClick={[Function]}
type="button"
>
<Memo(PreviousIcon) />
</button>
<button
className="btn btn-tertiary next disabled"
disabled={true}
onClick={[Function]}
type="button"
>
<Memo(NextIcon) />
</button>
</div>
</div>
`;

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

@@ -761,6 +761,7 @@ class GroupDetails extends React.PureComponent<Props, State> {
total={memberCount}
groupID={this.props.groupID}
getMembers={this.props.actions.getMembers}
source={group.source}
/>
</AdminPanel>
</div>

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

@@ -5,6 +5,7 @@ import {shallow} from 'enzyme';
import range from 'lodash/range';
import React from 'react';
import {GroupSource, PluginGroupSourcePrefix} from '@mattermost/types/groups';
import type {UserProfile} from '@mattermost/types/users';
import GroupUsers from 'components/admin_console/group_settings/group_details/group_users';
@@ -23,6 +24,7 @@ describe('components/admin_console/group_settings/group_details/GroupUsers', ()
groupID: 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
members: members.slice(0, 20),
total: 20,
source: GroupSource.Ldap,
getMembers: jest.fn().mockReturnValue(Promise.resolve()),
};
@@ -36,6 +38,16 @@ describe('components/admin_console/group_settings/group_details/GroupUsers', ()
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, plugin group', () => {
const wrapper = shallow(
<GroupUsers
{...defaultProps}
source={PluginGroupSourcePrefix.Plugin + 'keycloak'}
/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, on loading with data', () => {
const wrapper = shallow(<GroupUsers {...defaultProps}/>);
expect(wrapper).toMatchSnapshot();

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

@@ -5,6 +5,7 @@ import React from 'react';
import {FormattedMessage} from 'react-intl';
import {Link} from 'react-router-dom';
import {GroupSource, PluginGroupSourcePrefix} from '@mattermost/types/groups';
import type {UserProfile} from '@mattermost/types/users';
import type {ActionResult} from 'mattermost-redux/types/actions';
@@ -19,6 +20,7 @@ type Props = {
groupID: string;
members: UserProfile[];
total: number;
source?: string;
getMembers: (
id: string,
page?: number,
@@ -154,23 +156,42 @@ export default class GroupUsers extends React.PureComponent<Props, State> {
);
};
renderHeader = () => {
if (this.props.source === GroupSource.Ldap) {
return (
<FormattedMessage
id='admin.group_settings.group_profile.group_users.ldapConnectorText'
defaultMessage={
'AD/LDAP Connector is configured to sync and manage this group and its users. <a>Click here to view</a>'
}
values={{
a: (chunks: string) => (
<Link to='/admin_console/authentication/ldap'>
{chunks}
</Link>
),
}}
/>
);
}
if (this.props.source?.startsWith(PluginGroupSourcePrefix.Plugin)) {
return (
<FormattedMessage
id='admin.group_settings.group_profile.group_users.pluginGroupText'
defaultMessage='This group is managed by a plugin.'
/>
);
}
return null;
};
render = () => {
return (
<div className='group-users'>
<div className='group-users--header'>
<FormattedMessage
id='admin.group_settings.group_profile.group_users.ldapConnectorText'
defaultMessage={
'AD/LDAP Connector is configured to sync and manage this group and its users. <a>Click here to view</a>'
}
values={{
a: (chunks: string) => (
<Link to='/admin_console/authentication/ldap'>
{chunks}
</Link>
),
}}
/>
{this.renderHeader()}
</div>
<div className='group-users--body'>
<div

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

@@ -9,6 +9,7 @@ import ExternalLink from 'components/external_link';
import AdminHeader from 'components/widgets/admin_console/admin_header';
import AdminPanel from 'components/widgets/admin_console/admin_panel';
import Pluggable from 'plugins/pluggable';
import {DocLinks} from 'utils/constants';
import {getSiteURL} from 'utils/url';
@@ -66,6 +67,9 @@ const GroupSettings = ({isDisabled}: Props) => {
readOnly={isDisabled}
/>
</AdminPanel>
<Pluggable
pluggableName='SystemConsoleGroupTable'
/>
</div>
</div>
</div>

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

@@ -4,12 +4,12 @@
import React, {useCallback} from 'react';
import {useSelector} from 'react-redux';
import {GroupSource} from '@mattermost/types/groups';
import type {Group} from '@mattermost/types/groups';
import type {UserProfile} from '@mattermost/types/users';
import {getStatusForUserId} from 'mattermost-redux/selectors/entities/users';
import type {ActionResult} from 'mattermost-redux/types/actions';
import {isSyncableSource} from 'mattermost-redux/utils/group_utils';
import StatusIcon from 'components/status_icon';
import Avatar from 'components/widgets/users/avatar';
@@ -72,7 +72,7 @@ const ViewUserGroupListItem = (props: Props) => {
{`@${user.username}`}
</div>
{
(group.source.toLowerCase() !== GroupSource.Ldap && props.permissionToLeaveGroup) &&
(!isSyncableSource(group.source.toLowerCase()) && props.permissionToLeaveGroup) &&
<button
type='button'
className='remove-group-member btn btn-icon btn-xs'

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

@@ -7,7 +7,7 @@ import {Modal} from 'react-bootstrap';
import {defineMessage, FormattedMessage} from 'react-intl';
import type {Group} from '@mattermost/types/groups';
import {GroupSource} from '@mattermost/types/groups';
import {GroupSource, PluginGroupSourcePrefix} from '@mattermost/types/groups';
import type {UserProfile} from '@mattermost/types/users';
import {debounce} from 'mattermost-redux/actions/helpers';
@@ -183,6 +183,15 @@ export default class ViewUserGroupModal extends React.PureComponent<Props, State
/>
</span>
}
{
group.source.toLowerCase().startsWith(PluginGroupSourcePrefix.Plugin) &&
<span className='group-source'>
<FormattedMessage
id='view_user_group_modal.pluginSynced'
defaultMessage='Plugin SYNCED'
/>
</span>
}
</div>
);
}

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

@@ -1112,6 +1112,7 @@
"admin.group_settings.group_profile.group_teams_and_channels.name": "Name",
"admin.group_settings.group_profile.group_teams_and_channels.type": "Type",
"admin.group_settings.group_profile.group_users.ldapConnectorText": "AD/LDAP Connector is configured to sync and manage this group and its users. <a>Click here to view</a>",
"admin.group_settings.group_profile.group_users.pluginGroupText": "This group is managed by a plugin.",
"admin.group_settings.group_row.configure": "Configure",
"admin.group_settings.group_row.edit": "Edit",
"admin.group_settings.group_row.link_failed": "Link failed",
@@ -6006,6 +6007,7 @@
"view_image.zoom_reset": "Reset Zoom",
"view_user_group_modal.ldapSynced": "AD/LDAP SYNCED",
"view_user_group_modal.memberCount": "{member_count} {member_count, plural, one {Member} other {Members}}",
"view_user_group_modal.pluginSynced": "Plugin SYNCED",
"web.footer.about": "About",
"web.footer.help": "Help",
"web.footer.privacy": "Privacy Policy",

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

@@ -171,7 +171,7 @@ export function getGroups(opts: GetGroupsParams) {
});
}
export function getGroupsNotAssociatedToTeam(teamID: string, q = '', page = 0, perPage: number = General.PAGE_SIZE_DEFAULT, source = GroupSource.Ldap) {
export function getGroupsNotAssociatedToTeam(teamID: string, q = '', page = 0, perPage: number = General.PAGE_SIZE_DEFAULT, source: GroupSource | string = GroupSource.Ldap, onlySyncableSources = false) {
return bindClientFunc({
clientFunc: Client4.getGroupsNotAssociatedToTeam,
onSuccess: [GroupTypes.RECEIVED_GROUPS],
@@ -181,11 +181,12 @@ export function getGroupsNotAssociatedToTeam(teamID: string, q = '', page = 0, p
page,
perPage,
source,
onlySyncableSources,
],
});
}
export function getGroupsNotAssociatedToChannel(channelID: string, q = '', page = 0, perPage: number = General.PAGE_SIZE_DEFAULT, filterParentTeamPermitted = false, source = GroupSource.Ldap) {
export function getGroupsNotAssociatedToChannel(channelID: string, q = '', page = 0, perPage: number = General.PAGE_SIZE_DEFAULT, filterParentTeamPermitted = false, source: GroupSource | string = GroupSource.Ldap, onlySyncableSources = false) {
return bindClientFunc({
clientFunc: Client4.getGroupsNotAssociatedToChannel,
onSuccess: [GroupTypes.RECEIVED_GROUPS],
@@ -196,6 +197,7 @@ export function getGroupsNotAssociatedToChannel(channelID: string, q = '', page
perPage,
filterParentTeamPermitted,
source,
onlySyncableSources,
],
});
}

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

@@ -9,7 +9,7 @@ import {createSelector} from 'mattermost-redux/selectors/create_selector';
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import {getTeam} from 'mattermost-redux/selectors/entities/teams';
import type {UserMentionKey} from 'mattermost-redux/selectors/entities/users';
import {filterGroupsMatchingTerm, sortGroups} from 'mattermost-redux/utils/group_utils';
import {filterGroupsMatchingTerm, isSyncableSource, sortGroups} from 'mattermost-redux/utils/group_utils';
import {getCurrentUserLocale} from './i18n';
@@ -197,7 +197,7 @@ export const getGroupsNotAssociatedToTeam: (state: GlobalState, teamID: string)
getAllGroups,
(state: GlobalState, teamID: string) => getTeamGroupIDSet(state, teamID),
(allGroups, teamGroupIDSet) => {
return Object.entries(allGroups).filter(([groupID, group]) => !teamGroupIDSet.has(groupID) && group.source === GroupSource.Ldap).map((entry) => entry[1]);
return Object.entries(allGroups).filter(([groupID, group]) => !teamGroupIDSet.has(groupID) && isSyncableSource(group.source)).map((entry) => entry[1]);
},
);
@@ -217,7 +217,7 @@ export const getGroupsNotAssociatedToChannel: (state: GlobalState, channelID: st
(state: GlobalState, channelID: string, teamID: string) => getTeam(state, teamID),
(state: GlobalState, channelID: string, teamID: string) => getGroupsAssociatedToTeam(state, teamID),
(allGroups, channelGroupIDSet, team, teamGroups) => {
let result = Object.values(allGroups).filter((group) => !channelGroupIDSet.has(group.id) && group.source === GroupSource.Ldap);
let result = Object.values(allGroups).filter((group) => !channelGroupIDSet.has(group.id) && isSyncableSource(group.source));
if (team?.group_constrained) {
const gids = teamGroups.map((group) => group.id);
result = result.filter((group) => gids?.includes(group.id));

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

@@ -17,6 +17,7 @@ import {
haveISystemPermission as haveISystemPermissionInternal,
} from 'mattermost-redux/selectors/entities/roles_helpers';
import {getTeamMemberships, getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {isSyncableSource} from 'mattermost-redux/utils/group_utils';
// Re-define these types to ensure that these are typed correctly when mattermost-redux is published
export const getMySystemPermissions: (state: GlobalState) => Set<string> = getMySystemPermissionsInternal;
@@ -86,9 +87,9 @@ export const getGroupListPermissions: (state: GlobalState) => Record<string, Gro
const groupPermissionsMap: Record<string, GroupPermissions> = {};
groups.forEach((g) => {
groupPermissionsMap[g.id] = {
can_delete: permissions.has(Permissions.DELETE_CUSTOM_GROUP) && g.source.toLowerCase() !== 'ldap' && g.delete_at === 0,
can_manage_members: permissions.has(Permissions.MANAGE_CUSTOM_GROUP_MEMBERS) && g.source.toLowerCase() !== 'ldap' && g.delete_at === 0,
can_restore: permissions.has(Permissions.RESTORE_CUSTOM_GROUP) && g.source.toLowerCase() !== 'ldap' && g.delete_at !== 0,
can_delete: permissions.has(Permissions.DELETE_CUSTOM_GROUP) && !isSyncableSource(g.source) && g.delete_at === 0,
can_manage_members: permissions.has(Permissions.MANAGE_CUSTOM_GROUP_MEMBERS) && !isSyncableSource(g.source) && g.delete_at === 0,
can_restore: permissions.has(Permissions.RESTORE_CUSTOM_GROUP) && !isSyncableSource(g.source) && g.delete_at !== 0,
};
});
return groupPermissionsMap;
@@ -191,13 +192,13 @@ export const haveIGroupPermission: (state: GlobalState, groupID: string, permiss
(state: GlobalState, groupID: string, permission: string) => permission,
(systemPermissions, permissionGroups, group, permission) => {
if (permission === Permissions.RESTORE_CUSTOM_GROUP) {
if ((group.source !== 'ldap' && group.delete_at !== 0) && (systemPermissions.has(permission) || (permissionGroups[group.id] && permissionGroups[group.id].has(permission)))) {
if ((!isSyncableSource(group.source) && group.delete_at !== 0) && (systemPermissions.has(permission) || (permissionGroups[group.id] && permissionGroups[group.id].has(permission)))) {
return true;
}
return false;
}
if (group.source === 'ldap' || group.delete_at !== 0) {
if (isSyncableSource(group.source) || group.delete_at !== 0) {
return false;
}

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

@@ -2,11 +2,16 @@
// See LICENSE.txt for license information.
import type {Group} from '@mattermost/types/groups';
import {GroupSource, PluginGroupSourcePrefix} from '@mattermost/types/groups';
import {getSuggestionsSplitBy, getSuggestionsSplitByMultiple} from './user_utils';
import {General} from '../constants';
export function isSyncableSource(source: string): boolean {
return source.toLowerCase() === GroupSource.Ldap || source.toLowerCase().startsWith(PluginGroupSourcePrefix.Plugin);
}
export function filterGroupsMatchingTerm(groups: Group[], term: string): Group[] {
const lowercasedTerm = term.toLowerCase();
let trimmedTerm = lowercasedTerm;

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

@@ -1251,4 +1251,10 @@ export default class PluginRegistry {
data,
});
});
// Register a component to be displayed in the System Console Groups table.
// Accepts a React component. Returns a unique identifier.
registerSystemConsoleGroupTable = reArg(['component'], ({component}: DPluginComponentProp) => {
return dispatchPluginComponentAction('SystemConsoleGroupTable', this.id, component);
});
}

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

@@ -222,6 +222,7 @@ const initialComponents: PluginsState['components'] = {
MessageWillBePosted: [],
MessageWillBeUpdated: [],
SlashCommandWillBePosted: [],
SystemConsoleGroupTable: [],
};
function components(state: PluginsState['components'] = initialComponents, action: MMAction) {

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

@@ -72,6 +72,7 @@ export type PluginsState = {
MessageWillBePosted: MessageWillBePostedHook[];
SlashCommandWillBePosted: SlashCommandWillBePostedHook[];
MessageWillBeUpdated: MessageWillBeUpdatedHook[];
SystemConsoleGroupTable: SystemConsoleGroupTableComponent[];
};
postTypes: {
@@ -484,3 +485,7 @@ export type AdminConsolePluginCustomSection = {
key: string;
component: React.Component;
};
export type SystemConsoleGroupTableComponent = PluginComponent & {
component: React.ComponentType<BasePluggableProps>;
};

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

@@ -3713,14 +3713,14 @@ export default class Client4 {
);
};
getGroupsNotAssociatedToTeam = (teamID: string, q = '', page = 0, perPage = PER_PAGE_DEFAULT, source = 'ldap') => {
getGroupsNotAssociatedToTeam = (teamID: string, q = '', page = 0, perPage = PER_PAGE_DEFAULT, source = 'ldap', onlySyncableSources = false) => {
return this.doFetch<Group[]>(
`${this.getGroupsRoute()}${buildQueryString({not_associated_to_team: teamID, page, per_page: perPage, q, include_member_count: true, group_source: source})}`,
`${this.getGroupsRoute()}${buildQueryString({not_associated_to_team: teamID, page, per_page: perPage, q, include_member_count: true, group_source: source, only_syncable_sources: onlySyncableSources})}`,
{method: 'get'},
);
};
getGroupsNotAssociatedToChannel = (channelID: string, q = '', page = 0, perPage = PER_PAGE_DEFAULT, filterParentTeamPermitted = false, source = 'ldap') => {
getGroupsNotAssociatedToChannel = (channelID: string, q = '', page = 0, perPage = PER_PAGE_DEFAULT, filterParentTeamPermitted = false, source = 'ldap', onlySyncableSources = false) => {
const query = {
not_associated_to_channel: channelID,
page,
@@ -3729,6 +3729,7 @@ export default class Client4 {
include_member_count: true,
filter_parent_team_permitted: filterParentTeamPermitted,
group_source: source,
only_syncable_sources: onlySyncableSources,
};
return this.doFetch<Group[]>(
`${this.getGroupsRoute()}${buildQueryString(query)}`,

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

@@ -43,11 +43,22 @@ export type Group = {
member_ids?: string[];
};
/**
* Main sources for groups
*/
export enum GroupSource {
Ldap = 'ldap',
Custom = 'custom',
}
/**
* Special prefixes that can be added to group sources when they come from plugins
* Used for identifying plugin-created groups by checking if source starts with this prefix
*/
export enum PluginGroupSourcePrefix {
Plugin = 'plugin_'
}
export type GroupTeam = {
team_id: string;
team_display_name: string;