mattermost/server/channels/api4/group.go
Maria A Nunez 8c72083414
MM-68547: Tighten authorization on group syncable link and patch endpoints (#36316)
* MM-68547: Tighten authorization on group syncable link and patch endpoints

Adds an additional permission check on the group syncable link and patch
endpoints. Callers must hold the role-management permission for the
target team or channel (or the sysconsole groups-management permission).

Made-with: Cursor

* Linting

* MM-68547: Extend group syncable scheme_admin authorization checks

Gate any explicit scheme_admin value (in either direction) on link and
patch. Populate SchemeAdmin in the singular getGroupSyncable so that
patches that do not touch scheme_admin no longer overwrite the persisted
value. Restrict PermittedSyncableAdmins to active syncables. Start the
link upsert from the existing active row to preserve fields the caller
did not, or could not, set.

Made-with: Cursor

* MM-68547: Add store-layer regression coverage for SchemeAdmin handling

Extend testGetGroupSyncable to round-trip SchemeAdmin: true through
UpdateGroupSyncable and re-fetch, locking in that getGroupSyncable
populates the field from the persisted row.

Strengthen groupTestPermittedSyncableAdmins{Team,Channel} to assert
that DeleteGroupSyncable preserves SchemeAdmin in the persisted row
and that PermittedSyncableAdmins still excludes the row, making the
coupling between the two store changes explicit.

Made-with: Cursor

* MM-68547: Fix group details role-change dedup on remove

The roleChangeKey helper was reading team_id/channel_id from the items
in itemsToRemove, but onRemoveTeamOrChannel pushes those items with a
generic id field. The deletion of the staged role change in
handleRemovedTeamsAndChannels therefore never matched the key produced
by onChangeRoles, and a stale patchGroupSyncable was dispatched after
the unlink.

Accept either id or team_id/channel_id when computing the key. Also
extend the e2e assertion to verify the channel removal took effect
(delete_at != 0) alongside the existing scheme_admin check.

Made-with: Cursor

* MM-68547: Mirror delete_at assertion on the removed-team e2e test

The team variant of "does not update the role of a removed X" was left
asserting only on scheme_admin. Add the matching delete_at != 0 check
already present in the channel variant so both tests verify the same
user-visible contract.

Made-with: Cursor

* Skip SyncSyncableRoles if no scheme_admin
2026-05-05 09:01:56 -04:00

1590 lines
48 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/app"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/channels/web"
)
func (api *API) InitGroup() {
// GET /api/v4/groups
api.BaseRoutes.Groups.Handle("", api.APISessionRequired(getGroups)).Methods(http.MethodGet)
// POST /api/v4/groups
api.BaseRoutes.Groups.Handle("", api.APISessionRequired(createGroup)).Methods(http.MethodPost)
// GET /api/v4/groups/:group_id
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}",
api.APISessionRequired(getGroup)).Methods(http.MethodGet)
// PUT /api/v4/groups/:group_id/patch
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/patch",
api.APISessionRequired(patchGroup)).Methods(http.MethodPut)
// 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(http.MethodPost)
// 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(http.MethodDelete)
// 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(http.MethodGet)
// 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(http.MethodGet)
// 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(http.MethodPut)
// GET /api/v4/groups/:group_id/stats
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/stats",
api.APISessionRequired(getGroupStats)).Methods(http.MethodGet)
// GET /api/v4/groups/:group_id/members
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members",
api.APISessionRequired(getGroupMembers)).Methods(http.MethodGet)
// GET /api/v4/users/:user_id/groups
api.BaseRoutes.Users.Handle("/{user_id:[A-Za-z0-9]+}/groups",
api.APISessionRequired(getGroupsByUserId)).Methods(http.MethodGet)
// GET /api/v4/channels/:channel_id/groups
api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/groups",
api.APISessionRequired(getGroupsByChannel)).Methods(http.MethodGet)
// POST
api.BaseRoutes.Groups.Handle("/names",
api.APISessionRequired(getGroupsByNames)).Methods(http.MethodPost)
// GET /api/v4/teams/:team_id/groups
api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups",
api.APISessionRequired(getGroupsByTeam)).Methods(http.MethodGet)
// 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(http.MethodGet)
// DELETE /api/v4/groups/:group_id
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}",
api.APISessionRequired(deleteGroup)).Methods(http.MethodDelete)
// POST /api/v4/groups/:group_id
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/restore",
api.APISessionRequired(restoreGroup)).Methods(http.MethodPost)
// POST /api/v4/groups/:group_id/members
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members",
api.APISessionRequired(addGroupMembers)).Methods(http.MethodPost)
// DELETE /api/v4/groups/:group_id/members
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members",
api.APISessionRequired(deleteGroupMembers)).Methods(http.MethodDelete)
}
func getGroup(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext, c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
group, appErr := c.App.GetGroup(c.Params.GroupId, &model.GetGroupOpts{
IncludeMemberCount: c.Params.IncludeMemberCount,
IncludeMemberIDs: c.Params.IncludeMemberIDs,
}, restrictions)
if appErr != nil {
c.Err = appErr
return
}
if !group.AllowReference {
if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionSysconsoleReadUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementGroups)
return
}
}
if appErr := licensedAndConfiguredForGroupBySource(c.App, group.Source); appErr != nil {
appErr.Where = "Api4.getGroup"
c.Err = appErr
return
}
b, err := json.Marshal(group)
if err != nil {
c.Err = model.NewAppError("Api4.getGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(b); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func createGroup(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
var group *model.GroupWithUserIds
if err := json.NewDecoder(r.Body).Decode(&group); err != nil || group == nil {
c.SetInvalidParamWithErr("group", err)
return
}
if group.Source != model.GroupSourceCustom {
c.Err = model.NewAppError("createGroup", "app.group.crud_permission", nil, "", http.StatusBadRequest)
return
}
if appErr := licensedAndConfiguredForGroupBySource(c.App, group.Source); appErr != nil {
appErr.Where = "Api4.createGroup"
c.Err = appErr
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.StatusBadRequest)
return
}
if group.GetRemoteId() != "" {
c.Err = model.NewAppError("createGroup", "api.custom_groups.no_remote_id", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord(model.AuditEventCreateGroup, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterAuditableToAuditRec(auditRec, "group", group)
newGroup, appErr := c.App.CreateGroupWithUserIds(group)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(newGroup)
auditRec.AddEventObjectType("group")
js, err := json.Marshal(newGroup)
if err != nil {
c.Err = model.NewAppError("createGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.WriteHeader(http.StatusCreated)
if _, err := w.Write(js); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func patchGroup(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
group, appErr := c.App.GetGroup(c.Params.GroupId, nil, nil)
if appErr != nil {
c.Err = appErr
return
}
appErr = licensedAndConfiguredForGroupBySource(c.App, group.Source)
if appErr != nil {
appErr.Where = "Api4.patchGroup"
c.Err = appErr
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 err := json.NewDecoder(r.Body).Decode(&groupPatch); err != nil {
c.SetInvalidParamWithErr("group", err)
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(model.AuditEventPatchGroup, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterAuditableToAuditRec(auditRec, "group", group)
if groupPatch.AllowReference != nil && *groupPatch.AllowReference {
if groupPatch.Name == nil {
tmp := strings.ReplaceAll(strings.ToLower(group.DisplayName), " ", "-")
groupPatch.Name = &tmp
} else {
if *groupPatch.Name == model.UserNotifyAll || *groupPatch.Name == model.ChannelMentionsNotifyProp || *groupPatch.Name == model.UserNotifyHere {
c.Err = model.NewAppError("Api4.patchGroup", "api.ldap_groups.existing_reserved_name_error", nil, "", http.StatusBadRequest)
return
}
// check if a user already has this group name
user, _ := c.App.GetUserByUsername(*groupPatch.Name)
if user != nil {
c.Err = model.NewAppError("Api4.patchGroup", "api.ldap_groups.existing_user_name_error", nil, "", http.StatusBadRequest)
return
}
// check if a mentionable group already has this name
searchOpts := model.GroupSearchOpts{
FilterAllowReference: true,
}
existingGroup, _ := c.App.GetGroupByName(*groupPatch.Name, searchOpts)
if existingGroup != nil {
c.Err = model.NewAppError("Api4.patchGroup", "api.ldap_groups.existing_group_name_error", nil, "", http.StatusBadRequest)
return
}
}
}
group.Patch(&groupPatch)
group, appErr = c.App.UpdateGroup(group)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(group)
auditRec.AddEventObjectType("group")
b, err := json.Marshal(group)
if err != nil {
c.Err = model.NewAppError("Api4.patchGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
if _, err := w.Write(b); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func linkGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
c.RequireSyncableId()
if c.Err != nil {
return
}
syncableID := c.Params.SyncableId
c.RequireSyncableType()
if c.Err != nil {
return
}
syncableType := c.Params.SyncableType
body, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.createGroupSyncable", "api.io_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
auditRec := c.MakeAuditRecord(model.AuditEventLinkGroupSyncable, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "group_id", c.Params.GroupId)
model.AddEventParameterToAuditRec(auditRec, "syncable_id", syncableID)
model.AddEventParameterToAuditRec(auditRec, "syncable_type", string(syncableType))
var patch *model.GroupSyncablePatch
err = json.Unmarshal(body, &patch)
if err != nil || patch == nil {
c.SetInvalidParamWithErr(fmt.Sprintf("Group%s", syncableType), err)
return
}
model.AddEventParameterAuditableToAuditRec(auditRec, "patch", patch)
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.createGroupSyncable", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
return
}
appErr := verifyLinkUnlinkPermission(c, syncableType, syncableID)
if appErr != nil {
appErr.Where = "Api4.linkGroupSyncable"
c.Err = appErr
return
}
appErr = verifySchemeAdminAssignmentPermission(c, syncableType, syncableID, patch)
if appErr != nil {
appErr.Where = "Api4.linkGroupSyncable"
c.Err = appErr
return
}
// Upsert onto the existing row only when it is currently active so
// unspecified fields are preserved. A fresh link, or a re-link of a
// soft-deleted row, starts from a zero-value struct so that fields
// the caller did not (or was not authorized to) set are not carried
// over from the previous incarnation. The downstream upsert clears
// DeleteAt when re-activating.
existing, appErr := c.App.GetGroupSyncable(c.Params.GroupId, syncableID, syncableType)
if appErr != nil && appErr.StatusCode != http.StatusNotFound {
appErr.Where = "Api4.linkGroupSyncable"
c.Err = appErr
return
}
var groupSyncable *model.GroupSyncable
if existing != nil && existing.DeleteAt == 0 {
groupSyncable = existing
} else {
groupSyncable = &model.GroupSyncable{
GroupId: c.Params.GroupId,
SyncableId: syncableID,
Type: syncableType,
}
}
groupSyncable.Patch(patch)
groupSyncable, appErr = c.App.UpsertGroupSyncable(groupSyncable)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(groupSyncable)
auditRec.AddEventObjectType("group_syncable")
syncRoles := patch.SchemeAdmin != nil
c.App.Srv().Go(func() {
c.App.SyncRolesAndMembership(c.AppContext, syncableID, syncableType, c.Params.GroupId, syncRoles)
})
w.WriteHeader(http.StatusCreated)
b, err := json.Marshal(groupSyncable)
if err != nil {
c.Err = model.NewAppError("Api4.createGroupSyncable", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
if _, err := w.Write(b); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
c.RequireSyncableId()
if c.Err != nil {
return
}
syncableID := c.Params.SyncableId
c.RequireSyncableType()
if c.Err != nil {
return
}
syncableType := c.Params.SyncableType
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.getGroupSyncable", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
groupSyncable, appErr := c.App.GetGroupSyncable(c.Params.GroupId, syncableID, syncableType)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(groupSyncable)
if err != nil {
c.Err = model.NewAppError("Api4.getGroupSyncable", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(b); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getGroupSyncables(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
c.RequireSyncableType()
if c.Err != nil {
return
}
syncableType := c.Params.SyncableType
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.getGroupSyncables", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementGroups)
return
}
groupSyncables, appErr := c.App.GetGroupSyncables(c.Params.GroupId, syncableType)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(groupSyncables)
if err != nil {
c.Err = model.NewAppError("Api4.getGroupSyncables", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(b); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func patchGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
c.RequireSyncableId()
if c.Err != nil {
return
}
syncableID := c.Params.SyncableId
c.RequireSyncableType()
if c.Err != nil {
return
}
syncableType := c.Params.SyncableType
body, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.patchGroupSyncable", "api.io_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
auditRec := c.MakeAuditRecord(model.AuditEventPatchGroupSyncable, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "group_id", c.Params.GroupId)
model.AddEventParameterToAuditRec(auditRec, "old_syncable_id", syncableID)
model.AddEventParameterToAuditRec(auditRec, "old_syncable_type", string(syncableType))
var patch *model.GroupSyncablePatch
err = json.Unmarshal(body, &patch)
if err != nil || patch == nil {
c.SetInvalidParamWithErr(fmt.Sprintf("Group[%s]Patch", syncableType), err)
return
}
model.AddEventParameterAuditableToAuditRec(auditRec, "patch", patch)
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.patchGroupSyncable", "api.ldap_groups.license_error", nil, "",
http.StatusForbidden)
return
}
appErr := verifyLinkUnlinkPermission(c, syncableType, syncableID)
if appErr != nil {
appErr.Where = "Api4.patchGroupSyncable"
c.Err = appErr
return
}
appErr = verifySchemeAdminAssignmentPermission(c, syncableType, syncableID, patch)
if appErr != nil {
appErr.Where = "Api4.patchGroupSyncable"
c.Err = appErr
return
}
groupSyncable, appErr := c.App.GetGroupSyncable(c.Params.GroupId, syncableID, syncableType)
if appErr != nil {
c.Err = appErr
return
}
groupSyncable.Patch(patch)
groupSyncable, appErr = c.App.UpdateGroupSyncable(groupSyncable)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(groupSyncable)
auditRec.AddEventObjectType("group_syncable")
syncRoles := patch.SchemeAdmin != nil
c.App.Srv().Go(func() {
c.App.SyncRolesAndMembership(c.AppContext, syncableID, syncableType, c.Params.GroupId, syncRoles)
})
b, err := json.Marshal(groupSyncable)
if err != nil {
c.Err = model.NewAppError("Api4.patchGroupSyncable", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
if _, err := w.Write(b); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func unlinkGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
c.RequireSyncableId()
if c.Err != nil {
return
}
syncableID := c.Params.SyncableId
c.RequireSyncableType()
if c.Err != nil {
return
}
syncableType := c.Params.SyncableType
auditRec := c.MakeAuditRecord(model.AuditEventUnlinkGroupSyncable, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "group_id", c.Params.GroupId)
model.AddEventParameterToAuditRec(auditRec, "syncable_id", syncableID)
model.AddEventParameterToAuditRec(auditRec, "syncable_type", string(syncableType))
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.unlinkGroupSyncable", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
return
}
appErr := verifyLinkUnlinkPermission(c, syncableType, syncableID)
if appErr != nil {
appErr.Where = "Api4.unlinkGroupSyncable"
c.Err = appErr
return
}
_, appErr = c.App.DeleteGroupSyncable(c.Params.GroupId, syncableID, syncableType)
if appErr != nil {
c.Err = appErr
return
}
c.App.Srv().Go(func() {
c.App.RemoveMembershipsFromUnlinkedSyncable(c.AppContext, syncableID, syncableType)
})
auditRec.Success()
ReturnStatusOK(w)
}
func verifyLinkUnlinkPermission(c *Context, syncableType model.GroupSyncableType, syncableID string) *model.AppError {
group, appErr := c.App.GetGroup(c.Params.GroupId, nil, nil)
if appErr != nil {
return appErr
}
if !group.IsSyncable() {
return model.NewAppError("Api4.linkGroupSyncable", "app.group.crud_permission", nil, "", http.StatusBadRequest)
}
// If AllowReference is disabled, limit who can link the group.
// This voids leaking the list of group members.
// See https://mattermost.atlassian.net/browse/MM-55314 for more details.
if !group.AllowReference {
if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionSysconsoleReadUserManagementGroups) {
return model.MakePermissionError(c.AppContext.Session(), []*model.Permission{model.PermissionSysconsoleReadUserManagementGroups})
}
}
switch syncableType {
case model.GroupSyncableTypeTeam:
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), syncableID, model.PermissionInviteUser) &&
!c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementGroups) {
return model.MakePermissionError(c.AppContext.Session(), []*model.Permission{model.PermissionInviteUser})
}
case model.GroupSyncableTypeChannel:
channel, appErr := c.App.GetChannel(c.AppContext, syncableID)
if appErr != nil {
return appErr
}
// If it's the first time that the syncable gets linked to the team (i.e. no current sync to the team or to a team's channel),
// check that the user has the permission to manage the team.
_, appErr = c.App.GetGroupSyncable(c.Params.GroupId, channel.TeamId, model.GroupSyncableTypeTeam)
if appErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(appErr, &nfErr):
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), syncableID, model.PermissionInviteUser) &&
!c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementGroups) {
return model.MakePermissionError(c.AppContext.Session(), []*model.Permission{model.PermissionInviteUser})
}
default:
return appErr
}
}
var permission *model.Permission
if channel.Type == model.ChannelTypePrivate {
permission = model.PermissionManagePrivateChannelMembers
} else {
permission = model.PermissionManagePublicChannelMembers
}
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), syncableID, permission); !ok {
return model.MakePermissionError(c.AppContext.Session(), []*model.Permission{permission})
}
}
return nil
}
// verifySchemeAdminAssignmentPermission requires the caller to hold the
// role-management permission for the target syncable
// (manage_team_roles / manage_channel_roles), or the sysconsole groups
// write permission, before an explicit SchemeAdmin value in the patch is
// accepted. A nil patch.SchemeAdmin is a no-op.
func verifySchemeAdminAssignmentPermission(c *Context, syncableType model.GroupSyncableType, syncableID string, patch *model.GroupSyncablePatch) *model.AppError {
if patch == nil || patch.SchemeAdmin == nil {
return nil
}
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementGroups) {
return nil
}
switch syncableType {
case model.GroupSyncableTypeTeam:
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), syncableID, model.PermissionManageTeamRoles) {
return model.MakePermissionError(c.AppContext.Session(), []*model.Permission{model.PermissionManageTeamRoles})
}
case model.GroupSyncableTypeChannel:
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), syncableID, model.PermissionManageChannelRoles); !ok {
return model.MakePermissionError(c.AppContext.Session(), []*model.Permission{model.PermissionManageChannelRoles})
}
}
return nil
}
func getGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
appErr := hasPermissionToReadGroupMembers(c, c.Params.GroupId)
if appErr != nil {
appErr.Where = "Api4.getGroupMembers"
c.Err = appErr
return
}
restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext, c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
members, count, appErr := c.App.GetGroupMemberUsersPage(c.Params.GroupId, c.Params.Page, c.Params.PerPage, restrictions)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(model.GroupMemberList{
Members: members,
Count: count,
})
if err != nil {
c.Err = model.NewAppError("Api4.getGroupMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(b); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getGroupStats(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.getGroupStats", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementGroups)
return
}
groupID := c.Params.GroupId
count, appErr := c.App.GetGroupMemberCount(groupID, nil)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(model.GroupStats{
GroupID: groupID,
TotalMemberCount: count,
})
if err != nil {
c.Err = model.NewAppError("Api4.getGroupStats", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(b); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getGroupsByUserId(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireUserId()
if c.Err != nil {
return
}
if c.AppContext.Session().UserId != c.Params.UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.getGroupsByUserId", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
return
}
filterAllowReference := !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups)
opts := model.GroupSearchOpts{
FilterAllowReference: filterAllowReference,
}
groups, appErr := c.App.GetGroupsByUserId(c.Params.UserId, opts)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(groups)
if err != nil {
c.Err = model.NewAppError("Api4.getGroupsByUserId", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(b); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getGroupsByChannel(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireChannelId()
if c.Err != nil {
return
}
b, appErr := getGroupsByChannelCommon(c, r)
if appErr != nil {
c.Err = appErr
return
}
if _, err := w.Write(b); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getGroupsByNames(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
groupNames, err := model.SortedArrayFromJSON(r.Body)
if err != nil {
c.Err = model.NewAppError("getGroupsByNames", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
return
} else if len(groupNames) == 0 {
if _, err = w.Write([]byte("[]")); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
return
}
filterAllowReference := !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups)
opts := model.GroupSearchOpts{
FilterAllowReference: filterAllowReference,
}
groups, appErr := c.App.GetGroupsByNames(groupNames, opts)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(groups)
if err != nil {
c.Err = model.NewAppError("getGroupsByNames", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(js); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getGroupsByTeam(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
b, appError := getGroupsByTeamCommon(c, r)
if appError != nil {
c.Err = appError
return
}
if _, err := w.Write(b); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getGroupsByTeamCommon(c *Context, r *http.Request) ([]byte, *model.AppError) {
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
return nil, model.NewAppError("Api4.getGroupsByTeam", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionListTeamChannels) {
return nil, model.MakePermissionError(c.AppContext.Session(), []*model.Permission{model.PermissionListTeamChannels})
}
filterAllowReference := c.Params.FilterAllowReference || !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups)
opts := model.GroupSearchOpts{
Q: c.Params.Q,
IncludeMemberCount: c.Params.IncludeMemberCount,
FilterAllowReference: filterAllowReference,
}
if c.Params.Paginate == nil || *c.Params.Paginate {
opts.PageOpts = &model.PageOpts{Page: c.Params.Page, PerPage: c.Params.PerPage}
}
groups, totalCount, appErr := c.App.GetGroupsByTeam(c.Params.TeamId, opts)
if appErr != nil {
return nil, appErr
}
b, err := json.Marshal(struct {
Groups []*model.GroupWithSchemeAdmin `json:"groups"`
Count int `json:"total_group_count"`
}{
Groups: groups,
Count: totalCount,
})
if err != nil {
return nil, model.NewAppError("Api4.getGroupsByTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return b, nil
}
func getGroupsByChannelCommon(c *Context, r *http.Request) ([]byte, *model.AppError) {
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
return nil, model.NewAppError("Api4.getGroupsByChannel", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
}
channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if appErr != nil {
return nil, appErr
}
var permission *model.Permission
if channel.Type == model.ChannelTypePrivate {
permission = model.PermissionReadPrivateChannelGroups
} else {
permission = model.PermissionReadPublicChannelGroups
}
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, permission); !ok {
return nil, model.MakePermissionError(c.AppContext.Session(), []*model.Permission{permission})
}
filterAllowReference := c.Params.FilterAllowReference || !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups)
opts := model.GroupSearchOpts{
Q: c.Params.Q,
IncludeMemberCount: c.Params.IncludeMemberCount,
FilterAllowReference: filterAllowReference,
}
if c.Params.Paginate == nil || *c.Params.Paginate {
opts.PageOpts = &model.PageOpts{Page: c.Params.Page, PerPage: c.Params.PerPage}
}
groups, totalCount, appErr := c.App.GetGroupsByChannel(c.Params.ChannelId, opts)
if appErr != nil {
return nil, appErr
}
b, err := json.Marshal(struct {
Groups []*model.GroupWithSchemeAdmin `json:"groups"`
Count int `json:"total_group_count"`
}{
Groups: groups,
Count: totalCount,
})
if err != nil {
return nil, model.NewAppError("Api4.getGroupsByChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return b, nil
}
func getGroupsAssociatedToChannelsByTeam(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.getGroupsAssociatedToChannelsByTeam", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionListTeamChannels) {
c.Err = model.MakePermissionError(c.AppContext.Session(), []*model.Permission{model.PermissionListTeamChannels})
return
}
filterAllowReference := c.Params.FilterAllowReference || !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups)
opts := model.GroupSearchOpts{
Q: c.Params.Q,
IncludeMemberCount: c.Params.IncludeMemberCount,
FilterAllowReference: filterAllowReference,
}
if c.Params.Paginate == nil || *c.Params.Paginate {
opts.PageOpts = &model.PageOpts{Page: c.Params.Page, PerPage: c.Params.PerPage}
}
groupsAssociatedByChannelID, appErr := c.App.GetGroupsAssociatedToChannelsByTeam(c.Params.TeamId, opts)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(struct {
GroupsAssociatedToChannels map[string][]*model.GroupWithSchemeAdmin `json:"groups"`
}{
GroupsAssociatedToChannels: groupsAssociatedByChannelID,
})
if err != nil {
c.Err = model.NewAppError("Api4.getGroupsAssociatedToChannelsByTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(b); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
var teamID, NotAssociatedToChannelID, ChannelIDForMemberCount string
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
source := c.Params.GroupSource
onlySyncableSources := r.URL.Query().Get("only_syncable_sources") == "true"
if id := c.Params.NotAssociatedToTeam; model.IsValidId(id) {
teamID = id
}
if id := c.Params.NotAssociatedToChannel; model.IsValidId(id) {
NotAssociatedToChannelID = id
}
if id := c.Params.IncludeChannelMemberCount; model.IsValidId(id) {
ChannelIDForMemberCount = id
}
// If they specify the group_source as custom when the feature is disabled, throw an error
if appErr := licensedAndConfiguredForGroupBySource(c.App, source); appErr != nil {
appErr.Where = "Api4.getGroups"
c.Err = appErr
return
}
// If they don't specify a source and custom groups are disabled, ensure they only get the other sources
if !*c.App.Config().ServiceSettings.EnableCustomGroups {
onlySyncableSources = true
}
includeTimezones := r.URL.Query().Get("include_timezones") == "true"
// Include archived groups
includeArchived := r.URL.Query().Get("include_archived") == "true"
filterAllowReference := c.Params.FilterAllowReference || !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups)
opts := model.GroupSearchOpts{
Q: c.Params.Q,
IncludeMemberCount: c.Params.IncludeMemberCount,
FilterAllowReference: filterAllowReference,
FilterArchived: c.Params.FilterArchived,
FilterParentTeamPermitted: c.Params.FilterParentTeamPermitted,
Source: source,
FilterHasMember: c.Params.FilterHasMember,
IncludeTimezones: includeTimezones,
IncludeMemberIDs: c.Params.IncludeMemberIDs,
IncludeArchived: includeArchived,
OnlySyncableSources: onlySyncableSources,
}
if teamID != "" {
_, appErr := c.App.GetTeam(teamID)
if appErr != nil {
c.Err = appErr
return
}
opts.NotAssociatedToTeam = teamID
}
if NotAssociatedToChannelID != "" {
channel, appErr := c.App.GetChannel(c.AppContext, NotAssociatedToChannelID)
if appErr != nil {
c.Err = appErr
return
}
var permission *model.Permission
if channel.Type == model.ChannelTypePrivate {
permission = model.PermissionManagePrivateChannelMembers
} else {
permission = model.PermissionManagePublicChannelMembers
}
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), NotAssociatedToChannelID, permission); !ok {
c.SetPermissionError(permission)
return
}
opts.NotAssociatedToChannel = NotAssociatedToChannelID
}
if ChannelIDForMemberCount != "" {
channel, appErr := c.App.GetChannel(c.AppContext, ChannelIDForMemberCount)
if appErr != nil {
c.Err = appErr
return
}
var permission *model.Permission
if channel.Type == model.ChannelTypePrivate {
permission = model.PermissionManagePrivateChannelMembers
} else {
permission = model.PermissionManagePublicChannelMembers
}
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), ChannelIDForMemberCount, permission); !ok {
c.SetPermissionError(permission)
return
}
opts.IncludeChannelMemberCount = ChannelIDForMemberCount
}
sinceString := r.URL.Query().Get("since")
if sinceString != "" {
since, err := strconv.ParseInt(sinceString, 10, 64)
if err != nil {
c.SetInvalidParamWithErr("since", err)
return
}
opts.Since = since
}
restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext, c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
var (
groups = []*model.Group{}
canSee = true
)
if opts.FilterHasMember != "" {
canSee, appErr = c.App.UserCanSeeOtherUser(c.AppContext, c.AppContext.Session().UserId, opts.FilterHasMember)
if appErr != nil {
c.Err = appErr
return
}
}
if canSee {
groups, appErr = c.App.GetGroups(c.Params.Page, c.Params.PerPage, opts, restrictions)
if appErr != nil {
c.Err = appErr
return
}
}
var (
b []byte
err error
)
if c.Params.IncludeTotalCount {
totalCount, cerr := c.App.Srv().Store().Group().GroupCount()
if cerr != nil {
c.Err = model.NewAppError("Api4.getGroups", "api.custom_groups.count_err", nil, "", http.StatusInternalServerError).Wrap(cerr)
return
}
gwc := &model.GroupsWithCount{
Groups: groups,
TotalCount: totalCount,
}
b, err = json.Marshal(gwc)
} else {
b, err = json.Marshal(groups)
}
if err != nil {
c.Err = model.NewAppError("Api4.getGroups", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(b); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteGroup(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
group, err := c.App.GetGroup(c.Params.GroupId, nil, 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.StatusBadRequest)
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(model.AuditEventDeleteGroup, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "group_id", c.Params.GroupId)
group, err = c.App.DeleteGroup(c.Params.GroupId)
if err != nil {
c.Err = err
return
}
b, jsonErr := json.Marshal(group)
if jsonErr != nil {
c.Err = model.NewAppError("Api4.deleteGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
return
}
auditRec.Success()
if _, err := w.Write(b); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func restoreGroup(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
group, err := c.App.GetGroup(c.Params.GroupId, nil, nil)
if err != nil {
c.Err = err
return
}
if group.Source != model.GroupSourceCustom {
c.Err = model.NewAppError("Api4.restoreGroup", "app.group.crud_permission", nil, "", http.StatusNotImplemented)
return
}
if lcErr := licensedAndConfiguredForGroupBySource(c.App, model.GroupSourceCustom); lcErr != nil {
lcErr.Where = "Api4.restoreGroup"
c.Err = lcErr
return
}
if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionRestoreCustomGroup) {
c.SetPermissionError(model.PermissionRestoreCustomGroup)
return
}
auditRec := c.MakeAuditRecord(model.AuditEventRestoreGroup, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "group_id", c.Params.GroupId)
restoredGroup, err := c.App.RestoreGroup(c.Params.GroupId)
if err != nil {
c.Err = err
return
}
b, jsonErr := json.Marshal(restoredGroup)
if jsonErr != nil {
c.Err = model.NewAppError("Api4.restoreGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
return
}
auditRec.Success()
if _, err := w.Write(b); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func addGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
group, appErr := c.App.GetGroup(c.Params.GroupId, nil, nil)
if appErr != nil {
c.Err = appErr
return
}
if group.Source != model.GroupSourceCustom {
c.Err = model.NewAppError("Api4.addGroupMembers", "app.group.crud_permission", nil, "", http.StatusBadRequest)
return
}
appErr = licensedAndConfiguredForGroupBySource(c.App, model.GroupSourceCustom)
if appErr != nil {
appErr.Where = "Api4.addGroupMembers"
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionManageCustomGroupMembers) {
c.SetPermissionError(model.PermissionManageCustomGroupMembers)
return
}
var newMembers *model.GroupModifyMembers
if err := json.NewDecoder(r.Body).Decode(&newMembers); err != nil || newMembers == nil {
c.SetInvalidParamWithErr("addGroupMembers", err)
return
}
for _, userID := range newMembers.UserIds {
if !model.IsValidId(userID) {
c.SetInvalidParamWithDetails("user_id", fmt.Sprintf("UserID %s is invalid", userID))
return
}
}
auditRec := c.MakeAuditRecord(model.AuditEventAddGroupMembers, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "addGroupMembers_userids", newMembers.UserIds)
members, appErr := c.App.UpsertGroupMembers(c.Params.GroupId, newMembers.UserIds)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(members)
if err != nil {
c.Err = model.NewAppError("Api4.addGroupMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
if _, err := w.Write(b); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
group, appErr := c.App.GetGroup(c.Params.GroupId, nil, nil)
if appErr != nil {
c.Err = appErr
return
}
if group.Source != model.GroupSourceCustom {
c.Err = model.NewAppError("Api4.deleteGroupMembers", "app.group.crud_permission", nil, "", http.StatusBadRequest)
return
}
appErr = licensedAndConfiguredForGroupBySource(c.App, model.GroupSourceCustom)
if appErr != nil {
appErr.Where = "Api4.deleteGroupMembers"
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionManageCustomGroupMembers) {
c.SetPermissionError(model.PermissionManageCustomGroupMembers)
return
}
var deleteBody *model.GroupModifyMembers
if err := json.NewDecoder(r.Body).Decode(&deleteBody); err != nil || deleteBody == nil {
c.SetInvalidParamWithErr("deleteGroupMembers", err)
return
}
for _, userID := range deleteBody.UserIds {
if !model.IsValidId(userID) {
c.SetInvalidParamWithDetails("user_id", fmt.Sprintf("UserID %s is invalid", userID))
return
}
}
auditRec := c.MakeAuditRecord(model.AuditEventDeleteGroupMembers, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "deleteGroupMembers_userids", deleteBody.UserIds)
members, appErr := c.App.DeleteGroupMembers(c.Params.GroupId, deleteBody.UserIds)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(members)
if err != nil {
c.Err = model.NewAppError("Api4.addGroupMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
if _, err := w.Write(b); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
// hasPermissionToReadGroupMembers check if a user has the permission to read the list of members of a given team.
func hasPermissionToReadGroupMembers(c *web.Context, groupID string) *model.AppError {
group, err := c.App.GetGroup(groupID, nil, nil)
if err != nil {
return err
}
if lcErr := licensedAndConfiguredForGroupBySource(c.App, group.Source); lcErr != nil {
return lcErr
}
if group.IsSyncable() && !group.AllowReference {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) {
return model.MakePermissionError(c.AppContext.Session(), []*model.Permission{model.PermissionSysconsoleReadUserManagementGroups})
}
}
return nil
}
// 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"
func licensedAndConfiguredForGroupBySource(app *app.App, source model.GroupSource) *model.AppError {
lic := app.Srv().License()
if lic == nil {
return model.NewAppError("", "api.license_error", nil, "", http.StatusForbidden)
}
if source == model.GroupSourceLdap && !*lic.Features.LDAPGroups {
return model.NewAppError("", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
}
if strings.HasPrefix(string(source), string(model.GroupSourcePluginPrefix)) && !*lic.Features.LDAPGroups {
return model.NewAppError("", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
}
if source == model.GroupSourceCustom && !model.MinimumProfessionalLicense(lic) {
return model.NewAppError("", "api.custom_groups.license_error", nil, "", http.StatusBadRequest)
}
if source == model.GroupSourceCustom && !*app.Config().ServiceSettings.EnableCustomGroups {
return model.NewAppError("", "api.custom_groups.feature_disabled", nil, "", http.StatusBadRequest)
}
return nil
}