diff --git a/api4/apitestlib.go b/api4/apitestlib.go index 4dc006c37d..8500b8fc6e 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -894,7 +894,7 @@ func (th *TestHelper) CreateGroup() *model.Group { Name: model.NewString("n-" + id), DisplayName: "dn_" + id, Source: model.GroupSourceLdap, - RemoteId: "ri_" + id, + RemoteId: model.NewString("ri_" + model.NewId()), } group, err := th.App.CreateGroup(group) diff --git a/api4/channel_test.go b/api4/channel_test.go index 00fe7ed362..a052dc41f1 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -4320,7 +4320,7 @@ func TestGetChannelMemberCountsByGroup(t *testing.T) { DisplayName: "dn_" + id, Name: model.NewString("name" + id), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } _, appErr = th.App.CreateGroup(group) diff --git a/api4/group.go b/api4/group.go index d017036895..de33522745 100644 --- a/api4/group.go +++ b/api4/group.go @@ -11,70 +11,86 @@ import ( "strconv" "strings" + "github.com/mattermost/mattermost-server/v6/app" "github.com/mattermost/mattermost-server/v6/audit" "github.com/mattermost/mattermost-server/v6/model" ) func (api *API) InitGroup() { // GET /api/v4/groups - api.BaseRoutes.Groups.Handle("", api.APISessionRequired(getGroups)).Methods("GET") + api.BaseRoutes.Groups.Handle("", api.APISessionRequired(requireLicense(getGroups))).Methods("GET") + + // POST /api/v4/groups + api.BaseRoutes.Groups.Handle("", api.APISessionRequired(requireLicense(createGroup))).Methods("POST") // GET /api/v4/groups/:group_id api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}", - api.APISessionRequired(getGroup)).Methods("GET") + api.APISessionRequired(requireLicense(getGroup))).Methods("GET") // PUT /api/v4/groups/:group_id/patch api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/patch", - api.APISessionRequired(patchGroup)).Methods("PUT") + api.APISessionRequired(requireLicense(patchGroup))).Methods("PUT") // POST /api/v4/groups/:group_id/teams/:team_id/link // POST /api/v4/groups/:group_id/channels/:channel_id/link api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}/link", - api.APISessionRequired(linkGroupSyncable)).Methods("POST") + api.APISessionRequired(requireLicense(linkGroupSyncable))).Methods("POST") // DELETE /api/v4/groups/:group_id/teams/:team_id/link // DELETE /api/v4/groups/:group_id/channels/:channel_id/link api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}/link", - api.APISessionRequired(unlinkGroupSyncable)).Methods("DELETE") + api.APISessionRequired(requireLicense(unlinkGroupSyncable))).Methods("DELETE") // GET /api/v4/groups/:group_id/teams/:team_id // GET /api/v4/groups/:group_id/channels/:channel_id api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}", - api.APISessionRequired(getGroupSyncable)).Methods("GET") + api.APISessionRequired(requireLicense(getGroupSyncable))).Methods("GET") // GET /api/v4/groups/:group_id/teams // GET /api/v4/groups/:group_id/channels api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}", - api.APISessionRequired(getGroupSyncables)).Methods("GET") + api.APISessionRequired(requireLicense(getGroupSyncables))).Methods("GET") // PUT /api/v4/groups/:group_id/teams/:team_id/patch // PUT /api/v4/groups/:group_id/channels/:channel_id/patch api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}/patch", - api.APISessionRequired(patchGroupSyncable)).Methods("PUT") + api.APISessionRequired(requireLicense(patchGroupSyncable))).Methods("PUT") // GET /api/v4/groups/:group_id/stats api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/stats", - api.APISessionRequired(getGroupStats)).Methods("GET") + api.APISessionRequired(requireLicense(getGroupStats))).Methods("GET") - // GET /api/v4/groups/:group_id/members?page=0&per_page=100 + // GET /api/v4/groups/:group_id/members api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members", - api.APISessionRequired(getGroupMembers)).Methods("GET") + api.APISessionRequired(requireLicense(getGroupMembers))).Methods("GET") - // GET /api/v4/users/:user_id/groups?page=0&per_page=100 + // GET /api/v4/users/:user_id/groups api.BaseRoutes.Users.Handle("/{user_id:[A-Za-z0-9]+}/groups", - api.APISessionRequired(getGroupsByUserId)).Methods("GET") + api.APISessionRequired(requireLicense(getGroupsByUserId))).Methods("GET") - // GET /api/v4/channels/:channel_id/groups?page=0&per_page=100 + // GET /api/v4/channels/:channel_id/groups api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/groups", - api.APISessionRequired(getGroupsByChannel)).Methods("GET") + api.APISessionRequired(requireLicense(getGroupsByChannel))).Methods("GET") - // GET /api/v4/teams/:team_id/groups?page=0&per_page=100 + // GET /api/v4/teams/:team_id/groups api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups", - api.APISessionRequired(getGroupsByTeam)).Methods("GET") + api.APISessionRequired(requireLicense(getGroupsByTeam))).Methods("GET") - // GET /api/v4/teams/:team_id/groups_by_channels?page=0&per_page=100 + // GET /api/v4/teams/:team_id/groups_by_channels api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups_by_channels", - api.APISessionRequired(getGroupsAssociatedToChannelsByTeam)).Methods("GET") + api.APISessionRequired(requireLicense(getGroupsAssociatedToChannelsByTeam))).Methods("GET") + + // DELETE /api/v4/groups/:group_id + api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}", + api.APISessionRequired(requireLicense(deleteGroup))).Methods("DELETE") + + // POST /api/v4/groups/:group_id/members + api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members", + api.APISessionRequired(requireLicense(addGroupMembers))).Methods("POST") + + // DELETE /api/v4/groups/:group_id/members + api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members", + api.APISessionRequired(requireLicense(deleteGroupMembers))).Methods("DELETE") } func getGroup(c *Context, w http.ResponseWriter, r *http.Request) { @@ -83,22 +99,27 @@ func getGroup(c *Context, w http.ResponseWriter, r *http.Request) { return } - if c.App.Srv().License() == nil || !*c.App.Srv().License().Features.LDAPGroups { - c.Err = model.NewAppError("Api4.getGroup", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented) - return - } - - if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) { - c.SetPermissionError(model.PermissionSysconsoleReadUserManagementGroups) - return - } - - group, err := c.App.GetGroup(c.Params.GroupId) + group, err := c.App.GetGroup(c.Params.GroupId, &model.GetGroupOpts{ + IncludeMemberCount: c.Params.IncludeMemberCount, + }) if err != nil { c.Err = err return } + if group.Source == model.GroupSourceLdap { + if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionSysconsoleReadUserManagementGroups) { + c.SetPermissionError(model.PermissionSysconsoleReadUserManagementGroups) + return + } + } + + if lcErr := licensedAndConfiguredForGroupBySource(c.App, group.Source); lcErr != nil { + lcErr.Where = "Api4.getGroup" + c.Err = lcErr + return + } + b, marshalErr := json.Marshal(group) if marshalErr != nil { c.Err = model.NewAppError("Api4.getGroup", "api.marshal_error", nil, marshalErr.Error(), http.StatusInternalServerError) @@ -108,36 +129,102 @@ func getGroup(c *Context, w http.ResponseWriter, r *http.Request) { w.Write(b) } +func createGroup(c *Context, w http.ResponseWriter, r *http.Request) { + var group *model.GroupWithUserIds + if jsonErr := json.NewDecoder(r.Body).Decode(&group); jsonErr != nil { + c.SetInvalidParam("group") + return + } + + if group.Source != model.GroupSourceCustom { + c.Err = model.NewAppError("createGroup", "app.group.crud_permission", nil, "", http.StatusNotImplemented) + return + } + + if lcErr := licensedAndConfiguredForGroupBySource(c.App, group.Source); lcErr != nil { + lcErr.Where = "Api4.createGroup" + c.Err = lcErr + return + } + + if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateCustomGroup) { + c.SetPermissionError(model.PermissionCreateCustomGroup) + return + } + + if !group.AllowReference { + c.Err = model.NewAppError("createGroup", "api.custom_groups.must_be_referenceable", nil, "", http.StatusNotImplemented) + return + } + + if group.GetRemoteId() != "" { + c.Err = model.NewAppError("createGroup", "api.custom_groups.no_remote_id", nil, "", http.StatusNotImplemented) + return + } + + auditRec := c.MakeAuditRecord("createGroup", audit.Fail) + defer c.LogAuditRec(auditRec) + auditRec.AddMeta("group", group) + + newGroup, err := c.App.CreateGroupWithUserIds(group) + if err != nil { + c.Err = err + return + } + + auditRec.AddMeta("group", newGroup) + js, jsonErr := json.Marshal(newGroup) + if jsonErr != nil { + c.Err = model.NewAppError("createGroup", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + return + } + auditRec.Success() + w.WriteHeader(http.StatusCreated) + w.Write(js) +} + func patchGroup(c *Context, w http.ResponseWriter, r *http.Request) { c.RequireGroupId() if c.Err != nil { return } + group, err := c.App.GetGroup(c.Params.GroupId, nil) + if err != nil { + c.Err = err + return + } + + if lcErr := licensedAndConfiguredForGroupBySource(c.App, group.Source); lcErr != nil { + lcErr.Where = "Api4.patchGroup" + c.Err = lcErr + return + } + + var requiredPermission *model.Permission + if group.Source == model.GroupSourceCustom { + requiredPermission = model.PermissionEditCustomGroup + } else { + requiredPermission = model.PermissionSysconsoleWriteUserManagementGroups + } + if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, requiredPermission) { + c.SetPermissionError(requiredPermission) + return + } + var groupPatch model.GroupPatch if jsonErr := json.NewDecoder(r.Body).Decode(&groupPatch); jsonErr != nil { c.SetInvalidParam("group") return } + if group.Source == model.GroupSourceCustom && groupPatch.AllowReference != nil && !*groupPatch.AllowReference { + c.Err = model.NewAppError("Api4.patchGroup", "api.custom_groups.must_be_referenceable", nil, "", http.StatusBadRequest) + return + } + auditRec := c.MakeAuditRecord("patchGroup", audit.Fail) defer c.LogAuditRec(auditRec) - - if c.App.Srv().License() == nil || !*c.App.Srv().License().Features.LDAPGroups { - c.Err = model.NewAppError("Api4.patchGroup", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented) - return - } - - if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementGroups) { - c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementGroups) - return - } - - group, err := c.App.GetGroup(c.Params.GroupId) - if err != nil { - c.Err = err - return - } auditRec.AddMeta("group", group) if groupPatch.AllowReference != nil && *groupPatch.AllowReference { @@ -223,7 +310,7 @@ func linkGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) { return } - if c.App.Srv().License() == nil || !*c.App.Srv().License().Features.LDAPGroups { + if !*c.App.Srv().License().Features.LDAPGroups { c.Err = model.NewAppError("Api4.createGroupSyncable", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented) return } @@ -279,7 +366,7 @@ func getGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) { } syncableType := c.Params.SyncableType - if c.App.Srv().License() == nil || !*c.App.Srv().License().Features.LDAPGroups { + if !*c.App.Srv().License().Features.LDAPGroups { c.Err = model.NewAppError("Api4.getGroupSyncable", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented) return } @@ -316,7 +403,7 @@ func getGroupSyncables(c *Context, w http.ResponseWriter, r *http.Request) { } syncableType := c.Params.SyncableType - if c.App.Srv().License() == nil || !*c.App.Srv().License().Features.LDAPGroups { + if !*c.App.Srv().License().Features.LDAPGroups { c.Err = model.NewAppError("Api4.getGroupSyncables", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented) return } @@ -378,7 +465,7 @@ func patchGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) { return } - if c.App.Srv().License() == nil || !*c.App.Srv().License().Features.LDAPGroups { + if !*c.App.Srv().License().Features.LDAPGroups { c.Err = model.NewAppError("Api4.patchGroupSyncable", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented) return @@ -444,7 +531,7 @@ func unlinkGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) { auditRec.AddMeta("syncable_id", syncableID) auditRec.AddMeta("syncable_type", syncableType) - if c.App.Srv().License() == nil || !*c.App.Srv().License().Features.LDAPGroups { + if !*c.App.Srv().License().Features.LDAPGroups { c.Err = model.NewAppError("Api4.unlinkGroupSyncable", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented) return } @@ -503,12 +590,19 @@ func getGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) { return } - if c.App.Srv().License() == nil || !*c.App.Srv().License().Features.LDAPGroups { - c.Err = model.NewAppError("Api4.getGroupMembers", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented) + group, err := c.App.GetGroup(c.Params.GroupId, nil) + if err != nil { + c.Err = err return } - if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) { + if lcErr := licensedAndConfiguredForGroupBySource(c.App, group.Source); lcErr != nil { + lcErr.Where = "Api4.getGroupMembers" + c.Err = lcErr + return + } + + if group.Source == model.GroupSourceLdap && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) { c.SetPermissionError(model.PermissionSysconsoleReadUserManagementGroups) return } @@ -540,7 +634,7 @@ func getGroupStats(c *Context, w http.ResponseWriter, r *http.Request) { return } - if c.App.Srv().License() == nil || !*c.App.Srv().License().Features.LDAPGroups { + if !*c.App.Srv().License().Features.LDAPGroups { c.Err = model.NewAppError("Api4.getGroupStats", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented) return } @@ -580,7 +674,7 @@ func getGroupsByUserId(c *Context, w http.ResponseWriter, r *http.Request) { return } - if c.App.Srv().License() == nil || !*c.App.Srv().License().Features.LDAPGroups { + if !*c.App.Srv().License().Features.LDAPGroups { c.Err = model.NewAppError("Api4.getGroupsByUserId", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented) return } @@ -663,7 +757,6 @@ func getGroupsByTeam(c *Context, w http.ResponseWriter, r *http.Request) { if c.Err != nil { return } - if c.App.Srv().License() == nil || !*c.App.Srv().License().Features.LDAPGroups { c.Err = model.NewAppError("Api4.getGroupsByTeam", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented) return @@ -706,7 +799,7 @@ func getGroupsAssociatedToChannelsByTeam(c *Context, w http.ResponseWriter, r *h return } - if c.App.Srv().License() == nil || !*c.App.Srv().License().Features.LDAPGroups { + if !*c.App.Srv().License().Features.LDAPGroups { c.Err = model.NewAppError("Api4.getGroupsAssociatedToChannelsByTeam", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented) return } @@ -741,10 +834,6 @@ func getGroupsAssociatedToChannelsByTeam(c *Context, w http.ResponseWriter, r *h } func getGroups(c *Context, w http.ResponseWriter, r *http.Request) { - if c.App.Srv().License() == nil || !*c.App.Srv().License().Features.LDAPGroups { - c.Err = model.NewAppError("Api4.getGroups", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented) - return - } var teamID, channelID string if id := c.Params.NotAssociatedToTeam; model.IsValidId(id) { @@ -760,6 +849,19 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) { IncludeMemberCount: c.Params.IncludeMemberCount, FilterAllowReference: c.Params.FilterAllowReference, FilterParentTeamPermitted: c.Params.FilterParentTeamPermitted, + Source: c.Params.GroupSource, + FilterHasMember: c.Params.FilterHasMember, + } + + if !c.App.Config().FeatureFlags.CustomGroups && opts.Source == model.GroupSourceCustom { + c.Err = model.NewAppError("getGroups", "api.custom_groups.feature_disabled", nil, "", http.StatusNotImplemented) + return + } + + if lcErr := licensedAndConfiguredForGroupBySource(c.App, opts.Source); lcErr != nil { + lcErr.Where = "Api4.getGroups" + c.Err = lcErr + return } if teamID != "" { @@ -807,7 +909,23 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) { return } - b, marshalErr := json.Marshal(groups) + var b []byte + var marshalErr error + if c.Params.IncludeTotalCount { + totalCount, countErr := c.App.Srv().Store.Group().GroupCount() + if countErr != nil { + c.Err = model.NewAppError("Api4.getGroups", "api.custom_groups.count_err", nil, countErr.Error(), http.StatusInternalServerError) + return + } + gwc := &model.GroupsWithCount{ + Groups: groups, + TotalCount: totalCount, + } + b, marshalErr = json.Marshal(gwc) + } else { + b, marshalErr = json.Marshal(groups) + } + if marshalErr != nil { c.Err = model.NewAppError("Api4.getGroups", "api.marshal_error", nil, marshalErr.Error(), http.StatusInternalServerError) return @@ -815,3 +933,181 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) { w.Write(b) } + +func deleteGroup(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireGroupId() + if c.Err != nil { + return + } + + group, err := c.App.GetGroup(c.Params.GroupId, nil) + if err != nil { + c.Err = err + return + } + + if group.Source != model.GroupSourceCustom { + c.Err = model.NewAppError("Api4.deleteGroup", "app.group.crud_permission", nil, "", http.StatusNotImplemented) + return + } + + if lcErr := licensedAndConfiguredForGroupBySource(c.App, model.GroupSourceCustom); lcErr != nil { + lcErr.Where = "Api4.deleteGroup" + c.Err = lcErr + return + } + + if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionDeleteCustomGroup) { + c.SetPermissionError(model.PermissionDeleteCustomGroup) + return + } + + auditRec := c.MakeAuditRecord("deleteGroup", audit.Fail) + defer c.LogAuditRec(auditRec) + auditRec.AddMeta("group_id", c.Params.GroupId) + + _, err = c.App.DeleteGroup(c.Params.GroupId) + if err != nil { + c.Err = err + return + } + + auditRec.Success() + + ReturnStatusOK(w) +} + +func addGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireGroupId() + if c.Err != nil { + return + } + + group, err := c.App.GetGroup(c.Params.GroupId, nil) + if err != nil { + c.Err = err + return + } + + if group.Source != model.GroupSourceCustom { + c.Err = model.NewAppError("Api4.deleteGroup", "app.group.crud_permission", nil, "", http.StatusNotImplemented) + return + } + + if lcErr := licensedAndConfiguredForGroupBySource(c.App, model.GroupSourceCustom); lcErr != nil { + lcErr.Where = "Api4.deleteGroup" + c.Err = lcErr + return + } + + if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionManageCustomGroupMembers) { + c.SetPermissionError(model.PermissionManageCustomGroupMembers) + return + } + + var newMembers *model.GroupModifyMembers + if jsonErr := json.NewDecoder(r.Body).Decode(&newMembers); jsonErr != nil { + c.SetInvalidParam("addGroupMembers") + return + } + + auditRec := c.MakeAuditRecord("addGroupMembers", audit.Fail) + defer c.LogAuditRec(auditRec) + auditRec.AddMeta("addGroupMembers", newMembers) + + members, err := c.App.UpsertGroupMembers(c.Params.GroupId, newMembers.UserIds) + if err != nil { + c.Err = err + return + } + + b, marshalErr := json.Marshal(members) + if marshalErr != nil { + c.Err = model.NewAppError("Api4.addGroupMembers", "api.marshal_error", nil, marshalErr.Error(), http.StatusInternalServerError) + return + } + auditRec.Success() + w.Write(b) +} + +func deleteGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireGroupId() + if c.Err != nil { + return + } + + group, err := c.App.GetGroup(c.Params.GroupId, nil) + if err != nil { + c.Err = err + return + } + + if group.Source != model.GroupSourceCustom { + c.Err = model.NewAppError("Api4.deleteGroup", "app.group.crud_permission", nil, "", http.StatusNotImplemented) + return + } + + if lcErr := licensedAndConfiguredForGroupBySource(c.App, model.GroupSourceCustom); lcErr != nil { + lcErr.Where = "Api4.deleteGroup" + c.Err = lcErr + return + } + + if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionManageCustomGroupMembers) { + c.SetPermissionError(model.PermissionManageCustomGroupMembers) + return + } + + var deleteBody *model.GroupModifyMembers + if jsonErr := json.NewDecoder(r.Body).Decode(&deleteBody); jsonErr != nil { + c.SetInvalidParam("deleteGroupMembers") + return + } + + auditRec := c.MakeAuditRecord("deleteGroupMembers", audit.Fail) + defer c.LogAuditRec(auditRec) + auditRec.AddMeta("deleteGroupMembers", deleteBody) + + members, err := c.App.DeleteGroupMembers(c.Params.GroupId, deleteBody.UserIds) + if err != nil { + c.Err = err + return + } + + b, marshalErr := json.Marshal(members) + if marshalErr != nil { + c.Err = model.NewAppError("Api4.addGroupMembers", "api.marshal_error", nil, marshalErr.Error(), http.StatusInternalServerError) + return + } + auditRec.Success() + w.Write(b) +} + +// licensedAndConfiguredForGroupBySource returns an app error if not properly license or configured for the given group type. The returned app error +// will have a blank 'Where' field, which should be subsequently set by the caller, for example: +// +// err := licensedAndConfiguredForGroupBySource(c.App, group.Source) +// err.Where = "Api4.getGroup" +// +// Temporarily, this function also checks for the CustomGroups feature flag. +func licensedAndConfiguredForGroupBySource(app app.AppIface, source model.GroupSource) *model.AppError { + lic := app.Srv().License() + + if lic == nil { + return model.NewAppError("", "api.license_error", nil, "", http.StatusNotImplemented) + } + + if source == model.GroupSourceLdap && !*lic.Features.LDAPGroups { + return model.NewAppError("", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented) + } + + if source == model.GroupSourceCustom && lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise { + return model.NewAppError("", "api.custom_groups.license_error", nil, "", http.StatusNotImplemented) + } + + if source == model.GroupSourceCustom && (!app.Config().FeatureFlags.CustomGroups || !*app.Config().ServiceSettings.EnableCustomGroups) { + return model.NewAppError("", "api.custom_groups.feature_disabled", nil, "", http.StatusNotImplemented) + } + + return nil +} diff --git a/api4/group_test.go b/api4/group_test.go index 402cf156ed..92a2cf748c 100644 --- a/api4/group_test.go +++ b/api4/group_test.go @@ -26,7 +26,7 @@ func TestGetGroup(t *testing.T) { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) assert.Nil(t, appErr) @@ -65,7 +65,135 @@ func TestGetGroup(t *testing.T) { require.Error(t, err) CheckUnauthorizedStatus(t, response) } +func TestCreateGroup(t *testing.T) { + th := Setup(t) + defer th.TearDown() + id := model.NewId() + g := &model.Group{ + DisplayName: "dn_" + id, + Name: model.NewString("name" + id), + Source: model.GroupSourceCustom, + Description: "description_" + id, + AllowReference: true, + } + + th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional, "ldap")) + + group, _, err := th.SystemAdminClient.CreateGroup(g) + require.NoError(t, err) + + assert.Equal(t, g.DisplayName, group.DisplayName) + assert.Equal(t, g.Name, group.Name) + assert.Equal(t, g.Source, group.Source) + assert.Equal(t, g.Description, group.Description) + assert.Equal(t, g.RemoteId, group.RemoteId) + + gbroken := &model.Group{ + DisplayName: "dn_" + id, + Name: model.NewString("name" + id), + Source: "rrrr", + Description: "description_" + id, + } + + _, response, err := th.SystemAdminClient.CreateGroup(gbroken) + require.Error(t, err) + CheckNotImplementedStatus(t, response) + + validGroup := &model.Group{ + DisplayName: "dn_" + model.NewId(), + Name: model.NewString("name" + model.NewId()), + Source: model.GroupSourceCustom, + AllowReference: true, + } + + th.RemovePermissionFromRole(model.PermissionCreateCustomGroup.Id, model.SystemAdminRoleId) + th.RemovePermissionFromRole(model.PermissionCreateCustomGroup.Id, model.SystemUserRoleId) + defer th.AddPermissionToRole(model.PermissionCreateCustomGroup.Id, model.SystemUserRoleId) + _, response, err = th.SystemAdminClient.CreateGroup(validGroup) + require.Error(t, err) + CheckForbiddenStatus(t, response) + + th.AddPermissionToRole(model.PermissionCreateCustomGroup.Id, model.SystemAdminRoleId) + _, response, err = th.SystemAdminClient.CreateGroup(validGroup) + require.NoError(t, err) + CheckCreatedStatus(t, response) + + unReferenceableCustomGroup := &model.Group{ + DisplayName: "dn_" + model.NewId(), + Name: model.NewString("name" + model.NewId()), + Source: model.GroupSourceCustom, + AllowReference: false, + } + _, response, err = th.SystemAdminClient.CreateGroup(unReferenceableCustomGroup) + require.Error(t, err) + CheckNotImplementedStatus(t, response) + unReferenceableCustomGroup.AllowReference = true + _, response, err = th.SystemAdminClient.CreateGroup(unReferenceableCustomGroup) + require.NoError(t, err) + CheckCreatedStatus(t, response) + + customGroupWithRemoteID := &model.Group{ + DisplayName: "dn_" + model.NewId(), + Name: model.NewString("name" + model.NewId()), + Source: model.GroupSourceCustom, + AllowReference: true, + RemoteId: model.NewString(model.NewId()), + } + _, response, err = th.SystemAdminClient.CreateGroup(customGroupWithRemoteID) + require.Error(t, err) + CheckNotImplementedStatus(t, response) + + th.SystemAdminClient.Logout() + _, response, err = th.SystemAdminClient.CreateGroup(g) + require.Error(t, err) + CheckUnauthorizedStatus(t, response) +} + +func TestDeleteGroup(t *testing.T) { + th := Setup(t) + defer th.TearDown() + + id := model.NewId() + g, appErr := th.App.CreateGroup(&model.Group{ + DisplayName: "dn_" + id, + Name: model.NewString("name" + id), + Source: model.GroupSourceLdap, + Description: "description_" + id, + RemoteId: model.NewString(model.NewId()), + }) + assert.Nil(t, appErr) + + th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional)) + + _, response, err := th.Client.DeleteGroup(g.Id) + require.Error(t, err) + CheckNotImplementedStatus(t, response) + + th.AddPermissionToRole(model.PermissionDeleteCustomGroup.Id, model.SystemUserRoleId) + _, response, err = th.Client.DeleteGroup(g.Id) + require.Error(t, err) + CheckNotImplementedStatus(t, response) + + _, response, err = th.Client.DeleteGroup(g.Id) + require.Error(t, err) + CheckNotImplementedStatus(t, response) + + _, response, err = th.Client.DeleteGroup("wertyuijhbgvfcde") + require.Error(t, err) + CheckBadRequestStatus(t, response) + + validGroup, appErr := th.App.CreateGroup(&model.Group{ + DisplayName: "dn_" + model.NewId(), + Name: model.NewString("name" + model.NewId()), + Source: model.GroupSourceCustom, + }) + assert.Nil(t, appErr) + + _, response, err = th.Client.DeleteGroup(validGroup.Id) + require.NoError(t, err) + CheckOKStatus(t, response) +} func TestPatchGroup(t *testing.T) { th := Setup(t) defer th.TearDown() @@ -76,7 +204,15 @@ func TestPatchGroup(t *testing.T) { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), + }) + assert.Nil(t, appErr) + + g2, appErr := th.App.CreateGroup(&model.Group{ + DisplayName: "dn_" + model.NewId(), + Name: model.NewString("name" + model.NewId()), + Source: model.GroupSourceCustom, + AllowReference: true, }) assert.Nil(t, appErr) @@ -100,7 +236,7 @@ func TestPatchGroup(t *testing.T) { require.Error(t, err) CheckNotImplementedStatus(t, response) - th.App.Srv().SetLicense(model.NewTestLicense("ldap")) + th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional, "ldap")) group2, response, err := th.SystemAdminClient.PatchGroup(g.Id, gp) require.NoError(t, err) @@ -131,6 +267,23 @@ func TestPatchGroup(t *testing.T) { require.Error(t, err) CheckNotFoundStatus(t, response) + _, response, err = th.SystemAdminClient.PatchGroup(g2.Id, &model.GroupPatch{ + Name: model.NewString(model.NewId()), + DisplayName: model.NewString("foo"), + AllowReference: model.NewBool(false), + }) + require.Error(t, err) + CheckBadRequestStatus(t, response) + + // ensure that omitting the AllowReference field from the patch doesn't patch it to false + patchedG2, response, err := th.SystemAdminClient.PatchGroup(g2.Id, &model.GroupPatch{ + Name: model.NewString(model.NewId()), + DisplayName: model.NewString("foo"), + }) + require.NoError(t, err) + CheckOKStatus(t, response) + require.Equal(t, true, patchedG2.AllowReference) + th.SystemAdminClient.Logout() _, response, err = th.SystemAdminClient.PatchGroup(group.Id, gp) require.Error(t, err) @@ -147,7 +300,7 @@ func TestLinkGroupTeam(t *testing.T) { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) assert.Nil(t, appErr) @@ -187,7 +340,7 @@ func TestLinkGroupChannel(t *testing.T) { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) assert.Nil(t, appErr) @@ -229,7 +382,7 @@ func TestUnlinkGroupTeam(t *testing.T) { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) assert.Nil(t, appErr) @@ -280,7 +433,7 @@ func TestUnlinkGroupChannel(t *testing.T) { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) assert.Nil(t, appErr) @@ -332,7 +485,7 @@ func TestGetGroupTeam(t *testing.T) { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) assert.Nil(t, appErr) @@ -394,7 +547,7 @@ func TestGetGroupChannel(t *testing.T) { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) assert.Nil(t, appErr) @@ -456,7 +609,7 @@ func TestGetGroupTeams(t *testing.T) { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) assert.Nil(t, appErr) @@ -509,7 +662,7 @@ func TestGetGroupChannels(t *testing.T) { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) assert.Nil(t, appErr) @@ -561,7 +714,7 @@ func TestPatchGroupTeam(t *testing.T) { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) assert.Nil(t, appErr) @@ -633,7 +786,7 @@ func TestPatchGroupChannel(t *testing.T) { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) assert.Nil(t, appErr) @@ -716,7 +869,7 @@ func TestGetGroupsByChannel(t *testing.T) { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) assert.Nil(t, appErr) @@ -735,6 +888,8 @@ func TestGetGroupsByChannel(t *testing.T) { }, } + th.App.Srv().SetLicense(model.NewTestLicense("ldap")) + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { _, _, response, err := client.GetGroupsByChannel("asdfasdf", opts) require.Error(t, err) @@ -795,7 +950,7 @@ func TestGetGroupsAssociatedToChannelsByTeam(t *testing.T) { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) assert.Nil(t, appErr) @@ -814,6 +969,8 @@ func TestGetGroupsAssociatedToChannelsByTeam(t *testing.T) { }, } + th.App.Srv().SetLicense(model.NewTestLicense("ldap")) + _, response, err := th.SystemAdminClient.GetGroupsAssociatedToChannelsByTeam("asdfasdf", opts) require.Error(t, err) CheckBadRequestStatus(t, response) @@ -871,7 +1028,7 @@ func TestGetGroupsByTeam(t *testing.T) { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) assert.Nil(t, err) @@ -890,13 +1047,15 @@ func TestGetGroupsByTeam(t *testing.T) { }, } + th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional)) + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { _, _, response, err := client.GetGroupsByTeam("asdfasdf", opts) require.Error(t, err) CheckBadRequestStatus(t, response) }) - th.App.Srv().SetLicense(nil) + th.App.Srv().RemoveLicense() th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { _, _, response, err := client.GetGroupsByTeam(th.BasicTeam.Id, opts) @@ -945,27 +1104,32 @@ func TestGetGroups(t *testing.T) { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) assert.Nil(t, appErr) start := group.UpdateAt - 1 + id2 := model.NewId() + group2, appErr := th.App.CreateGroup(&model.Group{ + DisplayName: "dn-foo_" + id2, + Name: model.NewString("name" + id2), + Source: model.GroupSourceCustom, + Description: "description_" + id2, + RemoteId: model.NewString(model.NewId()), + }) + assert.Nil(t, appErr) + opts := model.GroupSearchOpts{ + Source: model.GroupSourceLdap, PageOpts: &model.PageOpts{ Page: 0, PerPage: 60, }, } - th.App.Srv().SetLicense(nil) + th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional)) - _, response, err := th.SystemAdminClient.GetGroups(opts) - require.Error(t, err) - CheckNotImplementedStatus(t, response) - - th.App.Srv().SetLicense(model.NewTestLicense("ldap")) - - _, _, err = th.SystemAdminClient.GetGroups(opts) + _, _, err := th.SystemAdminClient.GetGroups(opts) require.NoError(t, err) _, err = th.SystemAdminClient.UpdateChannelRoles(th.BasicChannel.Id, th.BasicUser.Id, "") @@ -1031,6 +1195,12 @@ func TestGetGroups(t *testing.T) { assert.Len(t, groups, 1) // make sure it returned th.Group,not group assert.Equal(t, groups[0].Id, th.Group.Id) + + opts.Source = model.GroupSourceCustom + groups, _, err = th.Client.GetGroups(opts) + assert.NoError(t, err) + assert.Len(t, groups, 1) + assert.Equal(t, groups[0].Id, group2.Id) } func TestGetGroupsByUserId(t *testing.T) { @@ -1043,7 +1213,7 @@ func TestGetGroupsByUserId(t *testing.T) { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) assert.Nil(t, appErr) @@ -1059,7 +1229,7 @@ func TestGetGroupsByUserId(t *testing.T) { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) assert.Nil(t, appErr) @@ -1109,7 +1279,7 @@ func TestGetGroupStats(t *testing.T) { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) assert.Nil(t, appErr) @@ -1129,7 +1299,8 @@ func TestGetGroupStats(t *testing.T) { }) t.Run("Returns stats for a group with no members", func(t *testing.T) { - stats, _, _ := th.SystemAdminClient.GetGroupStats(group.Id) + stats, _, err := th.SystemAdminClient.GetGroupStats(group.Id) + require.NoError(t, err) assert.Equal(t, stats.GroupID, group.Id) assert.Equal(t, stats.TotalMemberCount, int64(0)) }) @@ -1160,7 +1331,7 @@ func TestGetGroupsGroupConstrainedParentTeam(t *testing.T) { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) require.Nil(t, err) groups = append(groups, group) @@ -1228,3 +1399,149 @@ func TestGetGroupsGroupConstrainedParentTeam(t *testing.T) { require.NotContains(t, apiGroups, groups[0]) require.Contains(t, apiGroups, groups[2]) } + +func TestAddMembersToGroup(t *testing.T) { + th := Setup(t) + defer th.TearDown() + + // 1. Test with custom source + id := model.NewId() + group, err := th.App.CreateGroup(&model.Group{ + DisplayName: "dn_" + id, + Name: model.NewString("name" + id), + Source: model.GroupSourceCustom, + Description: "description_" + id, + }) + assert.Nil(t, err) + + user1, appErr := th.App.CreateUser(th.Context, &model.User{Email: th.GenerateTestEmail(), Nickname: "test user1", Password: "test-password-1", Username: "test-user-1", Roles: model.SystemUserRoleId}) + assert.Nil(t, appErr) + + user2, appErr := th.App.CreateUser(th.Context, &model.User{Email: th.GenerateTestEmail(), Nickname: "test user2", Password: "test-password-2", Username: "test-user-2", Roles: model.SystemUserRoleId}) + assert.Nil(t, appErr) + + members := &model.GroupModifyMembers{ + UserIds: []string{user1.Id, user2.Id}, + } + + th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional)) + + groupMembers, response, upsertErr := th.SystemAdminClient.UpsertGroupMembers(group.Id, members) + require.NoError(t, upsertErr) + CheckOKStatus(t, response) + + assert.Len(t, groupMembers, 2) + + count, countErr := th.App.GetGroupMemberCount(group.Id) + assert.Nil(t, countErr) + + assert.Equal(t, count, int64(2)) + + // 2. Test invalid group ID + _, response, upsertErr = th.Client.UpsertGroupMembers("abc123", members) + require.Error(t, upsertErr) + CheckBadRequestStatus(t, response) + + // 3. Test invalid user ID + invalidMembers := &model.GroupModifyMembers{ + UserIds: []string{"abc123"}, + } + + _, response, upsertErr = th.SystemAdminClient.UpsertGroupMembers(group.Id, invalidMembers) + require.Error(t, upsertErr) + CheckInternalErrorStatus(t, response) + + // 4. Test with ldap source + ldapId := model.NewId() + ldapGroup, err := th.App.CreateGroup(&model.Group{ + DisplayName: "dn_" + ldapId, + Name: model.NewString("name" + ldapId), + Source: model.GroupSourceLdap, + Description: "description_" + ldapId, + RemoteId: model.NewString(model.NewId()), + }) + assert.Nil(t, err) + + _, response, upsertErr = th.SystemAdminClient.UpsertGroupMembers(ldapGroup.Id, members) + + require.Error(t, upsertErr) + CheckNotImplementedStatus(t, response) +} + +func TestDeleteMembersFromGroup(t *testing.T) { + th := Setup(t) + defer th.TearDown() + + // 1. Test with custom source + user1, appErr := th.App.CreateUser(th.Context, &model.User{Email: th.GenerateTestEmail(), Nickname: "test user1", Password: "test-password-1", Username: "test-user-1", Roles: model.SystemUserRoleId}) + assert.Nil(t, appErr) + + user2, appErr := th.App.CreateUser(th.Context, &model.User{Email: th.GenerateTestEmail(), Nickname: "test user2", Password: "test-password-2", Username: "test-user-2", Roles: model.SystemUserRoleId}) + assert.Nil(t, appErr) + + id := model.NewId() + g := &model.Group{ + DisplayName: "dn_" + id, + Name: model.NewString("name" + id), + Source: model.GroupSourceCustom, + Description: "description_" + id, + } + group, err := th.App.CreateGroupWithUserIds(&model.GroupWithUserIds{ + Group: *g, + UserIds: []string{user1.Id, user2.Id}, + }) + assert.Nil(t, err) + + members := &model.GroupModifyMembers{ + UserIds: []string{user1.Id}, + } + + th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional)) + + groupMembers, response, deleteErr := th.SystemAdminClient.DeleteGroupMembers(group.Id, members) + require.NoError(t, deleteErr) + CheckOKStatus(t, response) + + assert.Len(t, groupMembers, 1) + assert.Equal(t, groupMembers[0].UserId, user1.Id) + + users, usersErr := th.App.GetGroupMemberUsers(group.Id) + assert.Nil(t, usersErr) + + assert.Len(t, users, 1) + assert.Equal(t, users[0].Id, user2.Id) + + // 2. Test invalid group ID + _, response, deleteErr = th.Client.DeleteGroupMembers("abc123", members) + require.Error(t, deleteErr) + CheckBadRequestStatus(t, response) + + // 3. Test invalid user ID + invalidMembers := &model.GroupModifyMembers{ + UserIds: []string{"abc123"}, + } + + _, response, deleteErr = th.SystemAdminClient.DeleteGroupMembers(group.Id, invalidMembers) + require.Error(t, deleteErr) + CheckInternalErrorStatus(t, response) + + // 4. Test with ldap source + ldapId := model.NewId() + g1 := &model.Group{ + DisplayName: "dn_" + ldapId, + Name: model.NewString("name" + ldapId), + Source: model.GroupSourceLdap, + Description: "description_" + ldapId, + RemoteId: model.NewString(model.NewId()), + } + ldapGroup, err := th.App.CreateGroupWithUserIds(&model.GroupWithUserIds{ + Group: *g1, + UserIds: []string{user1.Id, user2.Id}, + }) + assert.Nil(t, err) + + _, response, deleteErr = th.SystemAdminClient.DeleteGroupMembers(ldapGroup.Id, members) + + require.Error(t, deleteErr) + CheckNotImplementedStatus(t, response) +} diff --git a/api4/handlers.go b/api4/handlers.go index d35a2e3434..b0fe91ea3b 100644 --- a/api4/handlers.go +++ b/api4/handlers.go @@ -8,14 +8,17 @@ import ( "github.com/mattermost/gziphandler" + "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/web" ) type Context = web.Context +type handlerFunc func(*Context, http.ResponseWriter, *http.Request) + // APIHandler provides a handler for API endpoints which do not require the user to be logged in order for access to be // granted. -func (api *API) APIHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { +func (api *API) APIHandler(h handlerFunc) http.Handler { handler := &web.Handler{ Srv: api.srv, HandleFunc: h, @@ -34,7 +37,7 @@ func (api *API) APIHandler(h func(*Context, http.ResponseWriter, *http.Request)) // APISessionRequired provides a handler for API endpoints which require the user to be logged in in order for access to // be granted. -func (api *API) APISessionRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { +func (api *API) APISessionRequired(h handlerFunc) http.Handler { handler := &web.Handler{ Srv: api.srv, HandleFunc: h, @@ -53,7 +56,7 @@ func (api *API) APISessionRequired(h func(*Context, http.ResponseWriter, *http.R } // CloudAPIKeyRequired provides a handler for webhook endpoints to access Cloud installations from CWS -func (api *API) CloudAPIKeyRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { +func (api *API) CloudAPIKeyRequired(h handlerFunc) http.Handler { handler := &web.Handler{ Srv: api.srv, HandleFunc: h, @@ -73,7 +76,7 @@ func (api *API) CloudAPIKeyRequired(h func(*Context, http.ResponseWriter, *http. } // RemoteClusterTokenRequired provides a handler for remote cluster requests to /remotecluster endpoints. -func (api *API) RemoteClusterTokenRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { +func (api *API) RemoteClusterTokenRequired(h handlerFunc) http.Handler { handler := &web.Handler{ Srv: api.srv, HandleFunc: h, @@ -95,7 +98,7 @@ func (api *API) RemoteClusterTokenRequired(h func(*Context, http.ResponseWriter, // APISessionRequiredMfa provides a handler for API endpoints which require a logged-in user session but when accessed, // if MFA is enabled, the MFA process is not yet complete, and therefore the requirement to have completed the MFA // authentication must be waived. -func (api *API) APISessionRequiredMfa(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { +func (api *API) APISessionRequiredMfa(h handlerFunc) http.Handler { handler := &web.Handler{ Srv: api.srv, HandleFunc: h, @@ -116,7 +119,7 @@ func (api *API) APISessionRequiredMfa(h func(*Context, http.ResponseWriter, *htt // APIHandlerTrustRequester provides a handler for API endpoints which do not require the user to be logged in and are // allowed to be requested directly rather than via javascript/XMLHttpRequest, such as site branding images or the // websocket. -func (api *API) APIHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { +func (api *API) APIHandlerTrustRequester(h handlerFunc) http.Handler { handler := &web.Handler{ Srv: api.srv, HandleFunc: h, @@ -136,7 +139,7 @@ func (api *API) APIHandlerTrustRequester(h func(*Context, http.ResponseWriter, * // APISessionRequiredTrustRequester provides a handler for API endpoints which do require the user to be logged in and // are allowed to be requested directly rather than via javascript/XMLHttpRequest, such as emoji or file uploads. -func (api *API) APISessionRequiredTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { +func (api *API) APISessionRequiredTrustRequester(h handlerFunc) http.Handler { handler := &web.Handler{ Srv: api.srv, HandleFunc: h, @@ -156,7 +159,7 @@ func (api *API) APISessionRequiredTrustRequester(h func(*Context, http.ResponseW // DisableWhenBusy provides a handler for API endpoints which should be disabled when the server is under load, // responding with HTTP 503 (Service Unavailable). -func (api *API) APISessionRequiredDisableWhenBusy(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { +func (api *API) APISessionRequiredDisableWhenBusy(h handlerFunc) http.Handler { handler := &web.Handler{ Srv: api.srv, HandleFunc: h, @@ -179,7 +182,7 @@ func (api *API) APISessionRequiredDisableWhenBusy(h func(*Context, http.Response // mode, this is, through a UNIX socket and without an authenticated // session, but with one that has no user set and no permission // restrictions -func (api *API) APILocal(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { +func (api *API) APILocal(h handlerFunc) http.Handler { handler := &web.Handler{ Srv: api.srv, HandleFunc: h, @@ -196,3 +199,13 @@ func (api *API) APILocal(h func(*Context, http.ResponseWriter, *http.Request)) h } return handler } + +func requireLicense(f handlerFunc) handlerFunc { + return func(c *Context, w http.ResponseWriter, r *http.Request) { + if c.App.Srv().License() == nil { + c.Err = model.NewAppError("", "api.license_error", nil, "", http.StatusNotImplemented) + return + } + f(c, w, r) + } +} diff --git a/api4/ldap.go b/api4/ldap.go index d5af45e8ae..9314eda0e0 100644 --- a/api4/ldap.go +++ b/api4/ldap.go @@ -117,7 +117,7 @@ func getLdapGroups(c *Context, w http.ResponseWriter, r *http.Request) { for _, group := range groups { mug := &mixedUnlinkedGroup{ DisplayName: group.DisplayName, - RemoteId: group.RemoteId, + RemoteId: group.GetRemoteId(), } if len(group.Id) == 26 { mug.Id = &group.Id @@ -170,7 +170,7 @@ func linkLdapGroup(c *Context, w http.ResponseWriter, r *http.Request) { return } - group, err := c.App.GetGroupByRemoteID(ldapGroup.RemoteId, model.GroupSourceLdap) + group, err := c.App.GetGroupByRemoteID(ldapGroup.GetRemoteId(), model.GroupSourceLdap) if err != nil && err.Id != "app.group.no_rows" { c.Err = err return diff --git a/api4/team_test.go b/api4/team_test.go index 286bc0be95..8a3e0341ec 100644 --- a/api4/team_test.go +++ b/api4/team_test.go @@ -2915,7 +2915,7 @@ func TestImportTeam(t *testing.T) { fileData, err := base64.StdEncoding.DecodeString(fileResp["results"]) require.NoError(t, err, "failed to decode base64 results data") - fileReturned := fmt.Sprintf("%s", fileData) + fileReturned := string(fileData) require.Truef(t, strings.Contains(fileReturned, "darth.vader@stardeath.com"), "failed to report the user was imported, fileReturned: %s", fileReturned) // Checking the imported users diff --git a/api4/user.go b/api4/user.go index ab0084aed4..9db905bc20 100644 --- a/api4/user.go +++ b/api4/user.go @@ -19,6 +19,7 @@ import ( "github.com/mattermost/mattermost-server/v6/shared/mlog" "github.com/mattermost/mattermost-server/v6/store" "github.com/mattermost/mattermost-server/v6/utils" + "github.com/mattermost/mattermost-server/v6/web" ) func (api *API) InitUser() { @@ -641,6 +642,7 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { notInTeamId := r.URL.Query().Get("not_in_team") inChannelId := r.URL.Query().Get("in_channel") inGroupId := r.URL.Query().Get("in_group") + notInGroupId := r.URL.Query().Get("not_in_group") notInChannelId := r.URL.Query().Get("not_in_channel") groupConstrained := r.URL.Query().Get("group_constrained") withoutTeam := r.URL.Query().Get("without_team") @@ -664,7 +666,7 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { // Currently only supports sorting on a team // or sort="status" on inChannelId - if (sort == "last_activity_at" || sort == "create_at") && (inTeamId == "" || notInTeamId != "" || inChannelId != "" || notInChannelId != "" || withoutTeam != "" || inGroupId != "") { + if (sort == "last_activity_at" || sort == "create_at") && (inTeamId == "" || notInTeamId != "" || inChannelId != "" || notInChannelId != "" || withoutTeam != "" || inGroupId != "" || notInGroupId != "") { c.SetInvalidURLParam("sort") return } @@ -720,6 +722,7 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { NotInTeamId: notInTeamId, NotInChannelId: notInChannelId, InGroupId: inGroupId, + NotInGroupId: notInGroupId, GroupConstrained: groupConstrainedBool, WithoutTeam: withoutTeamBool, Inactive: inactiveBool, @@ -807,13 +810,9 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { profiles, err = c.App.GetUsersInChannelPage(userGetOptions, c.IsSystemAdmin()) } } else if inGroupId != "" { - if c.App.Srv().License() == nil || !*c.App.Srv().License().Features.LDAPGroups { - c.Err = model.NewAppError("Api4.getUsersInGroup", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented) - return - } - - if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) { - c.SetPermissionError(model.PermissionSysconsoleReadUserManagementGroups) + if gErr := requireGroupAccess(c, inGroupId); gErr != nil { + gErr.Where = "Api.getUsers" + c.Err = gErr return } @@ -822,6 +821,18 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { c.Err = err return } + } else if notInGroupId != "" { + if gErr := requireGroupAccess(c, notInGroupId); gErr != nil { + gErr.Where = "Api.getUsers" + c.Err = gErr + return + } + + profiles, err = c.App.GetUsersNotInGroupPage(notInGroupId, c.Params.Page, c.Params.PerPage) + if err != nil { + c.Err = err + return + } } else { userGetOptions, err = c.App.RestrictUsersGetByPermissions(c.AppContext.Session().UserId, userGetOptions) if err != nil { @@ -850,6 +861,25 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { w.Write(js) } +func requireGroupAccess(c *web.Context, groupID string) *model.AppError { + group, err := c.App.GetGroup(groupID, nil) + if err != nil { + return err + } + + if lcErr := licensedAndConfiguredForGroupBySource(c.App, group.Source); lcErr != nil { + return lcErr + } + + if group.Source == model.GroupSourceLdap { + if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) { + return c.App.MakePermissionError(c.AppContext.Session(), []*model.Permission{model.PermissionSysconsoleReadUserManagementGroups}) + } + } + + return nil +} + func getUsersByIds(c *Context, w http.ResponseWriter, r *http.Request) { userIds := model.ArrayFromJSON(r.Body) @@ -958,13 +988,17 @@ func searchUsers(c *Context, w http.ResponseWriter, r *http.Request) { } if props.InGroupId != "" { - if c.App.Srv().License() == nil || !*c.App.Srv().License().Features.LDAPGroups { - c.Err = model.NewAppError("Api4.searchUsers", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented) + if gErr := requireGroupAccess(c, props.InGroupId); gErr != nil { + gErr.Where = "Api.searchUsers" + c.Err = gErr return } + } - if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) { - c.SetPermissionError(model.PermissionManageSystem) + if props.NotInGroupId != "" { + if gErr := requireGroupAccess(c, props.NotInGroupId); gErr != nil { + gErr.Where = "Api.searchUsers" + c.Err = gErr return } } @@ -1904,7 +1938,7 @@ func loginCWS(c *Context, w http.ResponseWriter, r *http.Request) { if err != nil { c.LogAuditWithUserId("", "failure - login_id="+loginID) c.LogErrorByCode(err) - http.Redirect(w, r, *c.App.Config().ServiceSettings.SiteURL, 302) + http.Redirect(w, r, *c.App.Config().ServiceSettings.SiteURL, http.StatusNotFound) return } auditRec.AddMeta("user", user) @@ -1912,12 +1946,12 @@ func loginCWS(c *Context, w http.ResponseWriter, r *http.Request) { err = c.App.DoLogin(c.AppContext, w, r, user, "", false, false, false) if err != nil { c.LogErrorByCode(err) - http.Redirect(w, r, *c.App.Config().ServiceSettings.SiteURL, 302) + http.Redirect(w, r, *c.App.Config().ServiceSettings.SiteURL, http.StatusNotFound) return } c.LogAuditWithUserId(user.Id, "success") c.App.AttachSessionCookies(c.AppContext, w, r) - http.Redirect(w, r, *c.App.Config().ServiceSettings.SiteURL, 302) + http.Redirect(w, r, *c.App.Config().ServiceSettings.SiteURL, http.StatusNotFound) } func logout(c *Context, w http.ResponseWriter, r *http.Request) { diff --git a/api4/user_test.go b/api4/user_test.go index 15329af2a3..79489a2194 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -1179,7 +1179,7 @@ func TestSearchUsers(t *testing.T) { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) assert.Nil(t, appErr) @@ -2751,7 +2751,7 @@ func TestGetUsersInGroup(t *testing.T) { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) assert.Nil(t, appErr) diff --git a/api4/webhook_test.go b/api4/webhook_test.go index 2de602625e..8a89ecddbb 100644 --- a/api4/webhook_test.go +++ b/api4/webhook_test.go @@ -719,7 +719,7 @@ func TestGetOutgoingWebhook(t *testing.T) { CheckForbiddenStatus(t, resp) th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { - nonExistentHook := &model.OutgoingWebhook{ChannelId: th.BasicChannel.Id} + nonExistentHook := &model.OutgoingWebhook{} _, resp, err = client.GetOutgoingWebhook(nonExistentHook.Id) require.Error(t, err) CheckNotFoundStatus(t, resp) diff --git a/api4/websocket_norace_test.go b/api4/websocket_norace_test.go index 5bcbad50fe..88d2a508c7 100644 --- a/api4/websocket_norace_test.go +++ b/api4/websocket_norace_test.go @@ -1,8 +1,9 @@ -// +build !race - // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +//go:build !race +// +build !race + package api4 import ( diff --git a/app/app_iface.go b/app/app_iface.go index 4ea550518b..afbeddd89d 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -450,6 +450,7 @@ type AppIface interface { CreateEmoji(sessionUserId string, emoji *model.Emoji, multiPartImageData *multipart.Form) (*model.Emoji, *model.AppError) CreateGroup(group *model.Group) (*model.Group, *model.AppError) CreateGroupChannel(userIDs []string, creatorId string) (*model.Channel, *model.AppError) + CreateGroupWithUserIds(group *model.GroupWithUserIds) (*model.Group, *model.AppError) CreateIncomingWebhookForChannel(creatorId string, channel *model.Channel, hook *model.IncomingWebhook) (*model.IncomingWebhook, *model.AppError) CreateJob(job *model.Job) (*model.Job, *model.AppError) CreateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) @@ -491,6 +492,7 @@ type AppIface interface { DeleteExport(name string) *model.AppError DeleteGroup(groupID string) (*model.Group, *model.AppError) DeleteGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) + DeleteGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) DeleteIncomingWebhook(hookID string) *model.AppError DeleteOAuthApp(appID string) *model.AppError @@ -604,7 +606,7 @@ type AppIface interface { GetFlaggedPostsForChannel(userID, channelID string, offset int, limit int) (*model.PostList, *model.AppError) GetFlaggedPostsForTeam(userID, teamID string, offset int, limit int) (*model.PostList, *model.AppError) GetGlobalRetentionPolicy() (*model.GlobalRetentionPolicy, *model.AppError) - GetGroup(id string) (*model.Group, *model.AppError) + GetGroup(id string, opts *model.GetGroupOpts) (*model.Group, *model.AppError) GetGroupByName(name string, opts model.GroupSearchOpts) (*model.Group, *model.AppError) GetGroupByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, *model.AppError) GetGroupChannel(userIDs []string) (*model.Channel, *model.AppError) @@ -782,6 +784,7 @@ type AppIface interface { GetUsersNotInChannel(teamID string, channelID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) GetUsersNotInChannelMap(teamID string, channelID string, groupConstrained bool, offset int, limit int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) (map[string]*model.User, *model.AppError) GetUsersNotInChannelPage(teamID string, channelID string, groupConstrained bool, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) + GetUsersNotInGroupPage(groupID string, page int, perPage int) ([]*model.User, *model.AppError) GetUsersNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) GetUsersNotInTeamEtag(teamID string, restrictionsHash string) string GetUsersNotInTeamPage(teamID string, groupConstrained bool, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) @@ -960,6 +963,7 @@ type AppIface interface { SearchUsersInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) SearchUsersInTeam(teamID, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) SearchUsersNotInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) + SearchUsersNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) SearchUsersNotInTeam(notInTeamId string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) SearchUsersWithoutTeam(term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) SendAckToPushProxy(ack *model.PushNotificationAck) error @@ -979,6 +983,7 @@ type AppIface interface { SessionHasPermissionToChannel(session model.Session, channelID string, permission *model.Permission) bool SessionHasPermissionToChannelByPost(session model.Session, postID string, permission *model.Permission) bool SessionHasPermissionToCreateJob(session model.Session, job *model.Job) (bool, *model.Permission) + SessionHasPermissionToGroup(session model.Session, groupID string, permission *model.Permission) bool SessionHasPermissionToReadJob(session model.Session, jobType string) (bool, *model.Permission) SessionHasPermissionToTeam(session model.Session, teamID string, permission *model.Permission) bool SessionHasPermissionToUser(session model.Session, userID string) bool @@ -1082,6 +1087,7 @@ type AppIface interface { UploadEmojiImage(id string, imageData *multipart.FileHeader) *model.AppError UploadMultipartFiles(c *request.Context, teamID string, channelID string, userID string, fileHeaders []*multipart.FileHeader, clientIds []string, now time.Time) (*model.FileUploadResponse, *model.AppError) UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) + UpsertGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) UpsertGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError) VerifyEmailFromToken(userSuppliedTokenString string) *model.AppError diff --git a/app/app_test.go b/app/app_test.go index 310cd86076..525bb10667 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -164,6 +164,10 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { model.PermissionCreateGroupChannel.Id, model.PermissionViewMembers.Id, model.PermissionCreateTeam.Id, + model.PermissionCreateCustomGroup.Id, + model.PermissionEditCustomGroup.Id, + model.PermissionDeleteCustomGroup.Id, + model.PermissionManageCustomGroupMembers.Id, }, "system_post_all": { model.PermissionCreatePost.Id, @@ -219,6 +223,10 @@ func TestDoEmojisPermissionsMigration(t *testing.T) { role3, err3 := th.App.GetRoleByName(context.Background(), model.SystemUserRoleId) assert.Nil(t, err3) expected3 := []string{ + model.PermissionCreateCustomGroup.Id, + model.PermissionEditCustomGroup.Id, + model.PermissionDeleteCustomGroup.Id, + model.PermissionManageCustomGroupMembers.Id, model.PermissionListPublicTeams.Id, model.PermissionJoinPublicTeams.Id, model.PermissionCreateDirectChannel.Id, diff --git a/app/authorization.go b/app/authorization.go index 6edc586be3..a5064f93a6 100644 --- a/app/authorization.go +++ b/app/authorization.go @@ -5,6 +5,8 @@ package app import ( "context" + "database/sql" + "errors" "net/http" "strings" @@ -88,6 +90,25 @@ func (a *App) SessionHasPermissionToChannel(session model.Session, channelID str return a.SessionHasPermissionTo(session, permission) } +func (a *App) SessionHasPermissionToGroup(session model.Session, groupID string, permission *model.Permission) bool { + groupMember, err := a.Srv().Store.Group().GetMember(groupID, session.UserId) + // don't reject immediately on ErrNoRows error because there's further authz logic below for non-groupmembers + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return false + } + + // each member of a group is implicitly considered to have the 'custom_group_user' role in that group, so if the user is a member of the + // group and custom_group_user on their system has the requested permission then return true + if groupMember != nil && a.RolesGrantPermission([]string{model.CustomGroupUserRoleId}, permission.Id) { + return true + } + + // Not implemented: group-override schemes. + + // ...otherwise check their system roles to see if they have the requested permission system-wide + return a.SessionHasPermissionTo(session, permission) +} + func (a *App) SessionHasPermissionToChannelByPost(session model.Session, postID string, permission *model.Permission) bool { if channelMember, err := a.Srv().Store.Channel().GetMemberForPost(postID, session.UserId); err == nil { diff --git a/app/authorization_test.go b/app/authorization_test.go index ba1990116c..3c6f132869 100644 --- a/app/authorization_test.go +++ b/app/authorization_test.go @@ -4,7 +4,13 @@ package app import ( + "context" + "encoding/csv" "fmt" + "io/ioutil" + "os" + "strconv" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -118,3 +124,87 @@ func TestHasPermissionToCategory(t *testing.T) { require.Nil(t, err) require.False(t, th.App.SessionHasPermissionToCategory(*session, th.BasicUser.Id, th.BasicTeam.Id, categories2.Order[0])) } + +func TestSessionHasPermissionToGroup(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + file, e := os.Open("tests/group-role-has-permission.csv") + require.NoError(t, e) + defer file.Close() + + b, e := ioutil.ReadAll(file) + require.NoError(t, e) + + r := csv.NewReader(strings.NewReader(string(b))) + records, e := r.ReadAll() + require.NoError(t, e) + + systemRole, err := th.App.GetRoleByName(context.Background(), model.SystemUserRoleId) + require.Nil(t, err) + + groupRole, err := th.App.GetRoleByName(context.Background(), model.CustomGroupUserRoleId) + require.Nil(t, err) + + group, err := th.App.CreateGroup(&model.Group{ + Name: model.NewString(model.NewId()), + DisplayName: model.NewId(), + Source: model.GroupSourceCustom, + AllowReference: true, + }) + require.Nil(t, err) + + permission := model.PermissionDeleteCustomGroup + + for i, row := range records { + // skip csv header + if i == 0 { + continue + } + + systemRoleHasPermission, e := strconv.ParseBool(row[0]) + require.NoError(t, e) + + isGroupMember, e := strconv.ParseBool(row[1]) + require.NoError(t, e) + + groupRoleHasPermission, e := strconv.ParseBool(row[2]) + require.NoError(t, e) + + permissionShouldBeGranted, e := strconv.ParseBool(row[3]) + require.NoError(t, e) + + if systemRoleHasPermission { + th.AddPermissionToRole(permission.Id, systemRole.Name) + } else { + th.RemovePermissionFromRole(permission.Id, systemRole.Name) + } + + if isGroupMember { + _, err := th.App.UpsertGroupMember(group.Id, th.BasicUser.Id) + require.Nil(t, err) + } else { + _, err := th.App.DeleteGroupMember(group.Id, th.BasicUser.Id) + if err != nil && err.Id != "app.group.no_rows" { + t.Error(err) + } + } + + if groupRoleHasPermission { + th.AddPermissionToRole(permission.Id, groupRole.Name) + } else { + th.RemovePermissionFromRole(permission.Id, groupRole.Name) + } + + session, err := th.App.CreateSession(&model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}, Roles: systemRole.Name}) + require.Nil(t, err) + + result := th.App.SessionHasPermissionToGroup(*session, group.Id, permission) + + if permissionShouldBeGranted { + require.True(t, result, fmt.Sprintf("row: %v", row)) + } else { + require.False(t, result, fmt.Sprintf("row: %v", row)) + } + } +} diff --git a/app/group.go b/app/group.go index cb960f9248..f0f9e8482f 100644 --- a/app/group.go +++ b/app/group.go @@ -13,7 +13,7 @@ import ( "github.com/mattermost/mattermost-server/v6/store" ) -func (a *App) GetGroup(id string) (*model.Group, *model.AppError) { +func (a *App) GetGroup(id string, opts *model.GetGroupOpts) (*model.Group, *model.AppError) { group, err := a.Srv().Store.Group().Get(id) if err != nil { var nfErr *store.ErrNotFound @@ -25,6 +25,14 @@ func (a *App) GetGroup(id string) (*model.Group, *model.AppError) { } } + if opts != nil && opts.IncludeMemberCount { + memberCount, err := a.Srv().Store.Group().GetMemberCount(id) + if err != nil { + return nil, model.NewAppError("GetGroup", "app.member_count", nil, err.Error(), http.StatusInternalServerError) + } + group.MemberCount = model.NewInt(int(memberCount)) + } + return group, nil } @@ -94,10 +102,49 @@ func (a *App) CreateGroup(group *model.Group) (*model.Group, *model.AppError) { return group, nil } +func (a *App) CreateGroupWithUserIds(group *model.GroupWithUserIds) (*model.Group, *model.AppError) { + newGroup, err := a.Srv().Store.Group().CreateWithUserIds(group) + if err != nil { + var invErr *store.ErrInvalidInput + var appErr *model.AppError + var dupKey *store.ErrUniqueConstraint + switch { + case errors.As(err, &appErr): + return nil, appErr + case errors.As(err, &invErr): + return nil, model.NewAppError("CreateGroupWithUserIds", "app.group.id.app_error", nil, invErr.Error(), http.StatusBadRequest) + case errors.As(err, &dupKey): + return nil, model.NewAppError("CreateGroup", "app.custom_group.unique_name", nil, dupKey.Error(), http.StatusBadRequest) + default: + return nil, model.NewAppError("CreateGroupWithUserIds", "app.insert_error", nil, err.Error(), http.StatusInternalServerError) + } + } + + messageWs := model.NewWebSocketEvent(model.WebsocketEventReceivedGroup, "", "", "", nil) + count, err := a.Srv().Store.Group().GetMemberCount(newGroup.Id) + if err != nil { + return nil, model.NewAppError("CreateGroupWithUserIds", "app.group.id.app_error", nil, err.Error(), http.StatusBadRequest) + } + group.MemberCount = model.NewInt(int(count)) + groupJSON, jsonErr := json.Marshal(newGroup) + if jsonErr != nil { + mlog.Warn("Failed to encode group to JSON", mlog.Err(jsonErr)) + } + messageWs.Add("group", string(groupJSON)) + a.Publish(messageWs) + + return newGroup, nil +} + func (a *App) UpdateGroup(group *model.Group) (*model.Group, *model.AppError) { updatedGroup, err := a.Srv().Store.Group().Update(group) if err == nil { + count, countErr := a.Srv().Store.Group().GetMemberCount(updatedGroup.Id) + if countErr != nil { + return nil, model.NewAppError("CreateGroupWithUserIds", "app.group.id.app_error", nil, countErr.Error(), http.StatusBadRequest) + } + updatedGroup.MemberCount = model.NewInt(int(count)) messageWs := model.NewWebSocketEvent(model.WebsocketEventReceivedGroup, "", "", "", nil) groupJSON, jsonErr := json.Marshal(updatedGroup) if jsonErr != nil { @@ -110,11 +157,14 @@ func (a *App) UpdateGroup(group *model.Group) (*model.Group, *model.AppError) { if err != nil { var nfErr *store.ErrNotFound var appErr *model.AppError + var dupKey *store.ErrUniqueConstraint switch { case errors.As(err, &appErr): return nil, appErr case errors.As(err, &nfErr): return nil, model.NewAppError("UpdateGroup", "app.group.no_rows", nil, nfErr.Error(), http.StatusNotFound) + case errors.As(err, &dupKey): + return nil, model.NewAppError("CreateGroup", "app.custom_group.unique_name", nil, dupKey.Error(), http.StatusBadRequest) default: return nil, model.NewAppError("UpdateGroup", "app.select_error", nil, err.Error(), http.StatusInternalServerError) } @@ -125,17 +175,6 @@ func (a *App) UpdateGroup(group *model.Group) (*model.Group, *model.AppError) { func (a *App) DeleteGroup(groupID string) (*model.Group, *model.AppError) { deletedGroup, err := a.Srv().Store.Group().Delete(groupID) - - if err == nil { - messageWs := model.NewWebSocketEvent(model.WebsocketEventReceivedGroup, "", "", "", nil) - groupJSON, jsonErr := json.Marshal(deletedGroup) - if jsonErr != nil { - mlog.Warn("Failed to encode group to JSON", mlog.Err(jsonErr)) - } - messageWs.Add("group", string(groupJSON)) - a.Publish(messageWs) - } - if err != nil { var nfErr *store.ErrNotFound switch { @@ -177,7 +216,15 @@ func (a *App) GetGroupMemberUsersPage(groupID string, page int, perPage int) ([] if appErr != nil { return nil, 0, appErr } - return members, int(count), nil + return a.sanitizeProfiles(members, false), int(count), nil +} +func (a *App) GetUsersNotInGroupPage(groupID string, page int, perPage int) ([]*model.User, *model.AppError) { + members, err := a.Srv().Store.Group().GetNonMemberUsersPage(groupID, page, perPage) + if err != nil { + return nil, model.NewAppError("GetUsersNotInGroupPage", "app.select_error", nil, err.Error(), http.StatusInternalServerError) + } + + return a.sanitizeProfiles(members, false), nil } func (a *App) UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) { @@ -195,6 +242,8 @@ func (a *App) UpsertGroupMember(groupID string, userID string) (*model.GroupMemb } } + a.publishGroupMemberEvent(model.WebsocketEventGroupMemberAdd, groupMember) + return groupMember, nil } @@ -210,6 +259,8 @@ func (a *App) DeleteGroupMember(groupID string, userID string) (*model.GroupMemb } } + a.publishGroupMemberEvent(model.WebsocketEventGroupMemberDelete, groupMember) + return groupMember, nil } @@ -515,6 +566,10 @@ func (a *App) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, pag return nil, 0, model.NewAppError("TeamMembersMinusGroupMembers", "app.select_error", nil, err.Error(), http.StatusInternalServerError) } + for _, u := range users { + a.SanitizeProfile(&u.User, false) + } + // parse all group ids of all users allUsersGroupIDMap := map[string]bool{} for _, user := range users { @@ -579,6 +634,10 @@ func (a *App) ChannelMembersMinusGroupMembers(channelID string, groupIDs []strin return nil, 0, model.NewAppError("ChannelMembersMinusGroupMembers", "app.select_error", nil, err.Error(), http.StatusInternalServerError) } + for _, u := range users { + a.SanitizeProfile(&u.User, false) + } + // parse all group ids of all users allUsersGroupIDMap := map[string]bool{} for _, user := range users { @@ -637,3 +696,57 @@ func (a *App) UserIsInAdminRoleGroup(userID, syncableID string, syncableType mod return true, nil } + +func (a *App) UpsertGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) { + members, err := a.Srv().Store.Group().UpsertMembers(groupID, userIDs) + if err != nil { + var invErr *store.ErrInvalidInput + var appErr *model.AppError + switch { + case errors.As(err, &appErr): + return nil, appErr + case errors.As(err, &invErr): + return nil, model.NewAppError("UpsertGroupMembers", "app.group.uniqueness_error", nil, invErr.Error(), http.StatusBadRequest) + default: + return nil, model.NewAppError("UpsertGroupMembers", "app.update_error", nil, err.Error(), http.StatusInternalServerError) + } + } + + for _, groupMember := range members { + a.publishGroupMemberEvent(model.WebsocketEventGroupMemberAdd, groupMember) + } + + return members, nil +} + +func (a *App) DeleteGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) { + members, err := a.Srv().Store.Group().DeleteMembers(groupID, userIDs) + if err != nil { + var invErr *store.ErrInvalidInput + var appErr *model.AppError + switch { + case errors.As(err, &appErr): + return nil, appErr + case errors.As(err, &invErr): + return nil, model.NewAppError("DeleteGroupMember", "app.group.uniqueness_error", nil, invErr.Error(), http.StatusBadRequest) + default: + return nil, model.NewAppError("DeleteGroupMember", "app.update_error", nil, err.Error(), http.StatusInternalServerError) + } + } + + for _, groupMember := range members { + a.publishGroupMemberEvent(model.WebsocketEventGroupMemberDelete, groupMember) + } + + return members, nil +} + +func (a *App) publishGroupMemberEvent(eventName string, groupMember *model.GroupMember) { + messageWs := model.NewWebSocketEvent(eventName, "", "", groupMember.UserId, nil) + groupMemberJSON, jsonErr := json.Marshal(groupMember) + if jsonErr != nil { + mlog.Warn("failed to encode group member to JSON", mlog.Err(jsonErr)) + } + messageWs.Add("group_member", string(groupMemberJSON)) + a.Publish(messageWs) +} diff --git a/app/group_test.go b/app/group_test.go index 4b660b07af..2438689d58 100644 --- a/app/group_test.go +++ b/app/group_test.go @@ -17,13 +17,21 @@ func TestGetGroup(t *testing.T) { defer th.TearDown() group := th.CreateGroup() - group, err := th.App.GetGroup(group.Id) + group, err := th.App.GetGroup(group.Id, nil) require.Nil(t, err) require.NotNil(t, group) - group, err = th.App.GetGroup(model.NewId()) + nilGroup, err := th.App.GetGroup(model.NewId(), nil) require.NotNil(t, err) - require.Nil(t, group) + require.Nil(t, nilGroup) + + group, err = th.App.GetGroup(group.Id, &model.GetGroupOpts{IncludeMemberCount: false}) + require.Nil(t, err) + require.Nil(t, group.MemberCount) + + group, err = th.App.GetGroup(group.Id, &model.GetGroupOpts{IncludeMemberCount: true}) + require.Nil(t, err) + require.NotNil(t, group.MemberCount) } func TestGetGroupByRemoteID(t *testing.T) { @@ -31,7 +39,7 @@ func TestGetGroupByRemoteID(t *testing.T) { defer th.TearDown() group := th.CreateGroup() - g, err := th.App.GetGroupByRemoteID(group.RemoteId, model.GroupSourceLdap) + g, err := th.App.GetGroupByRemoteID(*group.RemoteId, model.GroupSourceLdap) require.Nil(t, err) require.NotNil(t, g) @@ -65,7 +73,7 @@ func TestCreateGroup(t *testing.T) { DisplayName: "dn_" + id, Name: model.NewString("name" + id), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } g, err := th.App.CreateGroup(group) diff --git a/app/helper_test.go b/app/helper_test.go index 19be290d87..e3f7d5c12f 100644 --- a/app/helper_test.go +++ b/app/helper_test.go @@ -479,7 +479,7 @@ func (th *TestHelper) CreateGroup() *model.Group { Name: model.NewString("name" + id), Source: model.GroupSourceLdap, Description: "description_" + id, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } var err *model.AppError diff --git a/app/notification.go b/app/notification.go index d572066891..34af28b6d2 100644 --- a/app/notification.go +++ b/app/notification.go @@ -1060,7 +1060,7 @@ func (a *App) allowGroupMentions(post *model.Post) bool { func (a *App) getGroupsAllowedForReferenceInChannel(channel *model.Channel, team *model.Team) (map[string]*model.Group, error) { var err error groupsMap := make(map[string]*model.Group) - opts := model.GroupSearchOpts{FilterAllowReference: true} + opts := model.GroupSearchOpts{FilterAllowReference: true, IncludeMemberCount: true} if channel.IsGroupConstrained() || (team != nil && team.IsGroupConstrained()) { var groups []*model.GroupWithSchemeAdmin diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index dad1e50ca1..67a39f8e5b 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -2044,6 +2044,28 @@ func (a *OpenTracingAppLayer) CreateGroupChannel(userIDs []string, creatorId str return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) CreateGroupWithUserIds(group *model.GroupWithUserIds) (*model.Group, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateGroupWithUserIds") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.CreateGroupWithUserIds(group) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) CreateGuest(c *request.Context, user *model.User) (*model.User, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateGuest") @@ -3044,6 +3066,28 @@ func (a *OpenTracingAppLayer) DeleteGroupMember(groupID string, userID string) ( return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) DeleteGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteGroupMembers") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.DeleteGroupMembers(groupID, userIDs) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteGroupSyncable") @@ -5975,7 +6019,7 @@ func (a *OpenTracingAppLayer) GetGlobalRetentionPolicy() (*model.GlobalRetention return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetGroup(id string) (*model.Group, *model.AppError) { +func (a *OpenTracingAppLayer) GetGroup(id string, opts *model.GetGroupOpts) (*model.Group, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroup") @@ -5987,7 +6031,7 @@ func (a *OpenTracingAppLayer) GetGroup(id string) (*model.Group, *model.AppError }() defer span.Finish() - resultVar0, resultVar1 := a.app.GetGroup(id) + resultVar0, resultVar1 := a.app.GetGroup(id, opts) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -10185,6 +10229,28 @@ func (a *OpenTracingAppLayer) GetUsersNotInChannelPage(teamID string, channelID return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) GetUsersNotInGroupPage(groupID string, page int, perPage int) ([]*model.User, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersNotInGroupPage") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.GetUsersNotInGroupPage(groupID, page, perPage) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) GetUsersNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersNotInTeam") @@ -14224,6 +14290,28 @@ func (a *OpenTracingAppLayer) SearchUsersNotInChannel(teamID string, channelID s return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) SearchUsersNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchUsersNotInGroup") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.SearchUsersNotInGroup(groupID, term, options) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) SearchUsersNotInTeam(notInTeamId string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchUsersNotInTeam") @@ -14644,6 +14732,23 @@ func (a *OpenTracingAppLayer) SessionHasPermissionToCreateJob(session model.Sess return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) SessionHasPermissionToGroup(session model.Session, groupID string, permission *model.Permission) bool { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToGroup") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0 := a.app.SessionHasPermissionToGroup(session, groupID, permission) + + return resultVar0 +} + func (a *OpenTracingAppLayer) SessionHasPermissionToManageBot(session model.Session, botUserId string) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToManageBot") @@ -17163,6 +17268,28 @@ func (a *OpenTracingAppLayer) UpsertGroupMember(groupID string, userID string) ( return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) UpsertGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpsertGroupMembers") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.UpsertGroupMembers(groupID, userIDs) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) UpsertGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpsertGroupSyncable") diff --git a/app/permissions_migrations.go b/app/permissions_migrations.go index 93fb4fd62d..d42bc36e43 100644 --- a/app/permissions_migrations.go +++ b/app/permissions_migrations.go @@ -915,6 +915,29 @@ func (a *App) getAddTestEmailAncillaryPermission() (permissionsMap, error) { return transformations, nil } +func (a *App) getAddCustomUserGroupsPermissions() (permissionsMap, error) { + t := []permissionTransformation{} + + customGroupPermissions := []string{ + model.PermissionCreateCustomGroup.Id, + model.PermissionManageCustomGroupMembers.Id, + model.PermissionEditCustomGroup.Id, + model.PermissionDeleteCustomGroup.Id, + } + + t = append(t, permissionTransformation{ + On: isRole(model.SystemUserRoleId), + Add: customGroupPermissions, + }) + + t = append(t, permissionTransformation{ + On: isRole(model.SystemAdminRoleId), + Add: customGroupPermissions, + }) + + return t, nil +} + func (a *App) getAddPlaybooksPermissions() (permissionsMap, error) { transformations := []permissionTransformation{} @@ -989,6 +1012,7 @@ func (s *Server) doPermissionsMigrations() error { {Key: model.MigrationKeyAddReportingSubsectionPermissions, Migration: a.getAddReportingSubsectionPermissions}, {Key: model.MigrationKeyAddTestEmailAncillaryPermission, Migration: a.getAddTestEmailAncillaryPermission}, {Key: model.MigrationKeyAddPlaybooksPermissions, Migration: a.getAddPlaybooksPermissions}, + {Key: model.MigrationKeyAddCustomUserGroupsPermissions, Migration: a.getAddCustomUserGroupsPermissions}, } roles, err := s.Store.Role().GetAll() diff --git a/app/plugin_api.go b/app/plugin_api.go index e93d81cfda..84280ac1d1 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -597,7 +597,7 @@ func (api *PluginAPI) DeleteChannelMember(channelID, userID string) *model.AppEr } func (api *PluginAPI) GetGroup(groupId string) (*model.Group, *model.AppError) { - return api.app.GetGroup(groupId) + return api.app.GetGroup(groupId, nil) } func (api *PluginAPI) GetGroupByName(name string) (*model.Group, *model.AppError) { diff --git a/app/syncables_test.go b/app/syncables_test.go index ae8a171eb0..778d4159d3 100644 --- a/app/syncables_test.go +++ b/app/syncables_test.go @@ -59,7 +59,7 @@ func TestCreateDefaultMemberships(t *testing.T) { gleeGroup, err := th.App.CreateGroup(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: "Glee Club", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, }) if err != nil { @@ -69,7 +69,7 @@ func TestCreateDefaultMemberships(t *testing.T) { scienceGroup, err := th.App.CreateGroup(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: "Science Club", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, }) if err != nil { diff --git a/app/user.go b/app/user.go index f042c4d31e..dc566165d1 100644 --- a/app/user.go +++ b/app/user.go @@ -1735,6 +1735,9 @@ func (a *App) SearchUsers(props *model.UserSearch, options *model.UserSearchOpti if props.InGroupId != "" { return a.SearchUsersInGroup(props.InGroupId, props.Term, options) } + if props.NotInGroupId != "" { + return a.SearchUsersNotInGroup(props.NotInGroupId, props.Term, options) + } return a.SearchUsersInTeam(props.TeamId, props.Term, options) } @@ -1822,6 +1825,20 @@ func (a *App) SearchUsersInGroup(groupID string, term string, options *model.Use return users, nil } +func (a *App) SearchUsersNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) { + term = strings.TrimSpace(term) + users, err := a.Srv().Store.User().SearchNotInGroup(groupID, term, options) + if err != nil { + return nil, model.NewAppError("SearchUsersNotInGroup", "app.user.search.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + for _, user := range users { + a.SanitizeProfile(user, options.IsAdmin) + } + + return users, nil +} + func (a *App) AutocompleteUsersInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, *model.AppError) { term = strings.TrimSpace(term) diff --git a/config/client.go b/config/client.go index 10fda6454e..c1918a1426 100644 --- a/config/client.go +++ b/config/client.go @@ -95,9 +95,6 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li props["CloudUserLimit"] = strconv.FormatInt(*c.ExperimentalSettings.CloudUserLimit, 10) - // TODO: remove this when the mobile client release reaches 1.52. - props["EnableReliableWebSockets"] = strconv.FormatBool(true) - // Set default values for all options that require a license. props["ExperimentalEnableAuthenticationTransfer"] = "true" props["LdapNicknameAttributeSet"] = "false" @@ -205,6 +202,10 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li props["ExperimentalSharedChannels"] = strconv.FormatBool(*c.ExperimentalSettings.EnableSharedChannels) props["ExperimentalRemoteClusterService"] = strconv.FormatBool(c.FeatureFlags.EnableRemoteClusterService && *c.ExperimentalSettings.EnableRemoteClusterService) } + + if license.SkuShortName == model.LicenseShortSkuProfessional || license.SkuShortName == model.LicenseShortSkuEnterprise { + props["EnableCustomGroups"] = strconv.FormatBool(*c.ServiceSettings.EnableCustomGroups) + } } return props diff --git a/go.tools.sum b/go.tools.sum new file mode 100644 index 0000000000..9f3e68876b --- /dev/null +++ b/go.tools.sum @@ -0,0 +1,868 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.63.0/go.mod h1:GmezbQc7T2snqkEXWfZ0sy0VfkB/ivI2DdtJL2DEmlg= +cloud.google.com/go v0.64.0 h1:xVP3LPvMjGT4J0a55y02Gw5y/dkY/rxGz58sfK1jqIo= +cloud.google.com/go v0.64.0/go.mod h1:xfORb36jGvE+6EexW71nMEtL025s3x6xvuYUKM4JLv4= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/spanner v1.9.0 h1:WXuGWhUp5i7MeUMzMrJlodqJvSGtU0Cdw6BdHGgCgVo= +cloud.google.com/go/spanner v1.9.0/go.mod h1:xvlEn0NZ5v1iJPYsBnUVRDNvccDxsBTEi16pJRKQVws= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/ClickHouse/clickhouse-go v1.3.12 h1:HvD2NhKPLSeO3Ots6YV0ePgs4l3wO0bLqa9Uk1yeMOs= +github.com/ClickHouse/clickhouse-go v1.3.12/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= +github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/apache/arrow/go/arrow v0.0.0-20200601151325-b2287a20f230 h1:5ultmol0yeX75oh1hY78uAFn3dupBQ/QUNxERCkiaUQ= +github.com/apache/arrow/go/arrow v0.0.0-20200601151325-b2287a20f230/go.mod h1:QNYViu/X0HXDHw7m3KXzWSVXIbfUvJqBFe6Gj8/pYA0= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go v1.17.7 h1:/4+rDPe0W95KBmNGYCG+NUvdL8ssPYBMxL+aSCg6nIA= +github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= +github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/cenkalti/backoff/v4 v4.0.2 h1:JIufpQLbh4DkbQoii76ItQIUFzevQSqOLZca4eamEDs= +github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY/k9Os/UFtwERei2/XzGemhpGnBKNg= +github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/cockroachdb/cockroach-go v0.0.0-20190925194419-606b3d062051 h1:eApuUG8W2EtBVwxqLlY2wgoqDYOg3WvIHGvW4fUbbow= +github.com/cockroachdb/cockroach-go v0.0.0-20190925194419-606b3d062051/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk= +github.com/containerd/containerd v1.4.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 h1:XNT/Zf5l++1Pyg08/HV04ppB0gKxAqtZQBRYiYrUuYk= +github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.0.0-20200620013148-b91950f658ec h1:NfhRXXFDPxcF5Cwo06DzeIaE7uuJtAUhsDwH3LNsjos= +github.com/denisenkom/go-mssqldb v0.0.0-20200620013148-b91950f658ec/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dhui/dktest v0.3.3/go.mod h1:EML9sP4sqJELHn4jV7B0TY8oF6077nk83/tz7M56jcQ= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 h1:aaQcKT9WumO6JEJcRyTqFVq4XUZiUcKR2/GI31TOcz8= +github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-bindata/go-bindata v1.0.0 h1:DZ34txDXWn1DyWa+vQf7V9ANc2ILTtrEjtlsdJRF26M= +github.com/go-bindata/go-bindata v3.1.2+incompatible h1:5vjJMVhowQdPzjE1LdxyFF7YFTXg5IgGVW4gBr5IbvE= +github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= +github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4 h1:vF83LI8tAakwEwvWZtrIEx7pOySacl2TOxx6eXk4ePo= +github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang-migrate/migrate v1.3.2 h1:QAlFV1QF9zdkzy/jujlBVkVu+L/+k18cg8tuY1/4JDY= +github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA= +github.com/golang-migrate/migrate/v4 v4.14.1 h1:qmRd/rNGjM1r3Ve5gHd5ZplytrD02UcItYNxJ3iUHHE= +github.com/golang-migrate/migrate/v4 v4.14.1/go.mod h1:l7Ks0Au6fYHuUIxUhQ0rcVX1uLlJg54C/VvW7tvxSz0= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/flatbuffers v1.11.0 h1:O7CEyB8Cb3/DmtxODGtLHcEvpr81Jm5qLg/hsHnxA2A= +github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.3.2 h1:9UIGICxEAW70RQDGilGwsCG63NCcm5amjuBQCFzrmsw= +github.com/jackc/pgconn v1.3.2/go.mod h1:LvCquS3HbBKwgl7KbX9KyqEIumJAbm1UMcTvGaIf3bM= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.1 h1:Rdjp4NFjwHnEslx2b66FfCI2S0LhO4itac3hXz6WX9M= +github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +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/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jteeuwen/go-bindata v3.0.7+incompatible h1:91Uy4d9SYVr1kyTJ15wJsog+esAZZl7JmEfTkwmhJts= +github.com/jteeuwen/go-bindata v3.0.7+incompatible/go.mod h1:JVvhzYOiGBnFSYRyV00iY8q7/0PThjIYav1p9h5dmKs= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/k0kubun/pp v2.3.0+incompatible h1:EKhKbi34VQDWJtq+zpsKSEhkHHs9w2P8Izbq8IhLVSo= +github.com/k0kubun/pp v2.3.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/ktrysmt/go-bitbucket v0.6.4 h1:C8dUGp0qkwncKtAnozHCbbqhptefzEd1I0sfnuy9rYQ= +github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg= +github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20200721093743-053c38dcd293 h1:N9FaqZD58xkAIJGFGY5qdKXGlY2aNMaYQ9KkVEXv8xE= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20200721093743-053c38dcd293/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20200730135456-e2334fe87160 h1:+JXFDXEkbMieBt54Rihj3ppnZ/Pm1kZxkLOD0hrWFzs= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20200730135456-e2334fe87160/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20200814100246-5fc14ed48292 h1:swpUQyq7HWhc0aStUAgdZI6o0UOD9OzXJxbxUTLo1fo= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20200814100246-5fc14ed48292/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20200821090819-f7569391b4d3 h1:H2Ir959KhgPrj+/zBP73pW5zxrC32HgICWtcerxCtcc= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20200821090819-f7569391b4d3/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20200825115902-a83581379d0a h1:X8TDSx47wrnwa+sSn6Sz3YSHVDyUwN2lA4dVpMdhX+g= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20200825115902-a83581379d0a/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20200828152206-519b99a4e51e h1:dQ+AQmmcn+kncwE/XfC3xVPyegR84d4dno/UOXI3eJs= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20200828152206-519b99a4e51e/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20200915114419-f4421bc07461 h1:dn2/HZjzUY5PQmKDmH95vgwVqpbR84FiU/t060g7rqg= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20200915114419-f4421bc07461/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20200926180007-fd1b679200e5 h1:1fVtMi+1XPAtxffRZiMSiy7Zm4Od3fyHWnC2BydfYHY= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20200926180007-fd1b679200e5/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20201001222518-66764584f346 h1:ByexaM3lb5ZdHGtcB1QwRkDXDxt/2MUTvI8+LjwhDXg= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20201001222518-66764584f346/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20201013204121-e463fcd00ba8 h1:6wiKDdS/MMY0Jht12lRZJq1zv5eHEd4fK60SwkUVGjA= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20201013204121-e463fcd00ba8/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20201216181404-3faa6075089a/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210103185547-4c12aa739237 h1:w6GQs7SU6abD0QXKoqwd4bpEzNaW1xKwpeftBpIX1Do= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210103185547-4c12aa739237/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210122154757-d807da7d142a h1:H8g7UkbNEpYE6UD7ej8UWp2W9q0eW+7BtvUl1v+bGMc= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210122154757-d807da7d142a/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210205183519-d9e542a75ab2 h1:HMz7yZQBfrnek35I5OOxdNdIt0rYoTnhFVn+Uq3TGi8= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210205183519-d9e542a75ab2/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210218104610-40d7640e8538 h1:7G1fAVBGjrQaKqXnxRjVYMJfuiE6iVMdt9YhBwe2enM= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210218104610-40d7640e8538/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210309083648-c1e5575135f9 h1:EdA8k1LBxdk1SslBITXYiGVIptfPWFt7fRwxiy2BsTk= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210309083648-c1e5575135f9/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210714114450-fbc82c4cf833 h1:Cgx5Md/4umqKYAgu8oPTZ+vDPZ5DaaRpjUjR+CUsmNI= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210714114450-fbc82c4cf833/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210721133912-8b250bf4d0f6 h1:G/PSgH19gSY1cMhFeUQzrrJ164PJ5OAW8ghyupR95qg= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210721133912-8b250bf4d0f6/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20211006080735-07b1b58b8b09 h1:I2CLjgClRQF/yGxHqUGzQ9bAibgwuUlAuReP8brP7Ro= +github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20211006080735-07b1b58b8b09/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= +github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mutecomm/go-sqlcipher/v4 v4.4.0 h1:sV1tWCWGAVlPhNGT95Q+z/txFxuhAYWwHD1afF5bMZg= +github.com/mutecomm/go-sqlcipher/v4 v4.4.0/go.mod h1:PyN04SaWalavxRGH9E8ZftG6Ju7rsPrGmQRjrEaVpiY= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8 h1:P48LjvUQpTReR3TQRbxSeSBsMXzfK0uol7eRcr7VBYQ= +github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA= +github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba h1:fhFP5RliM2HW/8XdcO5QngSfFli9GcRIpMXvypTQt6E= +github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba/go.mod h1:ncO5VaFWh0Nrt+4KT4mOZboaczBZcLuHrG+/sUeP8gI= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= +github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/reflog/struct2interface v0.6.1 h1:ZnCx+rf/kE+qVB80b5+AdTyJFgTVh3s2liGwcgWT14U= +github.com/reflog/struct2interface v0.6.1/go.mod h1:Hj4XSqbzQyLswqmKfmGqzOlh4xCRPSl27779XT9TPN4= +github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237 h1:HQagqIiBmr8YXawX/le3+O26N+vPPC1PtjaF3mwnook= +github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/snowflakedb/glog v0.0.0-20180824191149-f5055e6f21ce h1:CGR1hXCOeoZ1aJhCs8qdKJuEu3xoZnxsLcYoh5Bnr+4= +github.com/snowflakedb/glog v0.0.0-20180824191149-f5055e6f21ce/go.mod h1:EB/w24pR5VKI60ecFnKqXzxX3dOorz1rnVicQTQrGM0= +github.com/snowflakedb/gosnowflake v1.3.5 h1:/Ep0cXv4/3o+iXQvh+6CDjHCRPk2AM42l/AMR9PM94Q= +github.com/snowflakedb/gosnowflake v1.3.5/go.mod h1:13Ky+lxzIm3VqNDZJdyvu9MCGy+WgRdYFdXp96UcLZU= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31 h1:OXcKh35JaYsGMRzpvFkLv/MEyPuL49CThT1pZ8aSml4= +github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/vektra/mockery v1.1.2 h1:uc0Yn67rJpjt8U/mAZimdCKn9AeA97BOkjpmtBSlfP4= +github.com/vektra/mockery v1.1.2/go.mod h1:VcfZjKaFOPO+MpN4ZvwPjs4c48lkq1o3Ym8yHZJu0jU= +github.com/xanzy/go-gitlab v0.15.0 h1:rWtwKTgEnXyNUGrOArN7yyc3THRkpYcKXIXia9abywQ= +github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= +github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0= +github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs= +gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.mongodb.org/mongo-driver v1.1.0 h1:aeOqSrhl9eDRAap/3T5pCfMBEBxZ0vuXBP+RMtp2KX8= +go.mongodb.org/mongo-driver v1.1.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg= +golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190225153610-fe579d43d832/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d h1:dOiJ2n2cMwGLce/74I/QHMbnpk5GfY7InR8rczoMqRM= +golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201029080932-201ba4db2418 h1:HlFl4V6pEMziuLXyRkm5BIYq1y1GAbb02pRlWvI54OM= +golang.org/x/sys v0.0.0-20201029080932-201ba4db2418/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200206050830-dd0d5d485177 h1:E2vxBajJgSA3TcJhDGTh/kP3VnsvXKl9jSijv+h7svQ= +golang.org/x/tools v0.0.0-20200206050830-dd0d5d485177/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200323144430-8dcfad9e016e h1:ssd5ulOvVWlh4kDSUF2SqzmMeWfjmwDXM+uGw/aQjRE= +golang.org/x/tools v0.0.0-20200323144430-8dcfad9e016e/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200806022845-90696ccdc692/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200814230902-9882f1d1823d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200817023811-d00afeaade8f/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200818005847-188abfa75333 h1:a6ryybeZHQf5qnBc6IwRfVnI/75UmdtJo71f0//8Dqo= +golang.org/x/tools v0.0.0-20200818005847-188abfa75333/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200815001618-f69a88009b70/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200911024640-645f7a48b24f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201030142918-24207fddd1c3 h1:sg8vLDNIxFPHTchfhH1E3AI32BL3f23oie38xUWnJM8= +google.golang.org/genproto v0.0.0-20201030142918-24207fddd1c3/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1 h1:DGeFlSan2f+WEtCERJ4J9GJWk15TxUi8QGagfI87Xyc= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/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= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +modernc.org/b v1.0.0 h1:vpvqeyp17ddcQWF29Czawql4lDdABCDRbXRAS4+aF2o= +modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg= +modernc.org/db v1.0.0 h1:2c6NdCfaLnshSvY7OU09cyAY0gYXUZj4lmg5ItHyucg= +modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8= +modernc.org/file v1.0.0 h1:9/PdvjVxd5+LcWUQIfapAWRGOkDLK90rloa8s/au06A= +modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw= +modernc.org/fileutil v1.0.0 h1:Z1AFLZwl6BO8A5NldQg/xTSjGLetp+1Ubvl4alfGx8w= +modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= +modernc.org/golex v1.0.0 h1:wWpDlbK8ejRfSyi0frMyhilD3JBvtcx2AdGDnU+JtsE= +modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/internal v1.0.0 h1:XMDsFDcBDsibbBnHB2xzljZ+B1yrOVLEFkKL2u15Glw= +modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM= +modernc.org/lldb v1.0.0 h1:6vjDJxQEfhlOLwl4bhpwIz00uyFK4EmSYcbwqwbynsc= +modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8= +modernc.org/mathutil v1.0.0 h1:93vKjrJopTPrtTNpZ8XIovER7iCIH1QU7wNbOQXC60I= +modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= +modernc.org/ql v1.0.0 h1:bIQ/trWNVjQPlinI6jdOQsi195SIturGo3mp5hsDqVU= +modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY= +modernc.org/sortutil v1.1.0 h1:oP3U4uM+NT/qBQcbg/K2iqAX0Nx7B1b6YZtq3Gk/PjM= +modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k= +modernc.org/strutil v1.1.0 h1:+1/yCzZxY2pZwwrsbH+4T7BQMoLQ9QiBshRC9eicYsc= +modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= +modernc.org/zappy v1.0.0 h1:dPVaP+3ueIUv4guk8PuZ2wiUGcJ1WUVvIheeSSTD0yk= +modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/i18n/en.json b/i18n/en.json index cf3c2c38d4..e7ba44fa22 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1558,6 +1558,26 @@ "id": "api.create_terms_of_service.empty_text.app_error", "translation": "Please enter text for your Custom Terms of Service." }, + { + "id": "api.custom_groups.count_err", + "translation": "error counting groups" + }, + { + "id": "api.custom_groups.feature_disabled", + "translation": "custom groups feature is disabled" + }, + { + "id": "api.custom_groups.license_error", + "translation": "not licensed for custom groups" + }, + { + "id": "api.custom_groups.must_be_referenceable", + "translation": "allow_reference must be 'true' for custom groups" + }, + { + "id": "api.custom_groups.no_remote_id", + "translation": "remote_id must be blank for custom group" + }, { "id": "api.custom_status.disabled", "translation": "Custom status feature has been disabled. Please contact your system administrator for details." @@ -1981,6 +2001,10 @@ "id": "api.license.upgrade_needed.app_error", "translation": "Feature requires an upgrade to Enterprise Edition." }, + { + "id": "api.license_error", + "translation": "api endpoint requires a license" + }, { "id": "api.marshal_error", "translation": "Failed to marshal." @@ -4775,6 +4799,10 @@ "id": "app.create_basic_user.save_member.max_accounts.app_error", "translation": "Unable to create default team membership because no more members are allowed in that team" }, + { + "id": "app.custom_group.unique_name", + "translation": "group name is not unique" + }, { "id": "app.email.no_rate_limiter.app_error", "translation": "Rate limiter is not set up." @@ -4875,6 +4903,10 @@ "id": "app.file_info.save.app_error", "translation": "Unable to save the file info." }, + { + "id": "app.group.crud_permission", + "translation": "Unable to perform operation for that source type." + }, { "id": "app.group.group_syncable_already_deleted", "translation": "group syncable was already deleted" @@ -5515,6 +5547,10 @@ "id": "app.license.generate_renewal_token.no_license", "translation": "No license present" }, + { + "id": "app.member_count", + "translation": "error retrieving member count" + }, { "id": "app.notification.body.dm.subTitle", "translation": "While you were away, {{.SenderName}} sent you a new Direct Message." diff --git a/model/client4.go b/model/client4.go index 80ab262fbc..beabda82c9 100644 --- a/model/client4.go +++ b/model/client4.go @@ -5253,7 +5253,7 @@ func (c *Client4) GetGroupsAssociatedToChannelsByTeam(teamId string, opts GroupS // GetGroups retrieves Mattermost Groups func (c *Client4) GetGroups(opts GroupSearchOpts) ([]*Group, *Response, error) { path := fmt.Sprintf( - "%s?include_member_count=%v¬_associated_to_team=%v¬_associated_to_channel=%v&filter_allow_reference=%v&q=%v&filter_parent_team_permitted=%v", + "%s?include_member_count=%v¬_associated_to_team=%v¬_associated_to_channel=%v&filter_allow_reference=%v&q=%v&filter_parent_team_permitted=%v&group_source=%v", c.groupsRoute(), opts.IncludeMemberCount, opts.NotAssociatedToTeam, @@ -5261,6 +5261,7 @@ func (c *Client4) GetGroups(opts GroupSearchOpts) ([]*Group, *Response, error) { opts.FilterAllowReference, opts.Q, opts.FilterParentTeamPermitted, + opts.Source, ) if opts.Since > 0 { path = fmt.Sprintf("%s&since=%v", path, opts.Since) @@ -7072,6 +7073,36 @@ func (c *Client4) GetGroup(groupID, etag string) (*Group, *Response, error) { return &g, BuildResponse(r), nil } +func (c *Client4) CreateGroup(group *Group) (*Group, *Response, error) { + groupJSON, jsonErr := json.Marshal(group) + if jsonErr != nil { + return nil, nil, NewAppError("CreateGroup", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes("/groups", groupJSON) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var p Group + if jsonErr := json.NewDecoder(r.Body).Decode(&p); jsonErr != nil { + return nil, nil, NewAppError("CreateGroup", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &p, BuildResponse(r), nil +} + +func (c *Client4) DeleteGroup(groupID string) (*Group, *Response, error) { + r, err := c.DoAPIDelete(c.groupRoute(groupID)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var p Group + if jsonErr := json.NewDecoder(r.Body).Decode(&p); jsonErr != nil { + return nil, nil, NewAppError("DeleteGroup", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &p, BuildResponse(r), nil +} + func (c *Client4) PatchGroup(groupID string, patch *GroupPatch) (*Group, *Response, error) { payload, _ := json.Marshal(patch) r, err := c.DoAPIPut(c.groupRoute(groupID)+"/patch", string(payload)) @@ -7086,6 +7117,40 @@ func (c *Client4) PatchGroup(groupID string, patch *GroupPatch) (*Group, *Respon return &g, BuildResponse(r), nil } +func (c *Client4) UpsertGroupMembers(groupID string, userIds *GroupModifyMembers) ([]*GroupMember, *Response, error) { + payload, jsonErr := json.Marshal(userIds) + if jsonErr != nil { + return nil, nil, NewAppError("UpsertGroupMembers", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.groupRoute(groupID)+"/members", payload) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var g []*GroupMember + if jsonErr := json.NewDecoder(r.Body).Decode(&g); jsonErr != nil { + return nil, nil, NewAppError("UpsertGroupMembers", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return g, BuildResponse(r), nil +} + +func (c *Client4) DeleteGroupMembers(groupID string, userIds *GroupModifyMembers) ([]*GroupMember, *Response, error) { + payload, jsonErr := json.Marshal(userIds) + if jsonErr != nil { + return nil, nil, NewAppError("DeleteGroupMembers", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIDeleteBytes(c.groupRoute(groupID)+"/members", payload) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var g []*GroupMember + if jsonErr := json.NewDecoder(r.Body).Decode(&g); jsonErr != nil { + return nil, nil, NewAppError("DeleteGroupMembers", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return g, BuildResponse(r), nil +} + func (c *Client4) LinkGroupSyncable(groupID, syncableID string, syncableType GroupSyncableType, patch *GroupSyncablePatch) (*GroupSyncable, *Response, error) { payload, _ := json.Marshal(patch) url := fmt.Sprintf("%s/link", c.groupSyncableRoute(groupID, syncableID, syncableType)) diff --git a/model/config.go b/model/config.go index b41e202c59..f52e8f6c13 100644 --- a/model/config.go +++ b/model/config.go @@ -367,6 +367,7 @@ type ServiceSettings struct { ThreadAutoFollow *bool `access:"experimental_features"` CollapsedThreads *string `access:"experimental_features"` ManagedResourcePaths *string `access:"environment_web_server,write_restrictable,cloud_restrictable"` + EnableCustomGroups *bool `access:"site_users_and_teams"` } func (s *ServiceSettings) SetDefaults(isUpdate bool) { @@ -787,6 +788,10 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) { if s.ManagedResourcePaths == nil { s.ManagedResourcePaths = NewString("") } + + if s.EnableCustomGroups == nil { + s.EnableCustomGroups = NewBool(true) + } } type ClusterSettings struct { diff --git a/model/feature_flags.go b/model/feature_flags.go index fa462462e8..211d73b6bd 100644 --- a/model/feature_flags.go +++ b/model/feature_flags.go @@ -59,6 +59,11 @@ type FeatureFlags struct { // Determine after which duration in hours to send a second invitation to someone that didn't join after the initial invite, possible values = ("48", "72") ResendInviteEmailInterval string + // A/B test for whether radio buttons or toggle button is more effective in in-screen invite to team modal ("none", "toggle") + InviteToTeam string + + CustomGroups bool + // Enable inline post editing InlinePostEditing bool @@ -95,6 +100,8 @@ func (f *FeatureFlags) SetDefaults() { f.AddMembersToChannel = "top" f.GuidedChannelCreation = false f.ResendInviteEmailInterval = "" + f.InviteToTeam = "none" + f.CustomGroups = true f.InlinePostEditing = false f.BoardsDataRetention = false f.NormalizeLdapDNs = false @@ -102,7 +109,6 @@ func (f *FeatureFlags) SetDefaults() { f.WorkspaceOptimizationDashboard = false f.GraphQL = false } - func (f *FeatureFlags) Plugins() map[string]string { rFFVal := reflect.ValueOf(f).Elem() rFFType := reflect.TypeOf(f).Elem() diff --git a/model/group.go b/model/group.go index 566b23611a..1e56dc283a 100644 --- a/model/group.go +++ b/model/group.go @@ -9,7 +9,8 @@ import ( ) const ( - GroupSourceLdap GroupSource = "ldap" + GroupSourceLdap GroupSource = "ldap" + GroupSourceCustom GroupSource = "custom" GroupNameMaxLength = 64 GroupSourceMaxLength = 64 @@ -22,6 +23,7 @@ type GroupSource string var allGroupSources = []GroupSource{ GroupSourceLdap, + GroupSourceCustom, } var groupSourcesRequiringRemoteID = []GroupSource{ @@ -34,7 +36,7 @@ type Group struct { DisplayName string `json:"display_name"` Description string `json:"description"` Source GroupSource `json:"source"` - RemoteId string `json:"remote_id"` + RemoteId *string `json:"remote_id"` CreateAt int64 `json:"create_at"` UpdateAt int64 `json:"update_at"` DeleteAt int64 `json:"delete_at"` @@ -43,6 +45,11 @@ type Group struct { AllowReference bool `json:"allow_reference"` } +type GroupWithUserIds struct { + Group + UserIds []string `json:"user_ids"` +} + type GroupWithSchemeAdmin struct { Group SchemeAdmin *bool `db:"SyncableSchemeAdmin" json:"scheme_admin,omitempty"` @@ -63,6 +70,8 @@ type GroupPatch struct { DisplayName *string `json:"display_name"` Description *string `json:"description"` AllowReference *bool `json:"allow_reference"` + // For security reasons (including preventing unintended LDAP group synchronization) do no allow a Group's RemoteId or Source field to be + // included in patches. } type LdapGroupSearchOpts struct { @@ -79,12 +88,21 @@ type GroupSearchOpts struct { FilterAllowReference bool PageOpts *PageOpts Since int64 + Source GroupSource // FilterParentTeamPermitted filters the groups to the intersect of the // set associated to the parent team and those returned by the query. // If the parent team is not group-constrained or if NotAssociatedToChannel // is not set then this option is ignored. FilterParentTeamPermitted bool + + // FilterHasMember filters the groups to the intersect of the + // set returned by the query and those that have the given user as a member. + FilterHasMember string +} + +type GetGroupOpts struct { + IncludeMemberCount bool } type PageOpts struct { @@ -97,6 +115,10 @@ type GroupStats struct { TotalMemberCount int64 `json:"total_member_count"` } +type GroupModifyMembers struct { + UserIds []string `json:"user_ids"` +} + func (group *Group) Patch(patch *GroupPatch) { if patch.Name != nil { group.Name = patch.Name @@ -137,7 +159,7 @@ func (group *Group) IsValidForCreate() *AppError { return NewAppError("Group.IsValidForCreate", "model.group.source.app_error", nil, "", http.StatusBadRequest) } - if len(group.RemoteId) > GroupRemoteIDMaxLength || (group.RemoteId == "" && group.requiresRemoteId()) { + if (group.GetRemoteId() == "" && group.requiresRemoteId()) || len(group.GetRemoteId()) > GroupRemoteIDMaxLength { return NewAppError("Group.IsValidForCreate", "model.group.remote_id.app_error", nil, "", http.StatusBadRequest) } @@ -188,3 +210,15 @@ func (group *Group) IsValidName() *AppError { } return nil } + +func (group *Group) GetRemoteId() string { + if group.RemoteId == nil { + return "" + } + return *group.RemoteId +} + +type GroupsWithCount struct { + Groups []*Group `json:"groups"` + TotalCount int64 `json:"total_count"` +} diff --git a/model/license.go b/model/license.go index dbdc296e4f..c8a953053f 100644 --- a/model/license.go +++ b/model/license.go @@ -323,6 +323,12 @@ func NewTestLicense(features ...string) *License { return ret } +func NewTestLicenseSKU(skuShortName string, features ...string) *License { + lic := NewTestLicense(features...) + lic.SkuShortName = skuShortName + return lic +} + func (lr *LicenseRecord) IsValid() *AppError { if !IsValidId(lr.Id) { return NewAppError("LicenseRecord.IsValid", "model.license_record.is_valid.id.app_error", nil, "", http.StatusBadRequest) diff --git a/model/migration.go b/model/migration.go index 629189bbd7..16172e5081 100644 --- a/model/migration.go +++ b/model/migration.go @@ -36,4 +36,5 @@ const ( MigrationKeyAddAboutSubsectionPermissions = "about_subsection_permissions" MigrationKeyAddIntegrationsSubsectionPermissions = "integrations_subsection_permissions" MigrationKeyAddPlaybooksPermissions = "playbooks_permissions" + MigrationKeyAddCustomUserGroupsPermissions = "custom_groups_permissions" ) diff --git a/model/permission.go b/model/permission.go index 9a3e4aae17..e8d9d2c357 100644 --- a/model/permission.go +++ b/model/permission.go @@ -7,6 +7,7 @@ const ( PermissionScopeSystem = "system_scope" PermissionScopeTeam = "team_scope" PermissionScopeChannel = "channel_scope" + PermissionScopeGroup = "group_scope" PermissionScopePlaybook = "playbook_scope" PermissionScopeRun = "run_scope" ) @@ -355,6 +356,11 @@ var PermissionRunView *Permission // admin functions but not others var PermissionManageSystem *Permission +var PermissionCreateCustomGroup *Permission +var PermissionManageCustomGroupMembers *Permission +var PermissionEditCustomGroup *Permission +var PermissionDeleteCustomGroup *Permission + var AllPermissions []*Permission var DeprecatedPermissions []*Permission @@ -1914,6 +1920,34 @@ func initializePermissions() { PermissionScopeSystem, } + PermissionCreateCustomGroup = &Permission{ + "create_custom_group", + "authentication.permissions.create_custom_group.name", + "authentication.permissions.create_custom_group.description", + PermissionScopeSystem, + } + + PermissionManageCustomGroupMembers = &Permission{ + "manage_custom_group_members", + "authentication.permissions.manage_custom_group_members.name", + "authentication.permissions.manage_custom_group_members.description", + PermissionScopeGroup, + } + + PermissionEditCustomGroup = &Permission{ + "edit_custom_group", + "authentication.permissions.edit_custom_group.name", + "authentication.permissions.edit_custom_group.description", + PermissionScopeGroup, + } + + PermissionDeleteCustomGroup = &Permission{ + "delete_custom_group", + "authentication.permissions.delete_custom_group.name", + "authentication.permissions.delete_custom_group.description", + PermissionScopeGroup, + } + // Playbooks PermissionPublicPlaybookCreate = &Permission{ "playbook_public_create", @@ -2200,6 +2234,7 @@ func initializePermissions() { PermissionGetLogs, PermissionReadLicenseInformation, PermissionManageLicenseInformation, + PermissionCreateCustomGroup, } TeamScopedPermissions := []*Permission{ @@ -2259,6 +2294,12 @@ func initializePermissions() { PermissionUseGroupMentions, } + GroupScopedPermissions := []*Permission{ + PermissionManageCustomGroupMembers, + PermissionEditCustomGroup, + PermissionDeleteCustomGroup, + } + DeprecatedPermissions = []*Permission{ PermissionPermanentDeleteUser, PermissionManageWebhooks, @@ -2307,6 +2348,7 @@ func initializePermissions() { AllPermissions = append(AllPermissions, ChannelScopedPermissions...) AllPermissions = append(AllPermissions, SysconsoleReadPermissions...) AllPermissions = append(AllPermissions, SysconsoleWritePermissions...) + AllPermissions = append(AllPermissions, GroupScopedPermissions...) AllPermissions = append(AllPermissions, PlaybookScopedPermissions...) AllPermissions = append(AllPermissions, RunScopedPermissions...) diff --git a/model/role.go b/model/role.go index 0dcb405c6c..37edec4fb9 100644 --- a/model/role.go +++ b/model/role.go @@ -4,6 +4,7 @@ package model import ( + "fmt" "strings" ) @@ -42,6 +43,8 @@ func init() { ChannelUserRoleId, ChannelAdminRoleId, + CustomGroupUserRoleId, + PlaybookAdminRoleId, PlaybookMemberRoleId, RunAdminRoleId, @@ -367,6 +370,8 @@ const ( ChannelUserRoleId = "channel_user" ChannelAdminRoleId = "channel_admin" + CustomGroupUserRoleId = "custom_group_user" + PlaybookAdminRoleId = "playbook_admin" PlaybookMemberRoleId = "playbook_member" RunAdminRoleId = "run_admin" @@ -379,6 +384,7 @@ const ( RoleScopeSystem RoleScope = "System" RoleScopeTeam RoleScope = "Team" RoleScopeChannel RoleScope = "Channel" + RoleScopeGroup RoleScope = "Group" RoleTypeGuest RoleType = "Guest" RoleTypeUser RoleType = "User" @@ -683,6 +689,13 @@ func IsValidRoleName(roleName string) bool { func MakeDefaultRoles() map[string]*Role { roles := make(map[string]*Role) + roles[CustomGroupUserRoleId] = &Role{ + Name: CustomGroupUserRoleId, + DisplayName: fmt.Sprintf("authentication.roles.%s.name", CustomGroupUserRoleId), + Description: fmt.Sprintf("authentication.roles.%s.description", CustomGroupUserRoleId), + Permissions: []string{}, + } + roles[ChannelGuestRoleId] = &Role{ Name: "channel_guest", DisplayName: "authentication.roles.channel_guest.name", @@ -895,6 +908,10 @@ func MakeDefaultRoles() map[string]*Role { PermissionCreateGroupChannel.Id, PermissionViewMembers.Id, PermissionCreateTeam.Id, + PermissionCreateCustomGroup.Id, + PermissionEditCustomGroup.Id, + PermissionDeleteCustomGroup.Id, + PermissionManageCustomGroupMembers.Id, }, SchemeManaged: true, BuiltIn: true, diff --git a/model/user_get.go b/model/user_get.go index 2748d73513..0ba62f3f06 100644 --- a/model/user_get.go +++ b/model/user_get.go @@ -14,6 +14,8 @@ type UserGetOptions struct { NotInChannelId string // Filters the users in the group InGroupId string + // Filters the users not in the group + NotInGroupId string // Filters the users group constrained GroupConstrained bool // Filters the users without a team diff --git a/model/user_search.go b/model/user_search.go index 93bf600918..d0480fe580 100644 --- a/model/user_search.go +++ b/model/user_search.go @@ -22,6 +22,7 @@ type UserSearch struct { Roles []string `json:"roles"` ChannelRoles []string `json:"channel_roles"` TeamRoles []string `json:"team_roles"` + NotInGroupId string `json:"not_in_group_id"` } // UserSearchOptions captures internal parameters derived from the user's permissions and a diff --git a/model/websocket_message.go b/model/websocket_message.go index 8827a0017b..53d8b4c464 100644 --- a/model/websocket_message.go +++ b/model/websocket_message.go @@ -62,6 +62,8 @@ const ( WebsocketEventReceivedGroupNotAssociatedToTeam = "received_group_not_associated_to_team" WebsocketEventReceivedGroupAssociatedToChannel = "received_group_associated_to_channel" WebsocketEventReceivedGroupNotAssociatedToChannel = "received_group_not_associated_to_channel" + WebsocketEventGroupMemberDelete = "group_member_deleted" + WebsocketEventGroupMemberAdd = "group_member_add" WebsocketEventSidebarCategoryCreated = "sidebar_category_created" WebsocketEventSidebarCategoryUpdated = "sidebar_category_updated" WebsocketEventSidebarCategoryDeleted = "sidebar_category_deleted" diff --git a/plugin/environment_test.go b/plugin/environment_test.go index aa1789da50..fe15fd345e 100644 --- a/plugin/environment_test.go +++ b/plugin/environment_test.go @@ -28,7 +28,6 @@ func TestAvaliablePlugins(t *testing.T) { t.Run("Should be able to load available plugins", func(t *testing.T) { bundle1 := model.BundleInfo{ - ManifestPath: "", Manifest: &model.Manifest{ Id: "someid", Version: "1", diff --git a/services/telemetry/telemetry.go b/services/telemetry/telemetry.go index 3d44da602a..d2090d4c54 100644 --- a/services/telemetry/telemetry.go +++ b/services/telemetry/telemetry.go @@ -436,6 +436,7 @@ func (ts *TelemetryService) trackConfig() { "enable_permalink_previews": *cfg.ServiceSettings.EnablePermalinkPreviews, "enable_file_search": *cfg.ServiceSettings.EnableFileSearch, "restrict_link_previews": isDefault(*cfg.ServiceSettings.RestrictLinkPreviews, ""), + "enable_custom_groups": *cfg.ServiceSettings.EnableCustomGroups, }) ts.SendTelemetry(TrackConfigTeam, map[string]interface{}{ diff --git a/services/upgrader/upgrader.go b/services/upgrader/upgrader.go index 8218998c60..7411b31d94 100644 --- a/services/upgrader/upgrader.go +++ b/services/upgrader/upgrader.go @@ -1,5 +1,6 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +//go:build !linux // +build !linux package upgrader diff --git a/store/errors.go b/store/errors.go index ee9e896713..158ea0ceb0 100644 --- a/store/errors.go +++ b/store/errors.go @@ -5,6 +5,7 @@ package store import ( "fmt" + "strings" ) // ErrInvalidInput indicates an error that has occurred due to an invalid input. @@ -132,3 +133,29 @@ func (e *ErrNotImplemented) Error() string { func NewErrNotImplemented(detail string) *ErrNotImplemented { return &ErrNotImplemented{detail: detail} } + +type ErrUniqueConstraint struct { + Columns []string +} + +// NewErrUniqueConstraint creates a uniqueness constraint error for the given column(s). +// +// Examples: +// +// store.NewErrUniqueConstraint("DisplayName") // single column constraint +// store.NewErrUniqueConstraint("Name", "Source") // multi-column constaint +func NewErrUniqueConstraint(columns ...string) *ErrUniqueConstraint { + return &ErrUniqueConstraint{ + Columns: columns, + } +} + +func (e *ErrUniqueConstraint) Error() string { + var tmpl string + if len(e.Columns) > 1 { + tmpl = "unique constraint: (%s)" + } else { + tmpl = "unique constraint: %s" + } + return fmt.Sprintf(tmpl, strings.Join(e.Columns, ",")) +} diff --git a/store/opentracinglayer/opentracinglayer.go b/store/opentracinglayer/opentracinglayer.go index 608c7d8aa2..fdda9aade5 100644 --- a/store/opentracinglayer/opentracinglayer.go +++ b/store/opentracinglayer/opentracinglayer.go @@ -3709,6 +3709,24 @@ func (s *OpenTracingLayerGroupStore) CreateGroupSyncable(groupSyncable *model.Gr return result, err } +func (s *OpenTracingLayerGroupStore) CreateWithUserIds(group *model.GroupWithUserIds) (*model.Group, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.CreateWithUserIds") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.GroupStore.CreateWithUserIds(group) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerGroupStore) Delete(groupID string) (*model.Group, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.Delete") @@ -3763,6 +3781,24 @@ func (s *OpenTracingLayerGroupStore) DeleteMember(groupID string, userID string) return result, err } +func (s *OpenTracingLayerGroupStore) DeleteMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.DeleteMembers") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.GroupStore.DeleteMembers(groupID, userIDs) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerGroupStore) DistinctGroupMemberCount() (int64, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.DistinctGroupMemberCount") @@ -3997,6 +4033,24 @@ func (s *OpenTracingLayerGroupStore) GetGroupsByTeam(teamID string, opts model.G return result, err } +func (s *OpenTracingLayerGroupStore) GetMember(groupID string, userID string) (*model.GroupMember, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetMember") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.GroupStore.GetMember(groupID, userID) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerGroupStore) GetMemberCount(groupID string) (int64, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetMemberCount") @@ -4087,6 +4141,24 @@ func (s *OpenTracingLayerGroupStore) GetMemberUsersPage(groupID string, page int return result, err } +func (s *OpenTracingLayerGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int) ([]*model.User, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetNonMemberUsersPage") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.GroupStore.GetNonMemberUsersPage(groupID, page, perPage) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerGroupStore) GroupChannelCount() (int64, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GroupChannelCount") @@ -4321,6 +4393,24 @@ func (s *OpenTracingLayerGroupStore) UpsertMember(groupID string, userID string) return result, err } +func (s *OpenTracingLayerGroupStore) UpsertMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.UpsertMembers") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.GroupStore.UpsertMembers(groupID, userIDs) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerJobStore) Cleanup(expiryTime int64, batchSize int) error { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.Cleanup") @@ -10793,6 +10883,24 @@ func (s *OpenTracingLayerUserStore) SearchNotInChannel(teamID string, channelID return result, err } +func (s *OpenTracingLayerUserStore) SearchNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.SearchNotInGroup") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.UserStore.SearchNotInGroup(groupID, term, options) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerUserStore) SearchNotInTeam(notInTeamID string, term string, options *model.UserSearchOptions) ([]*model.User, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.SearchNotInTeam") diff --git a/store/retrylayer/retrylayer.go b/store/retrylayer/retrylayer.go index fcc504abcc..3a7e1ea454 100644 --- a/store/retrylayer/retrylayer.go +++ b/store/retrylayer/retrylayer.go @@ -4162,6 +4162,27 @@ func (s *RetryLayerGroupStore) CreateGroupSyncable(groupSyncable *model.GroupSyn } +func (s *RetryLayerGroupStore) CreateWithUserIds(group *model.GroupWithUserIds) (*model.Group, error) { + + tries := 0 + for { + result, err := s.GroupStore.CreateWithUserIds(group) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerGroupStore) Delete(groupID string) (*model.Group, error) { tries := 0 @@ -4225,6 +4246,27 @@ func (s *RetryLayerGroupStore) DeleteMember(groupID string, userID string) (*mod } +func (s *RetryLayerGroupStore) DeleteMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) { + + tries := 0 + for { + result, err := s.GroupStore.DeleteMembers(groupID, userIDs) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerGroupStore) DistinctGroupMemberCount() (int64, error) { tries := 0 @@ -4498,6 +4540,27 @@ func (s *RetryLayerGroupStore) GetGroupsByTeam(teamID string, opts model.GroupSe } +func (s *RetryLayerGroupStore) GetMember(groupID string, userID string) (*model.GroupMember, error) { + + tries := 0 + for { + result, err := s.GroupStore.GetMember(groupID, userID) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerGroupStore) GetMemberCount(groupID string) (int64, error) { tries := 0 @@ -4603,6 +4666,27 @@ func (s *RetryLayerGroupStore) GetMemberUsersPage(groupID string, page int, perP } +func (s *RetryLayerGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int) ([]*model.User, error) { + + tries := 0 + for { + result, err := s.GroupStore.GetNonMemberUsersPage(groupID, page, perPage) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerGroupStore) GroupChannelCount() (int64, error) { tries := 0 @@ -4876,6 +4960,27 @@ func (s *RetryLayerGroupStore) UpsertMember(groupID string, userID string) (*mod } +func (s *RetryLayerGroupStore) UpsertMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) { + + tries := 0 + for { + result, err := s.GroupStore.UpsertMembers(groupID, userIDs) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerJobStore) Cleanup(expiryTime int64, batchSize int) error { tries := 0 @@ -12289,6 +12394,27 @@ func (s *RetryLayerUserStore) SearchNotInChannel(teamID string, channelID string } +func (s *RetryLayerUserStore) SearchNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) { + + tries := 0 + for { + result, err := s.UserStore.SearchNotInGroup(groupID, term, options) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerUserStore) SearchNotInTeam(notInTeamID string, term string, options *model.UserSearchOptions) ([]*model.User, error) { tries := 0 diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index ec1d1dbd5f..20b45f2e6e 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -2173,7 +2173,7 @@ func (s SqlChannelStore) GetMemberCountsByGroup(ctx context.Context, channelID s query := s.getQueryBuilder(). Select(selectStr). From("ChannelMembers"). - Join("GroupMembers ON GroupMembers.UserId = ChannelMembers.UserId") + Join("GroupMembers ON GroupMembers.UserId = ChannelMembers.UserId AND GroupMembers.DeleteAt = 0") if includeTimezones { query = query.Join("Users ON Users.Id = GroupMembers.UserId") diff --git a/store/sqlstore/group_store.go b/store/sqlstore/group_store.go index d5aab1e6b4..ff09a600bd 100644 --- a/store/sqlstore/group_store.go +++ b/store/sqlstore/group_store.go @@ -81,6 +81,137 @@ func (s *SqlGroupStore) Create(group *model.Group) (*model.Group, error) { return group, nil } +func (s *SqlGroupStore) CreateWithUserIds(g *model.GroupWithUserIds) (*model.Group, error) { + if g.Id != "" { + return nil, store.NewErrInvalidInput("Group", "id", g.Id) + } + + // Check if group values are formatted correctly + if err := g.IsValidForCreate(); err != nil { + return nil, err + } + + // Check Users exist + if err := s.checkUsersExist(g.UserIds); err != nil { + return nil, err + } + + g.Id = model.NewId() + g.CreateAt = model.GetMillis() + g.UpdateAt = g.CreateAt + + groupInsertQuery, groupInsertArgs, err := s.getQueryBuilder(). + Insert("UserGroups"). + Columns("Id", "Name", "DisplayName", "Description", "Source", "RemoteId", "CreateAt", "UpdateAt", "DeleteAt", "AllowReference"). + Values(g.Id, g.Name, g.DisplayName, g.Description, g.Source, g.RemoteId, g.CreateAt, g.UpdateAt, 0, g.AllowReference). + ToSql() + if err != nil { + return nil, err + } + + usersInsertQuery, usersInsertArgs, err := s.buildInsertGroupUsersQuery(g.Id, g.UserIds) + if err != nil { + return nil, err + } + + txn, err := s.GetMasterX().Beginx() + if err != nil { + return nil, err + } + defer finalizeTransactionX(txn) + // Create a new usergroup + if _, err = txn.Exec(groupInsertQuery, groupInsertArgs...); err != nil { + if IsUniqueConstraintError(err, []string{"Name", "groups_name_key"}) { + return nil, store.NewErrUniqueConstraint("Name") + } + return nil, errors.Wrap(err, "failed to save Group") + } + // Insert the Group Members + if _, err = executePossiblyEmptyQuery(txn, usersInsertQuery, usersInsertArgs...); err != nil { + return nil, err + } + + // Get the new Group along with the member count + groupGroupQuery := ` + SELECT + UserGroups.*, + A.Count AS MemberCount + FROM + UserGroups + INNER JOIN ( + SELECT + UserGroups.Id, + COUNT(GroupMembers.UserId) AS Count + FROM + UserGroups + LEFT JOIN GroupMembers ON UserGroups.Id = GroupMembers.GroupId + WHERE + UserGroups.Id = ? + GROUP BY + UserGroups.Id + ORDER BY + UserGroups.DisplayName, + UserGroups.Id + LIMIT + ? OFFSET ? + ) AS A ON UserGroups.Id = A.Id + ORDER BY + UserGroups.CreateAt DESC` + var newGroup group + if err = txn.Get(&newGroup, groupGroupQuery, g.Id, 1, 0); err != nil { + return nil, err + } + if err = txn.Commit(); err != nil { + return nil, err + } + return newGroup.ToModel(), nil +} + +func (s *SqlGroupStore) checkUsersExist(userIDs []string) error { + if len(userIDs) == 0 { + return nil + } + usersSelectQuery, usersSelectArgs, err := s.getQueryBuilder(). + Select("Id"). + From("Users"). + Where(sq.Eq{"Id": userIDs, "DeleteAt": 0}). + ToSql() + if err != nil { + return err + } + var rows []string + err = s.GetReplicaX().Select(&rows, usersSelectQuery, usersSelectArgs...) + if err != nil { + return err + } + if len(rows) == len(userIDs) { + return nil + } + retrievedIDs := make(map[string]bool) + for _, userID := range rows { + retrievedIDs[userID] = true + } + for _, userID := range userIDs { + if _, ok := retrievedIDs[userID]; !ok { + return store.NewErrNotFound("User", userID) + } + } + return nil +} + +func (s *SqlGroupStore) buildInsertGroupUsersQuery(groupId string, userIds []string) (query string, args []interface{}, err error) { + if len(userIds) > 0 { + builder := s.getQueryBuilder(). + Insert("GroupMembers"). + Columns("GroupId", "UserId", "CreateAt", "DeleteAt") + for _, userId := range userIds { + builder = builder.Values(groupId, userId, model.GetMillis(), 0) + } + query, args, err = builder.ToSql() + } + return +} + func (s *SqlGroupStore) Get(groupId string) (*model.Group, error) { var group model.Group if err := s.GetReplicaX().Get(&group, "SELECT * from UserGroups WHERE Id = ?", groupId); err != nil { @@ -228,6 +359,26 @@ func (s *SqlGroupStore) Delete(groupID string) (*model.Group, error) { return &group, nil } +func (s *SqlGroupStore) GetMember(groupID, userID string) (*model.GroupMember, error) { + query, args, err := s.getQueryBuilder(). + Select("*"). + From("GroupMembers"). + Where(sq.Eq{"UserId": userID}). + Where(sq.Eq{"GroupId": groupID}). + Where(sq.Eq{"DeleteAt": 0}). + ToSql() + if err != nil { + return nil, errors.Wrap(err, "get_member_query") + } + var groupMember model.GroupMember + err = s.GetReplicaX().Get(&groupMember, query, args...) + if err != nil { + return nil, errors.Wrap(err, "GetMember") + } + + return &groupMember, nil +} + func (s *SqlGroupStore) GetMemberUsers(groupID string) ([]*model.User, error) { groupMembers := []*model.User{} @@ -276,6 +427,37 @@ func (s *SqlGroupStore) GetMemberUsersPage(groupID string, page int, perPage int return groupMembers, nil } +func (s *SqlGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int) ([]*model.User, error) { + groupMembers := []*model.User{} + + if err := s.GetReplicaX().Get(&model.Group{}, "SELECT * FROM UserGroups WHERE Id = ?", groupID); err != nil { + return nil, errors.Wrap(err, "GetNonMemberUsersPage") + } + + query := ` + SELECT + Users.* + FROM + Users + LEFT JOIN + GroupMembers ON (GroupMembers.UserId = Users.Id AND GroupMembers.GroupId = ?) + WHERE + Users.DeleteAt = 0 + AND ( GroupMembers.UserId IS NULL OR GroupMembers.DeleteAt != 0) + ORDER BY + GroupMembers.CreateAt DESC + LIMIT + ? + OFFSET + ?` + + if err := s.GetReplicaX().Select(&groupMembers, query, groupID, perPage, page*perPage); err != nil { + return nil, errors.Wrapf(err, "failed to find member Users for Group with id=%s", groupID) + } + + return groupMembers, nil +} + func (s *SqlGroupStore) GetMemberCount(groupID string) (int64, error) { query := ` SELECT @@ -285,7 +467,8 @@ func (s *SqlGroupStore) GetMemberCount(groupID string) (int64, error) { JOIN Users ON Users.Id = GroupMembers.UserId WHERE GroupMembers.GroupId = ? - AND Users.DeleteAt = 0` + AND Users.DeleteAt = 0 + AND GroupMembers.DeleteAt = 0` var count int64 err := s.GetReplicaX().Get(&count, query, groupID) @@ -361,64 +544,26 @@ func (s *SqlGroupStore) GetMemberUsersNotInChannel(groupID string, channelID str } func (s *SqlGroupStore) UpsertMember(groupID string, userID string) (*model.GroupMember, error) { - member := &model.GroupMember{ - GroupId: groupID, - UserId: userID, - CreateAt: model.GetMillis(), - DeleteAt: 0, - } - - if err := member.IsValid(); err != nil { + members, query, args, err := s.buildUpsertMembersQuery(groupID, []string{userID}) + if err != nil { return nil, err } - - var retrievedGroup model.Group - if err := s.GetReplicaX().Get(&retrievedGroup, "SELECT * FROM UserGroups WHERE Id = ?", groupID); err != nil { - return nil, errors.Wrapf(err, "failed to get UserGroup with groupId=%s and userId=%s", groupID, userID) - } - - query := s.getQueryBuilder(). - Insert("GroupMembers"). - Columns("GroupId", "UserId", "CreateAt", "DeleteAt"). - Values(member.GroupId, member.UserId, member.CreateAt, member.DeleteAt) - - if s.DriverName() == model.DatabaseDriverMysql { - query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE CreateAt = ?, DeleteAt = ?", member.CreateAt, member.DeleteAt)) - } else if s.DriverName() == model.DatabaseDriverPostgres { - query = query.SuffixExpr(sq.Expr("ON CONFLICT (groupid, userid) DO UPDATE SET CreateAt = ?, DeleteAt = ?", member.CreateAt, member.DeleteAt)) - } - - queryString, args, err := query.ToSql() - if err != nil { - return nil, errors.Wrap(err, "failed to generate sqlquery") - } - - if _, err = s.GetMasterX().Exec(queryString, args...); err != nil { + if _, err = s.GetMasterX().Exec(query, args...); err != nil { return nil, errors.Wrap(err, "failed to save GroupMember") } - return member, nil + return members[0], nil } func (s *SqlGroupStore) DeleteMember(groupID string, userID string) (*model.GroupMember, error) { - var retrievedMember model.GroupMember - if err := s.GetReplicaX().Get(&retrievedMember, "SELECT * FROM GroupMembers WHERE GroupId = ? AND UserId = ? AND DeleteAt = 0", groupID, userID); err != nil { - if err == sql.ErrNoRows { - return nil, store.NewErrNotFound("GroupMember", fmt.Sprintf("groupId=%s, userId=%s", groupID, userID)) - } - return nil, errors.Wrapf(err, "failed to get GroupMember with groupId=%s and userId=%s", groupID, userID) + members, query, args, err := s.buildDeleteMembersQuery(groupID, []string{userID}) + if err != nil { + return nil, err } - - retrievedMember.DeleteAt = model.GetMillis() - - if _, err := s.GetMasterX().NamedExec(`UPDATE GroupMembers - SET DeleteAt=:DeleteAt - WHERE GroupId=:GroupId - AND UserId=:UserId - AND DeleteAt=0`, retrievedMember); err != nil { + if _, err = s.GetMasterX().Exec(query, args...); err != nil { return nil, errors.Wrapf(err, "failed to update GroupMember with groupId=%s and userId=%s", groupID, userID) } - return &retrievedMember, nil + return members[0], nil } func (s *SqlGroupStore) PermanentDeleteMembersByUser(userId string) error { @@ -887,7 +1032,7 @@ type group struct { DisplayName string Description string Source model.GroupSource - RemoteId string + RemoteId *string CreateAt int64 UpdateAt int64 DeleteAt int64 @@ -1255,6 +1400,13 @@ func (s *SqlGroupStore) GetGroups(page, perPage int, opts model.GroupSearchOpts) LeftJoin("(SELECT GroupMembers.GroupId, COUNT(*) AS MemberCount FROM GroupMembers LEFT JOIN Users ON Users.Id = GroupMembers.UserId WHERE GroupMembers.DeleteAt = 0 AND Users.DeleteAt = 0 GROUP BY GroupId) AS Members ON Members.GroupId = g.Id") } + if opts.FilterHasMember != "" { + groupsQuery = groupsQuery. + LeftJoin("GroupMembers ON GroupMembers.GroupId = g.Id"). + Where("GroupMembers.UserId = ?", opts.FilterHasMember). + Where("GroupMembers.DeleteAt = 0") + } + groupsQuery = groupsQuery. From("UserGroups g"). OrderBy("g.DisplayName") @@ -1350,6 +1502,10 @@ func (s *SqlGroupStore) GetGroups(page, perPage int, opts model.GroupSearchOpts) `, opts.NotAssociatedToChannel, opts.NotAssociatedToChannel) } + if opts.Source != "" { + groupsQuery = groupsQuery.Where("g.Source = ?", opts.Source) + } + queryString, args, err := groupsQuery.ToSql() if err != nil { return nil, errors.Wrap(err, "get_groups_tosql") @@ -1608,3 +1764,117 @@ func (s *SqlGroupStore) countTableWithSelectAndWhere(selectStr, tableName string return count, nil } + +func (s *SqlGroupStore) UpsertMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) { + members, query, args, err := s.buildUpsertMembersQuery(groupID, userIDs) + if err != nil { + return nil, err + } + + if _, err = s.GetMasterX().Exec(query, args...); err != nil { + return nil, errors.Wrap(err, "failed to save GroupMember") + } + + return members, err +} + +func (s *SqlGroupStore) buildUpsertMembersQuery(groupID string, userIDs []string) (members []*model.GroupMember, query string, args []interface{}, err error) { + var retrievedGroup model.Group + // Check Group exists + if err = s.GetReplicaX().Get(&retrievedGroup, "SELECT * FROM UserGroups WHERE Id = ?", groupID); err != nil { + err = errors.Wrapf(err, "failed to get UserGroup with groupId=%s", groupID) + return + } + + // Check Users exist + if err = s.checkUsersExist(userIDs); err != nil { + return + } + + builder := s.getQueryBuilder(). + Insert("GroupMembers"). + Columns("GroupId", "UserId", "CreateAt", "DeleteAt") + + members = make([]*model.GroupMember, 0, len(userIDs)) + createAt := model.GetMillis() + for _, userId := range userIDs { + member := &model.GroupMember{ + GroupId: groupID, + UserId: userId, + CreateAt: createAt, + DeleteAt: 0, + } + builder = builder.Values(member.GroupId, member.UserId, member.CreateAt, member.DeleteAt) + members = append(members, member) + } + + if s.DriverName() == model.DatabaseDriverMysql { + builder = builder.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE CreateAt = ?, DeleteAt = ?", createAt, 0)) + } else if s.DriverName() == model.DatabaseDriverPostgres { + builder = builder.SuffixExpr(sq.Expr("ON CONFLICT (groupid, userid) DO UPDATE SET CreateAt = ?, DeleteAt = ?", createAt, 0)) + } + + query, args, err = builder.ToSql() + return +} + +func (s *SqlGroupStore) DeleteMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) { + members, query, args, err := s.buildDeleteMembersQuery(groupID, userIDs) + if err != nil { + return nil, err + } + + if _, err = s.GetMasterX().Exec(query, args...); err != nil { + return nil, errors.Wrap(err, "failed to delete GroupMembers") + } + return members, err +} + +func (s *SqlGroupStore) buildDeleteMembersQuery(groupID string, userIDs []string) (members []*model.GroupMember, query string, args []interface{}, err error) { + membersSelectQuery, membersSelectArgs, err := s.getQueryBuilder(). + Select("*"). + From("GroupMembers"). + Where(sq.And{ + sq.Eq{"GroupId": groupID}, + sq.Eq{"UserId": userIDs}, + sq.Eq{"DeleteAt": 0}, + }). + ToSql() + if err != nil { + return + } + + err = s.GetReplicaX().Select(&members, membersSelectQuery, membersSelectArgs...) + if err != nil { + return + } + if len(members) != len(userIDs) { + retrievedRecords := make(map[string]bool) + for _, member := range members { + retrievedRecords[member.UserId] = true + } + for _, userID := range userIDs { + if _, ok := retrievedRecords[userID]; !ok { + err = store.NewErrNotFound("User", userID) + return + } + } + } + + deleteAt := model.GetMillis() + + for _, member := range members { + member.DeleteAt = deleteAt + } + + builder := s.getQueryBuilder(). + Update("GroupMembers"). + Set("DeleteAt", deleteAt). + Where(sq.And{ + sq.Eq{"GroupId": groupID}, + sq.Eq{"UserId": userIDs}, + }) + + query, args, err = builder.ToSql() + return +} diff --git a/store/sqlstore/user_store.go b/store/sqlstore/user_store.go index 5b459b2fb9..a34a5ef94e 100644 --- a/store/sqlstore/user_store.go +++ b/store/sqlstore/user_store.go @@ -1438,7 +1438,17 @@ func (us SqlUserStore) SearchInChannel(channelId string, term string, options *m func (us SqlUserStore) SearchInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) { query := us.usersQuery. - Join("GroupMembers gm ON ( gm.UserId = u.Id AND gm.GroupId = ? )", groupID). + Join("GroupMembers gm ON ( gm.UserId = u.Id AND gm.GroupId = ? AND gm.DeleteAt = 0 )", groupID). + OrderBy("Username ASC"). + Limit(uint64(options.Limit)) + + return us.performSearch(query, term, options) +} + +func (us SqlUserStore) SearchNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) { + query := us.usersQuery. + LeftJoin("GroupMembers gm ON ( gm.UserId = u.Id AND gm.GroupId = ? )", groupID). + Where("gm.UserId IS NULL"). OrderBy("Username ASC"). Limit(uint64(options.Limit)) @@ -2070,9 +2080,11 @@ func (us SqlUserStore) GetUsersWithInvalidEmails(page int, perPage int, restrict query = query.Where("u.Email NOT LIKE LOWER(?)", wildcardSearchTerm(d)) } } + query = query.Offset(uint64(page * perPage)).Limit(uint64(perPage)) queryString, args, err := query.ToSql() + if err != nil { return nil, errors.Wrap(err, "users_get_many_tosql") } diff --git a/store/store.go b/store/store.go index ba0aea695d..f9ff4e5f9f 100644 --- a/store/store.go +++ b/store/store.go @@ -420,6 +420,7 @@ type UserStore interface { SearchNotInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) ([]*model.User, error) SearchWithoutTeam(term string, options *model.UserSearchOptions) ([]*model.User, error) SearchInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) + SearchNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) AnalyticsGetInactiveUsersCount() (int64, error) AnalyticsGetExternalUsers(hostDomain string) (bool, error) AnalyticsGetSystemAdminCount() (int64, error) @@ -774,6 +775,7 @@ type UserTermsOfServiceStore interface { type GroupStore interface { Create(group *model.Group) (*model.Group, error) + CreateWithUserIds(group *model.GroupWithUserIds) (*model.Group, error) Get(groupID string) (*model.Group, error) GetByName(name string, opts model.GroupSearchOpts) (*model.Group, error) GetByIDs(groupIDs []string) ([]*model.Group, error) @@ -787,6 +789,8 @@ type GroupStore interface { GetMemberUsersPage(groupID string, page int, perPage int) ([]*model.User, error) GetMemberCount(groupID string) (int64, error) + GetNonMemberUsersPage(groupID string, page int, perPage int) ([]*model.User, error) + GetMemberUsersInTeam(groupID string, teamID string) ([]*model.User, error) GetMemberUsersNotInChannel(groupID string, channelID string) ([]*model.User, error) @@ -861,6 +865,11 @@ type GroupStore interface { // GroupCountWithAllowReference returns the count of records in the Groups table with AllowReference set to true. GroupCountWithAllowReference() (int64, error) + + UpsertMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) + DeleteMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) + + GetMember(groupID string, userID string) (*model.GroupMember, error) } type LinkMetadataStore interface { diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index 858d81c96c..722acbcc20 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -3718,7 +3718,7 @@ func testChannelStoreGetAllChannels(t *testing.T, ss store.Store, s SqlStore) { Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } _, err = ss.Group().Create(group) require.NoError(t, err) @@ -5044,7 +5044,7 @@ func testGetMemberCountsByGroup(t *testing.T, ss store.Store) { Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } _, err := ss.Group().Create(g1) require.NoError(t, err) @@ -5116,7 +5116,7 @@ func testGetMemberCountsByGroup(t *testing.T, ss store.Store) { Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } _, err = ss.Group().Create(g2) require.NoError(t, err) @@ -5155,7 +5155,7 @@ func testGetMemberCountsByGroup(t *testing.T, ss store.Store) { Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } _, err = ss.Group().Create(g3) @@ -6146,7 +6146,7 @@ func testChannelStoreSearchAllChannels(t *testing.T, ss store.Store) { Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } _, err = ss.Group().Create(group) require.NoError(t, err) diff --git a/store/storetest/group_store.go b/store/storetest/group_store.go index 9baae5c768..dba3f01376 100644 --- a/store/storetest/group_store.go +++ b/store/storetest/group_store.go @@ -22,6 +22,8 @@ import ( func TestGroupStore(t *testing.T, ss store.Store) { t.Run("Create", func(t *testing.T) { testGroupStoreCreate(t, ss) }) + t.Run("CreateWithUserIds", func(t *testing.T) { testGroupCreateWithUserIds(t, ss) }) + t.Run("Get", func(t *testing.T) { testGroupStoreGet(t, ss) }) t.Run("GetByName", func(t *testing.T) { testGroupStoreGetByName(t, ss) }) t.Run("GetByIDs", func(t *testing.T) { testGroupStoreGetByIDs(t, ss) }) @@ -38,7 +40,9 @@ func TestGroupStore(t *testing.T, ss store.Store) { t.Run("GetMemberUsersNotInChannel", func(t *testing.T) { testGroupGetMemberUsersNotInChannel(t, ss) }) t.Run("UpsertMember", func(t *testing.T) { testUpsertMember(t, ss) }) + t.Run("UpsertMembers", func(t *testing.T) { testUpsertMembers(t, ss) }) t.Run("DeleteMember", func(t *testing.T) { testGroupDeleteMember(t, ss) }) + t.Run("DeleteMembers", func(t *testing.T) { testGroupDeleteMembers(t, ss) }) t.Run("PermanentDeleteMembersByUser", func(t *testing.T) { testGroupPermanentDeleteMembersByUser(t, ss) }) t.Run("CreateGroupSyncable", func(t *testing.T) { testCreateGroupSyncable(t, ss) }) @@ -83,6 +87,9 @@ func TestGroupStore(t *testing.T, ss store.Store) { t.Run("GroupMemberCount", func(t *testing.T) { groupTestGroupMemberCount(t, ss) }) t.Run("DistinctGroupMemberCount", func(t *testing.T) { groupTestDistinctGroupMemberCount(t, ss) }) t.Run("GroupCountWithAllowReference", func(t *testing.T) { groupTestGroupCountWithAllowReference(t, ss) }) + + t.Run("GetMember", func(t *testing.T) { groupTestGetMember(t, ss) }) + t.Run("GetNonMemberUsersPage", func(t *testing.T) { groupTestGetNonMemberUsersPage(t, ss) }) } func testGroupStoreCreate(t *testing.T, ss store.Store) { @@ -92,7 +99,7 @@ func testGroupStoreCreate(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Source: model.GroupSourceLdap, Description: model.NewId(), - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } // Happy path @@ -112,7 +119,7 @@ func testGroupStoreCreate(t *testing.T, ss store.Store) { Name: model.NewString(model.NewId()), DisplayName: "", Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } data, err := ss.Group().Create(g2) require.Nil(t, data) @@ -126,7 +133,7 @@ func testGroupStoreCreate(t *testing.T, ss store.Store) { Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } _, err = ss.Group().Create(g4) require.NoError(t, err) @@ -134,7 +141,7 @@ func testGroupStoreCreate(t *testing.T, ss store.Store) { Name: g4.Name, DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } data, err = ss.Group().Create(g4b) require.Nil(t, data) @@ -147,7 +154,7 @@ func testGroupStoreCreate(t *testing.T, ss store.Store) { DisplayName: strings.Repeat("x", model.GroupDisplayNameMaxLength), Description: strings.Repeat("x", model.GroupDescriptionMaxLength), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } require.Nil(t, g5.IsValidForCreate()) @@ -172,7 +179,7 @@ func testGroupStoreCreate(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Description: model.NewId(), Source: model.GroupSource("fake"), - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } require.Equal(t, g6.IsValidForCreate().Id, "model.group.source.app_error") @@ -182,11 +189,176 @@ func testGroupStoreCreate(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Description: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } require.Equal(t, g7.IsValidForCreate().Id, "model.group.name.invalid_chars.app_error") } +func testGroupCreateWithUserIds(t *testing.T, ss store.Store) { + // Create user 1 + u1 := &model.User{ + Email: MakeEmail(), + Username: model.NewId(), + } + user1, nErr := ss.User().Save(u1) + require.NoError(t, nErr) + + // Create user 2 + u2 := &model.User{ + Email: MakeEmail(), + Username: model.NewId(), + } + user2, nErr := ss.User().Save(u2) + require.NoError(t, nErr) + + g1 := &model.Group{ + Name: model.NewString(model.NewId()), + DisplayName: model.NewId(), + Source: model.GroupSourceCustom, + Description: model.NewId(), + RemoteId: model.NewString(model.NewId()), + } + + // Save a new group + guids1 := &model.GroupWithUserIds{ + Group: *g1, + UserIds: []string{user1.Id, user2.Id}, + } + + // Happy path + d1, err := ss.Group().CreateWithUserIds(guids1) + require.NoError(t, err) + require.Len(t, d1.Id, 26) + require.Equal(t, *guids1.Name, *d1.Name) + require.Equal(t, guids1.DisplayName, d1.DisplayName) + require.Equal(t, guids1.Description, d1.Description) + require.Equal(t, guids1.RemoteId, d1.RemoteId) + require.NotZero(t, d1.CreateAt) + require.NotZero(t, d1.UpdateAt) + require.Zero(t, d1.DeleteAt) + require.Equal(t, *model.NewInt64(2), int64(*d1.MemberCount)) + + // Requires display name + + g2 := &model.Group{ + Name: model.NewString(model.NewId()), + DisplayName: "", + Source: model.GroupSourceCustom, + Description: model.NewId(), + RemoteId: model.NewString(model.NewId()), + } + + guids2 := &model.GroupWithUserIds{ + Group: *g2, + UserIds: []string{user1.Id, user2.Id}, + } + data, err := ss.Group().CreateWithUserIds(guids2) + require.Nil(t, data) + require.Error(t, err) + var appErr *model.AppError + require.True(t, errors.As(err, &appErr)) + require.Equal(t, appErr.Id, "model.group.display_name.app_error") + + // Won't accept a duplicate name + g4 := &model.Group{ + Name: model.NewString(model.NewId()), + DisplayName: model.NewId(), + Source: model.GroupSourceCustom, + RemoteId: model.NewString(model.NewId()), + } + guids4 := &model.GroupWithUserIds{ + Group: *g4, + UserIds: []string{user1.Id, user2.Id}, + } + _, err = ss.Group().CreateWithUserIds(guids4) + require.NoError(t, err) + g4b := &model.Group{ + Name: g4.Name, + DisplayName: model.NewId(), + Source: model.GroupSourceCustom, + RemoteId: model.NewString(model.NewId()), + } + guids4b := &model.GroupWithUserIds{ + Group: *g4b, + UserIds: []string{user1.Id}, + } + data, err = ss.Group().CreateWithUserIds(guids4b) + require.Nil(t, data) + require.Error(t, err) + require.Contains(t, err.Error(), "unique constraint: Name") + + // Fields cannot be greater than max values + g5 := &model.Group{ + Name: model.NewString(strings.Repeat("x", model.GroupNameMaxLength)), + DisplayName: strings.Repeat("x", model.GroupDisplayNameMaxLength), + Description: strings.Repeat("x", model.GroupDescriptionMaxLength), + Source: model.GroupSourceCustom, + RemoteId: model.NewString(model.NewId()), + } + guids5 := &model.GroupWithUserIds{ + Group: *g5, + } + require.Nil(t, guids5.IsValidForCreate()) + + guids5.Name = model.NewString(*guids5.Name + "x") + require.Equal(t, guids5.IsValidForCreate().Id, "model.group.name.invalid_length.app_error") + guids5.Name = model.NewString(model.NewId()) + require.Nil(t, guids5.IsValidForCreate()) + + guids5.DisplayName = guids5.DisplayName + "x" + require.Equal(t, guids5.IsValidForCreate().Id, "model.group.display_name.app_error") + guids5.DisplayName = model.NewId() + require.Nil(t, guids5.IsValidForCreate()) + + guids5.Description = guids5.Description + "x" + require.Equal(t, guids5.IsValidForCreate().Id, "model.group.description.app_error") + guids5.Description = model.NewId() + require.Nil(t, guids5.IsValidForCreate()) + + // Must use a valid type + g6 := &model.Group{ + Name: model.NewString(model.NewId()), + DisplayName: model.NewId(), + Description: model.NewId(), + Source: model.GroupSource("fake"), + RemoteId: model.NewString(model.NewId()), + } + guids6 := &model.GroupWithUserIds{ + Group: *g6, + } + require.Equal(t, guids6.IsValidForCreate().Id, "model.group.source.app_error") + + //must use valid characters + g7 := &model.Group{ + Name: model.NewString("%^#@$$"), + DisplayName: model.NewId(), + Description: model.NewId(), + Source: model.GroupSourceCustom, + RemoteId: model.NewString(model.NewId()), + } + guids7 := &model.GroupWithUserIds{ + Group: *g7, + } + require.Equal(t, guids7.IsValidForCreate().Id, "model.group.name.invalid_chars.app_error") + + // Invalid user ids + g8 := &model.Group{ + Name: model.NewString(model.NewId()), + DisplayName: model.NewId(), + Description: model.NewId(), + Source: model.GroupSourceCustom, + RemoteId: model.NewString(model.NewId()), + } + guids8 := &model.GroupWithUserIds{ + Group: *g8, + UserIds: []string{"1234uid"}, + } + data, err = ss.Group().CreateWithUserIds(guids8) + require.Nil(t, data) + require.Error(t, err) + require.Equal(t, store.NewErrNotFound("User", "1234uid"), err) +} + func testGroupStoreGet(t *testing.T, ss store.Store) { // Create a group g1 := &model.Group{ @@ -194,7 +366,7 @@ func testGroupStoreGet(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Description: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } d1, err := ss.Group().Create(g1) require.NoError(t, err) @@ -226,7 +398,7 @@ func testGroupStoreGetByName(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Description: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } g1Opts := model.GroupSearchOpts{ FilterAllowReference: false, @@ -265,7 +437,7 @@ func testGroupStoreGetByIDs(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Description: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group, err := ss.Group().Create(group) require.NoError(t, err) @@ -295,14 +467,14 @@ func testGroupStoreGetByRemoteID(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Description: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } d1, err := ss.Group().Create(g1) require.NoError(t, err) require.Len(t, d1.Id, 26) // Get the group - d2, err := ss.Group().GetByRemoteID(d1.RemoteId, model.GroupSourceLdap) + d2, err := ss.Group().GetByRemoteID(*d1.RemoteId, model.GroupSourceLdap) require.NoError(t, err) require.Equal(t, d1.Id, d2.Id) require.Equal(t, *d1.Name, *d2.Name) @@ -332,7 +504,7 @@ func testGroupStoreGetAllByType(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Description: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } groups = append(groups, g) _, err := ss.Group().Create(g) @@ -362,7 +534,7 @@ func testGroupStoreGetByUser(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Description: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } g1, err := ss.Group().Create(g1) require.NoError(t, err) @@ -372,7 +544,7 @@ func testGroupStoreGetByUser(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Description: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } g2, err = ss.Group().Create(g2) require.NoError(t, err) @@ -432,7 +604,7 @@ func testGroupStoreUpdate(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Source: model.GroupSourceLdap, Description: model.NewId(), - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } // Create a group @@ -445,7 +617,7 @@ func testGroupStoreUpdate(t *testing.T, ss store.Store) { g1Update.Name = model.NewString(model.NewId()) g1Update.DisplayName = model.NewId() g1Update.Description = model.NewId() - g1Update.RemoteId = model.NewId() + g1Update.RemoteId = model.NewString(model.NewId()) ud1, err := ss.Group().Update(g1Update) require.NoError(t, err) @@ -467,7 +639,7 @@ func testGroupStoreUpdate(t *testing.T, ss store.Store) { Name: model.NewString(model.NewId()), DisplayName: "", Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) require.Nil(t, data) require.Error(t, err) @@ -481,7 +653,7 @@ func testGroupStoreUpdate(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Source: model.GroupSourceLdap, Description: model.NewId(), - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } d2, err := ss.Group().Create(g2) require.NoError(t, err) @@ -493,7 +665,7 @@ func testGroupStoreUpdate(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Source: model.GroupSourceLdap, Description: model.NewId(), - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) require.Error(t, err) require.Contains(t, err.Error(), fmt.Sprintf("Group with name %s already exists", *g1Update.Name)) @@ -525,7 +697,7 @@ func testGroupStoreDelete(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Description: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } d1, err := ss.Group().Create(g1) @@ -574,7 +746,7 @@ func testGroupGetMemberUsers(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Description: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group, err := ss.Group().Create(g1) require.NoError(t, err) @@ -626,7 +798,7 @@ func testGroupGetMemberUsersPage(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Description: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group, err := ss.Group().Create(g1) require.NoError(t, err) @@ -713,7 +885,7 @@ func testGroupGetMemberUsersInTeam(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Description: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group, err := ss.Group().Create(g1) require.NoError(t, err) @@ -799,7 +971,7 @@ func testGroupGetMemberUsersNotInChannel(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Description: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group, err := ss.Group().Create(g1) require.NoError(t, err) @@ -925,7 +1097,7 @@ func testUpsertMember(t *testing.T, ss store.Store) { Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group, err := ss.Group().Create(g1) require.NoError(t, err) @@ -980,13 +1152,79 @@ func testUpsertMember(t *testing.T, ss store.Store) { require.Equal(t, beforeRestoreCount+1, afterRestoreCount) } +func testUpsertMembers(t *testing.T, ss store.Store) { + // Create group + g1 := &model.Group{ + Name: model.NewString(model.NewId()), + DisplayName: model.NewId(), + Source: model.GroupSourceLdap, + RemoteId: model.NewString(model.NewId()), + } + group, err := ss.Group().Create(g1) + require.NoError(t, err) + + // Create user + u1 := &model.User{ + Email: MakeEmail(), + Username: model.NewId(), + } + user, nErr := ss.User().Save(u1) + require.NoError(t, nErr) + + // Create user + u2 := &model.User{ + Email: MakeEmail(), + Username: model.NewId(), + } + user2, nErr := ss.User().Save(u2) + require.NoError(t, nErr) + + // Happy path + m, err := ss.Group().UpsertMembers(group.Id, []string{user.Id, user2.Id}) + require.NoError(t, err) + require.Equal(t, 2, len(m)) + + // Duplicate composite key (GroupId, UserId) + // Ensure new CreateAt > previous CreateAt for the same (groupId, userId) + // time.Sleep(1 * time.Millisecond) + _, err = ss.Group().UpsertMembers(group.Id, []string{user.Id}) + require.NoError(t, err) + + // Invalid GroupId + _, err = ss.Group().UpsertMembers(model.NewId(), []string{user.Id}) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to get UserGroup with") + + // Restores a deleted member + // Ensure new CreateAt > previous CreateAt for the same (groupId, userId) + time.Sleep(1 * time.Millisecond) + _, err = ss.Group().UpsertMembers(group.Id, []string{user.Id, user2.Id}) + require.NoError(t, err) + + _, err = ss.Group().DeleteMembers(group.Id, []string{user.Id}) + require.NoError(t, err) + + groupMembers, err := ss.Group().GetMemberUsers(group.Id) + require.NoError(t, err) + beforeRestoreCount := len(groupMembers) + + _, err = ss.Group().UpsertMembers(group.Id, []string{user.Id, user2.Id}) + require.NoError(t, err) + + groupMembers, err = ss.Group().GetMemberUsers(group.Id) + require.NoError(t, err) + afterRestoreCount := len(groupMembers) + + require.Equal(t, beforeRestoreCount+1, afterRestoreCount) +} + func testGroupDeleteMember(t *testing.T, ss store.Store) { // Create group g1 := &model.Group{ Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group, err := ss.Group().Create(g1) require.NoError(t, err) @@ -1025,6 +1263,49 @@ func testGroupDeleteMember(t *testing.T, ss store.Store) { require.True(t, errors.As(err, &nfErr)) } +func testGroupDeleteMembers(t *testing.T, ss store.Store) { + // Create user + u1 := &model.User{ + Email: MakeEmail(), + Username: model.NewId(), + } + user, nErr := ss.User().Save(u1) + require.NoError(t, nErr) + // Create group + g1 := &model.Group{ + Name: model.NewString(model.NewId()), + DisplayName: model.NewId(), + Source: model.GroupSourceLdap, + RemoteId: model.NewString(model.NewId()), + } + guids := &model.GroupWithUserIds{ + Group: *g1, + UserIds: []string{user.Id}, + } + group, err := ss.Group().CreateWithUserIds(guids) + require.NoError(t, err) + + // Happy path + d2, err := ss.Group().DeleteMembers(group.Id, []string{user.Id}) + require.NoError(t, err) + require.Equal(t, d2[0].GroupId, group.Id) + require.Equal(t, d2[0].UserId, user.Id) + require.NotZero(t, d2[0].DeleteAt) + + // Delete an already deleted member + _, err = ss.Group().DeleteMembers(group.Id, []string{user.Id}) + var nfErr *store.ErrNotFound + require.True(t, errors.As(err, &nfErr)) + + // Delete with non-existent User + _, err = ss.Group().DeleteMembers(group.Id, []string{model.NewId()}) + require.True(t, errors.As(err, &nfErr)) + + // Delete non-existent Group + _, err = ss.Group().DeleteMembers(model.NewId(), []string{user.Id}) + require.True(t, errors.As(err, &nfErr)) +} + func testGroupPermanentDeleteMembersByUser(t *testing.T, ss store.Store) { var g *model.Group var groups []*model.Group @@ -1035,7 +1316,7 @@ func testGroupPermanentDeleteMembersByUser(t *testing.T, ss store.Store) { Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group, err := ss.Group().Create(g) groups = append(groups, group) @@ -1073,7 +1354,7 @@ func testCreateGroupSyncable(t *testing.T, ss store.Store) { Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group, err := ss.Group().Create(g1) require.NoError(t, err) @@ -1110,7 +1391,7 @@ func testGetGroupSyncable(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Description: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group, err := ss.Group().Create(g1) require.NoError(t, err) @@ -1154,7 +1435,7 @@ func testGetAllGroupSyncablesByGroup(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Description: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group, err := ss.Group().Create(g) require.NoError(t, err) @@ -1210,7 +1491,7 @@ func testUpdateGroupSyncable(t *testing.T, ss store.Store) { Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group, err := ss.Group().Create(g1) require.NoError(t, err) @@ -1278,7 +1559,7 @@ func testDeleteGroupSyncable(t *testing.T, ss store.Store) { Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group, err := ss.Group().Create(g1) require.NoError(t, err) @@ -1333,7 +1614,7 @@ func testTeamMembersToAdd(t *testing.T, ss store.Store) { group, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: "TeamMembersToAdd Test Group", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, }) require.NoError(t, err) @@ -1511,7 +1792,7 @@ func testTeamMembersToAddSingleTeam(t *testing.T, ss store.Store) { group1, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: "TeamMembersToAdd Test Group", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, }) require.NoError(t, err) @@ -1519,7 +1800,7 @@ func testTeamMembersToAddSingleTeam(t *testing.T, ss store.Store) { group2, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: "TeamMembersToAdd Test Group", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, }) require.NoError(t, err) @@ -1602,7 +1883,7 @@ func testChannelMembersToAdd(t *testing.T, ss store.Store) { group, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: "ChannelMembersToAdd Test Group", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, }) require.NoError(t, err) @@ -1778,7 +2059,7 @@ func testChannelMembersToAddSingleChannel(t *testing.T, ss store.Store) { group1, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: "TeamMembersToAdd Test Group", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, }) require.NoError(t, err) @@ -1786,7 +2067,7 @@ func testChannelMembersToAddSingleChannel(t *testing.T, ss store.Store) { group2, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: "TeamMembersToAdd Test Group", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, }) require.NoError(t, err) @@ -2166,7 +2447,7 @@ func pendingMemberRemovalsDataSetup(t *testing.T, ss store.Store) *removalsData group, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: "Pending[Channel|Team]MemberRemovals Test Group", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, }) require.NoError(t, err) @@ -2329,7 +2610,7 @@ func testGetGroupsByChannel(t *testing.T, ss store.Store) { group1, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: "group-1", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, AllowReference: true, }) @@ -2338,7 +2619,7 @@ func testGetGroupsByChannel(t *testing.T, ss store.Store) { group2, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: "group-2", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, AllowReference: false, }) @@ -2347,7 +2628,7 @@ func testGetGroupsByChannel(t *testing.T, ss store.Store) { deletedGroup, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: "group-deleted", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, AllowReference: true, DeleteAt: 1, @@ -2379,7 +2660,7 @@ func testGetGroupsByChannel(t *testing.T, ss store.Store) { group3, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: "group-3", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, AllowReference: true, }) @@ -2577,7 +2858,7 @@ func testGetGroupsAssociatedToChannelsByTeam(t *testing.T, ss store.Store) { group1, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: "group-1", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, AllowReference: false, }) @@ -2586,7 +2867,7 @@ func testGetGroupsAssociatedToChannelsByTeam(t *testing.T, ss store.Store) { group2, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: "group-2", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, AllowReference: true, }) @@ -2595,7 +2876,7 @@ func testGetGroupsAssociatedToChannelsByTeam(t *testing.T, ss store.Store) { deletedGroup, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: "group-deleted", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, AllowReference: true, DeleteAt: 1, @@ -2627,7 +2908,7 @@ func testGetGroupsAssociatedToChannelsByTeam(t *testing.T, ss store.Store) { group3, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: "group-3", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, AllowReference: true, }) @@ -2810,7 +3091,7 @@ func testGetGroupsByTeam(t *testing.T, ss store.Store) { group1, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: "group-1", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, AllowReference: false, }) @@ -2819,7 +3100,7 @@ func testGetGroupsByTeam(t *testing.T, ss store.Store) { group2, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: "group-2", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, AllowReference: true, }) @@ -2828,7 +3109,7 @@ func testGetGroupsByTeam(t *testing.T, ss store.Store) { deletedGroup, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: "group-deleted", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, AllowReference: true, DeleteAt: 1, @@ -2864,7 +3145,7 @@ func testGetGroupsByTeam(t *testing.T, ss store.Store) { group3, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: "group-3", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, AllowReference: true, }) @@ -3069,7 +3350,7 @@ func testGetGroups(t *testing.T, ss store.Store) { group1, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: "group-1", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, AllowReference: true, }) @@ -3078,7 +3359,7 @@ func testGetGroups(t *testing.T, ss store.Store) { group2, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId() + "-group-2"), DisplayName: "group-2", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, AllowReference: false, }) @@ -3087,7 +3368,7 @@ func testGetGroups(t *testing.T, ss store.Store) { deletedGroup, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId() + "-group-deleted"), DisplayName: "group-deleted", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, AllowReference: false, DeleteAt: 1, @@ -3143,7 +3424,7 @@ func testGetGroups(t *testing.T, ss store.Store) { group3, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId() + "-group-3"), DisplayName: "group-3", - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), Source: model.GroupSourceLdap, AllowReference: true, }) @@ -3199,6 +3480,9 @@ func testGetGroups(t *testing.T, ss store.Store) { _, err = ss.Group().UpsertMember(group1.Id, user2.Id) require.NoError(t, err) + _, err = ss.Group().UpsertMember(group2.Id, user2.Id) + require.NoError(t, err) + _, err = ss.Group().UpsertMember(deletedGroup.Id, user1.Id) require.NoError(t, err) @@ -3451,6 +3735,33 @@ func testGetGroups(t *testing.T, ss store.Store) { return len(groups) > 0 }, }, + { + Name: "Filter by group member", + Opts: model.GroupSearchOpts{FilterHasMember: user1.Id}, + Page: 0, + PerPage: 100, + Resultf: func(groups []*model.Group) bool { + return len(groups) == 1 && groups[0].Id == group1.Id + }, + }, + { + Name: "Filter by non-existent group member", + Opts: model.GroupSearchOpts{FilterHasMember: model.NewId()}, + Page: 0, + PerPage: 100, + Resultf: func(groups []*model.Group) bool { + return len(groups) == 0 + }, + }, + { + Name: "Filter by non-member member", + Opts: model.GroupSearchOpts{FilterHasMember: user2.Id}, + Page: 0, + PerPage: 100, + Resultf: func(groups []*model.Group) bool { + return len(groups) == 2 + }, + }, } for _, tc := range testCases { @@ -3514,7 +3825,7 @@ func testTeamMembersMinusGroupMembers(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Source: model.GroupSourceLdap, Description: model.NewId(), - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group, err := ss.Group().Create(group) require.NoError(t, err) @@ -3677,7 +3988,7 @@ func testChannelMembersMinusGroupMembers(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Source: model.GroupSourceLdap, Description: model.NewId(), - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group, err := ss.Group().Create(group) require.NoError(t, err) @@ -3787,7 +4098,7 @@ func groupTestGetMemberCount(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Source: model.GroupSourceLdap, Description: model.NewId(), - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group, err := ss.Group().Create(group) require.NoError(t, err) @@ -3832,7 +4143,7 @@ func groupTestAdminRoleGroupsForSyncableMemberChannel(t *testing.T, ss store.Sto DisplayName: model.NewId(), Source: model.GroupSourceLdap, Description: model.NewId(), - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group1, err = ss.Group().Create(group1) require.NoError(t, err) @@ -3845,7 +4156,7 @@ func groupTestAdminRoleGroupsForSyncableMemberChannel(t *testing.T, ss store.Sto DisplayName: model.NewId(), Source: model.GroupSourceLdap, Description: model.NewId(), - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group2, err = ss.Group().Create(group2) require.NoError(t, err) @@ -3920,7 +4231,7 @@ func groupTestAdminRoleGroupsForSyncableMemberTeam(t *testing.T, ss store.Store) DisplayName: model.NewId(), Source: model.GroupSourceLdap, Description: model.NewId(), - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group1, err = ss.Group().Create(group1) require.NoError(t, err) @@ -3933,7 +4244,7 @@ func groupTestAdminRoleGroupsForSyncableMemberTeam(t *testing.T, ss store.Store) DisplayName: model.NewId(), Source: model.GroupSourceLdap, Description: model.NewId(), - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group2, err = ss.Group().Create(group2) require.NoError(t, err) @@ -4021,7 +4332,7 @@ func groupTestPermittedSyncableAdminsTeam(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Source: model.GroupSourceLdap, Description: model.NewId(), - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group1, err = ss.Group().Create(group1) require.NoError(t, err) @@ -4036,7 +4347,7 @@ func groupTestPermittedSyncableAdminsTeam(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Source: model.GroupSourceLdap, Description: model.NewId(), - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group2, err = ss.Group().Create(group2) require.NoError(t, err) @@ -4127,7 +4438,7 @@ func groupTestPermittedSyncableAdminsChannel(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Source: model.GroupSourceLdap, Description: model.NewId(), - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group1, err = ss.Group().Create(group1) require.NoError(t, err) @@ -4142,7 +4453,7 @@ func groupTestPermittedSyncableAdminsChannel(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Source: model.GroupSourceLdap, Description: model.NewId(), - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } group2, err = ss.Group().Create(group2) require.NoError(t, err) @@ -4426,7 +4737,7 @@ func groupTestGroupCount(t *testing.T, ss store.Store) { Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) require.NoError(t, err) defer ss.Group().Delete(group1.Id) @@ -4439,7 +4750,7 @@ func groupTestGroupCount(t *testing.T, ss store.Store) { Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) require.NoError(t, err) defer ss.Group().Delete(group2.Id) @@ -4466,7 +4777,7 @@ func groupTestGroupTeamCount(t *testing.T, ss store.Store) { Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) require.NoError(t, err) defer ss.Group().Delete(group1.Id) @@ -4475,7 +4786,7 @@ func groupTestGroupTeamCount(t *testing.T, ss store.Store) { Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) require.NoError(t, err) defer ss.Group().Delete(group2.Id) @@ -4511,7 +4822,7 @@ func groupTestGroupChannelCount(t *testing.T, ss store.Store) { Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) require.NoError(t, err) defer ss.Group().Delete(group1.Id) @@ -4520,7 +4831,7 @@ func groupTestGroupChannelCount(t *testing.T, ss store.Store) { Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) require.NoError(t, err) defer ss.Group().Delete(group2.Id) @@ -4543,16 +4854,30 @@ func groupTestGroupChannelCount(t *testing.T, ss store.Store) { } func groupTestGroupMemberCount(t *testing.T, ss store.Store) { + user := &model.User{ + Email: fmt.Sprintf("test.%s@localhost", model.NewId()), + Username: model.NewId(), + } + user, err := ss.User().Save(user) + require.NoError(t, err) + + user2 := &model.User{ + Email: fmt.Sprintf("test.%s@localhost", model.NewId()), + Username: model.NewId(), + } + user2, err = ss.User().Save(user2) + require.NoError(t, err) + group, err := ss.Group().Create(&model.Group{ Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) require.NoError(t, err) defer ss.Group().Delete(group.Id) - member1, err := ss.Group().UpsertMember(group.Id, model.NewId()) + member1, err := ss.Group().UpsertMember(group.Id, user.Id) require.NoError(t, err) defer ss.Group().DeleteMember(group.Id, member1.UserId) @@ -4560,7 +4885,7 @@ func groupTestGroupMemberCount(t *testing.T, ss store.Store) { require.NoError(t, err) require.GreaterOrEqual(t, count, int64(1)) - member2, err := ss.Group().UpsertMember(group.Id, model.NewId()) + member2, err := ss.Group().UpsertMember(group.Id, user2.Id) require.NoError(t, err) defer ss.Group().DeleteMember(group.Id, member2.UserId) @@ -4574,7 +4899,7 @@ func groupTestDistinctGroupMemberCount(t *testing.T, ss store.Store) { Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) require.NoError(t, err) defer ss.Group().Delete(group1.Id) @@ -4583,12 +4908,26 @@ func groupTestDistinctGroupMemberCount(t *testing.T, ss store.Store) { Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) require.NoError(t, err) defer ss.Group().Delete(group2.Id) - member1, err := ss.Group().UpsertMember(group1.Id, model.NewId()) + user := &model.User{ + Email: fmt.Sprintf("test.%s@localhost", model.NewId()), + Username: model.NewId(), + } + user, err = ss.User().Save(user) + require.NoError(t, err) + + user2 := &model.User{ + Email: fmt.Sprintf("test.%s@localhost", model.NewId()), + Username: model.NewId(), + } + user2, err = ss.User().Save(user2) + require.NoError(t, err) + + member1, err := ss.Group().UpsertMember(group1.Id, user.Id) require.NoError(t, err) defer ss.Group().DeleteMember(group1.Id, member1.UserId) @@ -4596,7 +4935,7 @@ func groupTestDistinctGroupMemberCount(t *testing.T, ss store.Store) { require.NoError(t, err) require.GreaterOrEqual(t, count, int64(1)) - member2, err := ss.Group().UpsertMember(group1.Id, model.NewId()) + member2, err := ss.Group().UpsertMember(group1.Id, user2.Id) require.NoError(t, err) defer ss.Group().DeleteMember(group1.Id, member2.UserId) @@ -4621,7 +4960,7 @@ func groupTestGroupCountWithAllowReference(t *testing.T, ss store.Store) { Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), }) require.NoError(t, err) defer ss.Group().Delete(group1.Id) @@ -4634,7 +4973,7 @@ func groupTestGroupCountWithAllowReference(t *testing.T, ss store.Store) { Name: model.NewString(model.NewId()), DisplayName: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), AllowReference: true, }) require.NoError(t, err) @@ -4644,3 +4983,82 @@ func groupTestGroupCountWithAllowReference(t *testing.T, ss store.Store) { require.NoError(t, err) require.Greater(t, countAfter, count) } + +func groupTestGetMember(t *testing.T, ss store.Store) { + g1 := &model.Group{ + Name: model.NewString(model.NewId()), + DisplayName: model.NewId(), + Description: model.NewId(), + Source: model.GroupSourceLdap, + RemoteId: model.NewString(model.NewId()), + } + group, err := ss.Group().Create(g1) + require.NoError(t, err) + + u1 := &model.User{ + Email: MakeEmail(), + Username: model.NewId(), + } + user1, nErr := ss.User().Save(u1) + require.NoError(t, nErr) + + u2 := &model.User{ + Email: MakeEmail(), + Username: model.NewId(), + } + user2, nErr := ss.User().Save(u2) + require.NoError(t, nErr) + + _, err = ss.Group().UpsertMember(group.Id, user1.Id) + require.NoError(t, err) + + member, err := ss.Group().GetMember(g1.Id, u1.Id) + require.NoError(t, err) + require.NotNil(t, member) + + member, err = ss.Group().GetMember(g1.Id, user2.Id) + require.Error(t, err) + require.Nil(t, member) +} + +func groupTestGetNonMemberUsersPage(t *testing.T, ss store.Store) { + g1 := &model.Group{ + Name: model.NewString(model.NewId()), + DisplayName: model.NewId(), + Description: model.NewId(), + Source: model.GroupSourceLdap, + RemoteId: model.NewString(model.NewId()), + } + group, err := ss.Group().Create(g1) + require.NoError(t, err) + + u1 := &model.User{ + Email: MakeEmail(), + Username: model.NewId(), + } + user1, nErr := ss.User().Save(u1) + require.NoError(t, nErr) + + u2 := &model.User{ + Email: MakeEmail(), + Username: model.NewId(), + } + _, nErr = ss.User().Save(u2) + require.NoError(t, nErr) + + users, err := ss.Group().GetNonMemberUsersPage(group.Id, 0, 1000) + require.NoError(t, err) + + originalLen := len(users) + + _, err = ss.Group().UpsertMember(group.Id, user1.Id) + require.NoError(t, err) + + users, err = ss.Group().GetNonMemberUsersPage(group.Id, 0, 1000) + require.NoError(t, err) + require.Len(t, users, originalLen-1) + + users, err = ss.Group().GetNonMemberUsersPage(model.NewId(), 0, 1000) + require.Error(t, err) + require.Nil(t, users) +} diff --git a/store/storetest/mocks/GroupStore.go b/store/storetest/mocks/GroupStore.go index 77a0d568ab..b6ce4267e4 100644 --- a/store/storetest/mocks/GroupStore.go +++ b/store/storetest/mocks/GroupStore.go @@ -236,6 +236,29 @@ func (_m *GroupStore) CreateGroupSyncable(groupSyncable *model.GroupSyncable) (* return r0, r1 } +// CreateWithUserIds provides a mock function with given fields: group +func (_m *GroupStore) CreateWithUserIds(group *model.GroupWithUserIds) (*model.Group, error) { + ret := _m.Called(group) + + var r0 *model.Group + if rf, ok := ret.Get(0).(func(*model.GroupWithUserIds) *model.Group); ok { + r0 = rf(group) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Group) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*model.GroupWithUserIds) error); ok { + r1 = rf(group) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Delete provides a mock function with given fields: groupID func (_m *GroupStore) Delete(groupID string) (*model.Group, error) { ret := _m.Called(groupID) @@ -305,6 +328,29 @@ func (_m *GroupStore) DeleteMember(groupID string, userID string) (*model.GroupM return r0, r1 } +// DeleteMembers provides a mock function with given fields: groupID, userIDs +func (_m *GroupStore) DeleteMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) { + ret := _m.Called(groupID, userIDs) + + var r0 []*model.GroupMember + 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) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, []string) error); ok { + r1 = rf(groupID, userIDs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // DistinctGroupMemberCount provides a mock function with given fields: func (_m *GroupStore) DistinctGroupMemberCount() (int64, error) { ret := _m.Called() @@ -602,6 +648,29 @@ func (_m *GroupStore) GetGroupsByTeam(teamID string, opts model.GroupSearchOpts) return r0, r1 } +// GetMember provides a mock function with given fields: groupID, userID +func (_m *GroupStore) GetMember(groupID string, userID string) (*model.GroupMember, error) { + ret := _m.Called(groupID, userID) + + var r0 *model.GroupMember + 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) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string) error); ok { + r1 = rf(groupID, userID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetMemberCount provides a mock function with given fields: groupID func (_m *GroupStore) GetMemberCount(groupID string) (int64, error) { ret := _m.Called(groupID) @@ -715,6 +784,29 @@ func (_m *GroupStore) GetMemberUsersPage(groupID string, page int, perPage int) return r0, r1 } +// GetNonMemberUsersPage provides a mock function with given fields: groupID, page, perPage +func (_m *GroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int) ([]*model.User, error) { + ret := _m.Called(groupID, page, perPage) + + var r0 []*model.User + if rf, ok := ret.Get(0).(func(string, int, int) []*model.User); ok { + r0 = rf(groupID, page, perPage) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.User) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, int, int) error); ok { + r1 = rf(groupID, page, perPage) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GroupChannelCount provides a mock function with given fields: func (_m *GroupStore) GroupChannelCount() (int64, error) { ret := _m.Called() @@ -994,3 +1086,26 @@ func (_m *GroupStore) UpsertMember(groupID string, userID string) (*model.GroupM return r0, r1 } + +// UpsertMembers provides a mock function with given fields: groupID, userIDs +func (_m *GroupStore) UpsertMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) { + ret := _m.Called(groupID, userIDs) + + var r0 []*model.GroupMember + 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) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, []string) error); ok { + r1 = rf(groupID, userIDs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/store/storetest/mocks/UserStore.go b/store/storetest/mocks/UserStore.go index ab8112d87d..3aa5d3c7b1 100644 --- a/store/storetest/mocks/UserStore.go +++ b/store/storetest/mocks/UserStore.go @@ -1274,6 +1274,29 @@ func (_m *UserStore) SearchNotInChannel(teamID string, channelID string, term st return r0, r1 } +// SearchNotInGroup provides a mock function with given fields: groupID, term, options +func (_m *UserStore) SearchNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) { + ret := _m.Called(groupID, term, options) + + var r0 []*model.User + if rf, ok := ret.Get(0).(func(string, string, *model.UserSearchOptions) []*model.User); ok { + r0 = rf(groupID, term, options) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.User) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string, *model.UserSearchOptions) error); ok { + r1 = rf(groupID, term, options) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // SearchNotInTeam provides a mock function with given fields: notInTeamID, term, options func (_m *UserStore) SearchNotInTeam(notInTeamID string, term string, options *model.UserSearchOptions) ([]*model.User, error) { ret := _m.Called(notInTeamID, term, options) diff --git a/store/storetest/team_store.go b/store/storetest/team_store.go index 5edaf4b229..654e936cf5 100644 --- a/store/storetest/team_store.go +++ b/store/storetest/team_store.go @@ -625,12 +625,6 @@ func testTeamStoreGetByInviteId(t *testing.T, ss store.Store) { save1, err := ss.Team().Save(&o1) require.NoError(t, err) - o2 := model.Team{} - o2.DisplayName = "DisplayName" - o2.Name = NewTestId() - o2.Email = MakeEmail() - o2.Type = model.TeamOpen - r1, err := ss.Team().GetByInviteId(save1.InviteId) require.NoError(t, err) require.Equal(t, *r1, o1, "invalid returned team") diff --git a/store/storetest/user_store.go b/store/storetest/user_store.go index 825fe26219..4caeb67701 100644 --- a/store/storetest/user_store.go +++ b/store/storetest/user_store.go @@ -80,6 +80,7 @@ func TestUserStore(t *testing.T, ss store.Store, s SqlStore) { t.Run("SearchNotInTeam", func(t *testing.T) { testUserStoreSearchNotInTeam(t, ss) }) t.Run("SearchWithoutTeam", func(t *testing.T) { testUserStoreSearchWithoutTeam(t, ss) }) t.Run("SearchInGroup", func(t *testing.T) { testUserStoreSearchInGroup(t, ss) }) + t.Run("SearchNotInGroup", func(t *testing.T) { testUserStoreSearchNotInGroup(t, ss) }) t.Run("GetProfilesNotInTeam", func(t *testing.T) { testUserStoreGetProfilesNotInTeam(t, ss) }) t.Run("ClearAllCustomRoleAssignments", func(t *testing.T) { testUserStoreClearAllCustomRoleAssignments(t, ss) }) t.Run("GetAllAfter", func(t *testing.T) { testUserStoreGetAllAfter(t, ss) }) @@ -1467,7 +1468,7 @@ func testUserStoreGetProfilesNotInChannel(t *testing.T, ss store.Store) { Name: model.NewString("n_" + model.NewId()), DisplayName: "dn_" + model.NewId(), Source: model.GroupSourceLdap, - RemoteId: "ri_" + model.NewId(), + RemoteId: model.NewString("ri_" + model.NewId()), }) require.NoError(t, err) @@ -3489,7 +3490,6 @@ func testUserStoreSearchInGroup(t *testing.T, ss store.Store) { u3 := &model.User{ Username: "jimbo3" + model.NewId(), Email: MakeEmail(), - DeleteAt: 1, } _, err = ss.User().Save(u3) require.NoError(t, err) @@ -3507,7 +3507,7 @@ func testUserStoreSearchInGroup(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Description: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } _, err = ss.Group().Create(g1) require.NoError(t, err) @@ -3517,7 +3517,7 @@ func testUserStoreSearchInGroup(t *testing.T, ss store.Store) { DisplayName: model.NewId(), Description: model.NewId(), Source: model.GroupSourceLdap, - RemoteId: model.NewId(), + RemoteId: model.NewString(model.NewId()), } _, err = ss.Group().Create(g2) require.NoError(t, err) @@ -3531,6 +3531,10 @@ func testUserStoreSearchInGroup(t *testing.T, ss store.Store) { _, err = ss.Group().UpsertMember(g1.Id, u3.Id) require.NoError(t, err) + u3.DeleteAt = 1 + _, err = ss.User().Update(u3, true) + require.NoError(t, err) + testCases := []struct { Description string GroupId string @@ -3606,6 +3610,138 @@ func testUserStoreSearchInGroup(t *testing.T, ss store.Store) { } } +func testUserStoreSearchNotInGroup(t *testing.T, ss store.Store) { + u1 := &model.User{ + Username: "jimbo1" + model.NewId(), + FirstName: "Tim", + LastName: "Bill", + Nickname: "Rob", + Email: "harold" + model.NewId() + "@simulator.amazonses.com", + } + _, err := ss.User().Save(u1) + require.NoError(t, err) + defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }() + + u2 := &model.User{ + Username: "jim-bobby" + model.NewId(), + Email: MakeEmail(), + } + _, err = ss.User().Save(u2) + require.NoError(t, err) + defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }() + + u3 := &model.User{ + Username: "jimbo3" + model.NewId(), + Email: MakeEmail(), + } + _, err = ss.User().Save(u3) + require.NoError(t, err) + defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }() + + // The users returned from the database will have AuthData as an empty string. + nilAuthData := model.NewString("") + + u1.AuthData = nilAuthData + u2.AuthData = nilAuthData + u3.AuthData = nilAuthData + + g1 := &model.Group{ + Name: model.NewString(model.NewId()), + DisplayName: model.NewId(), + Description: model.NewId(), + Source: model.GroupSourceCustom, + RemoteId: model.NewString(model.NewId()), + } + _, err = ss.Group().Create(g1) + require.NoError(t, err) + + g2 := &model.Group{ + Name: model.NewString(model.NewId()), + DisplayName: model.NewId(), + Description: model.NewId(), + Source: model.GroupSourceCustom, + RemoteId: model.NewString(model.NewId()), + } + _, err = ss.Group().Create(g2) + require.NoError(t, err) + + _, err = ss.Group().UpsertMember(g1.Id, u1.Id) + require.NoError(t, err) + + _, err = ss.Group().UpsertMember(g2.Id, u2.Id) + require.NoError(t, err) + + _, err = ss.Group().UpsertMember(g1.Id, u3.Id) + require.NoError(t, err) + + u3.DeleteAt = 1 + _, err = ss.User().Update(u3, true) + require.NoError(t, err) + + testCases := []struct { + Description string + GroupId string + Term string + Options *model.UserSearchOptions + Expected []*model.User + }{ + { + "search jimb, not in group 1", + g1.Id, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.UserSearchDefaultLimit, + }, + []*model.User{}, + }, + { + "search jim, not in group 1", + g1.Id, + "jim", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.UserSearchDefaultLimit, + }, + []*model.User{u2}, + }, + { + "search jimb, not in group 3, allow inactive", + g2.Id, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowInactive: true, + Limit: model.UserSearchDefaultLimit, + }, + []*model.User{u1, u3}, + }, + { + "search jim, not in group 2", + g2.Id, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowInactive: true, + Limit: model.UserSearchDefaultLimit, + }, + []*model.User{u1, u3}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + users, err := ss.User().SearchNotInGroup( + testCase.GroupId, + testCase.Term, + testCase.Options, + ) + require.NoError(t, err) + assertUsers(t, testCase.Expected, users) + }) + } +} + func testCount(t *testing.T, ss store.Store) { // Regular teamId := model.NewId() @@ -4362,7 +4498,7 @@ func testUserStoreGetProfilesNotInTeam(t *testing.T, ss store.Store) { Name: model.NewString("n_" + model.NewId()), DisplayName: "dn_" + model.NewId(), Source: model.GroupSourceLdap, - RemoteId: "ri_" + model.NewId(), + RemoteId: model.NewString("ri_" + model.NewId()), }) require.NoError(t, err) @@ -4679,7 +4815,7 @@ func testUserStoreGetTeamGroupUsers(t *testing.T, ss store.Store) { Name: model.NewString("n_" + id), DisplayName: "dn_" + id, Source: model.GroupSourceLdap, - RemoteId: "ri_" + id, + RemoteId: model.NewString("ri_" + id), }) require.NoError(t, err) require.NotNil(t, group) @@ -4800,7 +4936,7 @@ func testUserStoreGetChannelGroupUsers(t *testing.T, ss store.Store) { Name: model.NewString("n_" + id), DisplayName: "dn_" + id, Source: model.GroupSourceLdap, - RemoteId: "ri_" + id, + RemoteId: model.NewString("ri_" + id), }) require.NoError(t, err) require.NotNil(t, group) diff --git a/store/timerlayer/timerlayer.go b/store/timerlayer/timerlayer.go index 70ab563494..15bc065dc9 100644 --- a/store/timerlayer/timerlayer.go +++ b/store/timerlayer/timerlayer.go @@ -3387,6 +3387,22 @@ func (s *TimerLayerGroupStore) CreateGroupSyncable(groupSyncable *model.GroupSyn return result, err } +func (s *TimerLayerGroupStore) CreateWithUserIds(group *model.GroupWithUserIds) (*model.Group, error) { + start := timemodule.Now() + + result, err := s.GroupStore.CreateWithUserIds(group) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.CreateWithUserIds", success, elapsed) + } + return result, err +} + func (s *TimerLayerGroupStore) Delete(groupID string) (*model.Group, error) { start := timemodule.Now() @@ -3435,6 +3451,22 @@ func (s *TimerLayerGroupStore) DeleteMember(groupID string, userID string) (*mod return result, err } +func (s *TimerLayerGroupStore) DeleteMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) { + start := timemodule.Now() + + result, err := s.GroupStore.DeleteMembers(groupID, userIDs) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.DeleteMembers", success, elapsed) + } + return result, err +} + func (s *TimerLayerGroupStore) DistinctGroupMemberCount() (int64, error) { start := timemodule.Now() @@ -3643,6 +3675,22 @@ func (s *TimerLayerGroupStore) GetGroupsByTeam(teamID string, opts model.GroupSe return result, err } +func (s *TimerLayerGroupStore) GetMember(groupID string, userID string) (*model.GroupMember, error) { + start := timemodule.Now() + + result, err := s.GroupStore.GetMember(groupID, userID) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetMember", success, elapsed) + } + return result, err +} + func (s *TimerLayerGroupStore) GetMemberCount(groupID string) (int64, error) { start := timemodule.Now() @@ -3723,6 +3771,22 @@ func (s *TimerLayerGroupStore) GetMemberUsersPage(groupID string, page int, perP return result, err } +func (s *TimerLayerGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int) ([]*model.User, error) { + start := timemodule.Now() + + result, err := s.GroupStore.GetNonMemberUsersPage(groupID, page, perPage) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetNonMemberUsersPage", success, elapsed) + } + return result, err +} + func (s *TimerLayerGroupStore) GroupChannelCount() (int64, error) { start := timemodule.Now() @@ -3931,6 +3995,22 @@ func (s *TimerLayerGroupStore) UpsertMember(groupID string, userID string) (*mod return result, err } +func (s *TimerLayerGroupStore) UpsertMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) { + start := timemodule.Now() + + result, err := s.GroupStore.UpsertMembers(groupID, userIDs) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.UpsertMembers", success, elapsed) + } + return result, err +} + func (s *TimerLayerJobStore) Cleanup(expiryTime int64, batchSize int) error { start := timemodule.Now() @@ -9730,6 +9810,22 @@ func (s *TimerLayerUserStore) SearchNotInChannel(teamID string, channelID string return result, err } +func (s *TimerLayerUserStore) SearchNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) { + start := timemodule.Now() + + result, err := s.UserStore.SearchNotInGroup(groupID, term, options) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("UserStore.SearchNotInGroup", success, elapsed) + } + return result, err +} + func (s *TimerLayerUserStore) SearchNotInTeam(notInTeamID string, term string, options *model.UserSearchOptions) ([]*model.User, error) { start := timemodule.Now() diff --git a/testlib/store.go b/testlib/store.go index 7d3437ef91..d16e4c3a33 100644 --- a/testlib/store.go +++ b/testlib/store.go @@ -64,6 +64,7 @@ func GetMockStoreForSetupFunctions() *mocks.Store { systemStore.On("GetByName", model.MigrationKeyAddManageSharedChannelPermissions).Return(&model.System{Name: model.MigrationKeyAddManageSharedChannelPermissions, Value: "true"}, nil) systemStore.On("GetByName", model.MigrationKeyAddManageSecureConnectionsPermissions).Return(&model.System{Name: model.MigrationKeyAddManageSecureConnectionsPermissions, Value: "true"}, nil) systemStore.On("GetByName", model.MigrationKeyAddPlaybooksPermissions).Return(&model.System{Name: model.MigrationKeyAddPlaybooksPermissions, Value: "true"}, nil) + systemStore.On("GetByName", model.MigrationKeyAddCustomUserGroupsPermissions).Return(&model.System{Name: model.MigrationKeyAddCustomUserGroupsPermissions, Value: "true"}, nil) systemStore.On("InsertIfExists", mock.AnythingOfType("*model.System")).Return(&model.System{}, nil).Once() systemStore.On("Save", mock.AnythingOfType("*model.System")).Return(nil) diff --git a/tests/group-role-has-permission.csv b/tests/group-role-has-permission.csv new file mode 100644 index 0000000000..5c9fbc37c1 --- /dev/null +++ b/tests/group-role-has-permission.csv @@ -0,0 +1,8 @@ +"let p be ""system role has permission""","let q be ""is group member""","let r be ""group role has permission""","""permission granted"" = p ∨ (q ∧ r)" +TRUE,TRUE,TRUE,TRUE +TRUE,TRUE,FALSE,TRUE +TRUE,FALSE,TRUE,TRUE +TRUE,FALSE,FALSE,TRUE +FALSE,TRUE,TRUE,TRUE +FALSE,TRUE,FALSE,FALSE +FALSE,FALSE,TRUE,FALSE \ No newline at end of file diff --git a/web/params.go b/web/params.go index 4bfc98afa9..c67b02e98d 100644 --- a/web/params.go +++ b/web/params.go @@ -87,6 +87,8 @@ type Params struct { WarnMetricId string ExportName string ExcludePolicyConstrained bool + GroupSource model.GroupSource + FilterHasMember string // Cloud InvoiceId string @@ -355,6 +357,19 @@ func ParamsFromRequest(r *http.Request) *Params { params.ExcludePolicyConstrained = val } + if val := query.Get("group_source"); val != "" { + switch val { + case "custom": + params.GroupSource = model.GroupSourceCustom + case "ldap": + params.GroupSource = model.GroupSourceLdap + default: + params.GroupSource = model.GroupSourceLdap + } + } + + params.FilterHasMember = query.Get("filter_has_member") + return params }