diff --git a/server/legalhold/legal_hold.go b/server/legalhold/legal_hold.go index 062b497..3f44b24 100644 --- a/server/legalhold/legal_hold.go +++ b/server/legalhold/legal_hold.go @@ -87,7 +87,7 @@ func (ex *Execution) GetChannels() error { return appErr } - channelIDs, err := ex.store.GetChannelIDsForUserDuring(userID, ex.ExecutionStartTime, ex.ExecutionEndTime) + channelIDs, err := ex.store.GetChannelIDsForUserDuring(userID, ex.ExecutionStartTime, ex.ExecutionEndTime, ex.LegalHold.ExcludePublicChannels) if err != nil { return err } diff --git a/server/model/legal_hold.go b/server/model/legal_hold.go index 833a203..698416f 100644 --- a/server/model/legal_hold.go +++ b/server/model/legal_hold.go @@ -11,17 +11,18 @@ import ( // LegalHold represents one legal hold. type LegalHold struct { - ID string `json:"id"` - Name string `json:"name"` - DisplayName string `json:"display_name"` - CreateAt int64 `json:"create_at"` - UpdateAt int64 `json:"update_at"` - UserIDs []string `json:"user_ids"` - StartsAt int64 `json:"starts_at"` - EndsAt int64 `json:"ends_at"` - LastExecutionEndedAt int64 `json:"last_execution_ended_at"` - ExecutionLength int64 `json:"execution_length"` - Secret string `json:"secret"` + ID string `json:"id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + UserIDs []string `json:"user_ids"` + StartsAt int64 `json:"starts_at"` + EndsAt int64 `json:"ends_at"` + ExcludePublicChannels bool `json:"exclude_public_channels"` + LastExecutionEndedAt int64 `json:"last_execution_ended_at"` + ExecutionLength int64 `json:"execution_length"` + Secret string `json:"secret"` } // DeepCopy creates a deep copy of the LegalHold. @@ -31,16 +32,17 @@ func (lh *LegalHold) DeepCopy() LegalHold { } newLegalHold := LegalHold{ - ID: lh.ID, - Name: lh.Name, - DisplayName: lh.DisplayName, - CreateAt: lh.CreateAt, - UpdateAt: lh.UpdateAt, - StartsAt: lh.StartsAt, - EndsAt: lh.EndsAt, - LastExecutionEndedAt: lh.LastExecutionEndedAt, - ExecutionLength: lh.ExecutionLength, - Secret: lh.Secret, + ID: lh.ID, + Name: lh.Name, + DisplayName: lh.DisplayName, + CreateAt: lh.CreateAt, + UpdateAt: lh.UpdateAt, + StartsAt: lh.StartsAt, + EndsAt: lh.EndsAt, + ExcludePublicChannels: lh.ExcludePublicChannels, + LastExecutionEndedAt: lh.LastExecutionEndedAt, + ExecutionLength: lh.ExecutionLength, + Secret: lh.Secret, } if len(lh.UserIDs) > 0 { @@ -131,34 +133,37 @@ func (lh *LegalHold) BasePath() string { // CreateLegalHold holds the data that is specified in the API call to create a LegalHold. type CreateLegalHold struct { - Name string `json:"name"` - DisplayName string `json:"display_name"` - UserIDs []string `json:"user_ids"` - StartsAt int64 `json:"starts_at"` - EndsAt int64 `json:"ends_at"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + UserIDs []string `json:"user_ids"` + StartsAt int64 `json:"starts_at"` + EndsAt int64 `json:"ends_at"` + ExcludePublicChannels bool `json:"exclude_public_channels"` } // NewLegalHoldFromCreate creates and populates a new LegalHold instance from // the provided CreateLegalHold instance. func NewLegalHoldFromCreate(lhc CreateLegalHold) LegalHold { return LegalHold{ - ID: mattermostModel.NewId(), - Name: lhc.Name, - DisplayName: lhc.DisplayName, - UserIDs: lhc.UserIDs, - StartsAt: lhc.StartsAt, - EndsAt: lhc.EndsAt, - LastExecutionEndedAt: 0, - ExecutionLength: 86400000, + ID: mattermostModel.NewId(), + Name: lhc.Name, + DisplayName: lhc.DisplayName, + UserIDs: lhc.UserIDs, + StartsAt: lhc.StartsAt, + EndsAt: lhc.EndsAt, + ExcludePublicChannels: lhc.ExcludePublicChannels, + LastExecutionEndedAt: 0, + ExecutionLength: 86400000, } } // UpdateLegalHold holds the data that is specified in the API call to update a LegalHold. type UpdateLegalHold struct { - ID string `json:"id"` - DisplayName string `json:"display_name"` - UserIDs []string `json:"user_ids"` - EndsAt int64 `json:"ends_at"` + ID string `json:"id"` + DisplayName string `json:"display_name"` + UserIDs []string `json:"user_ids"` + ExcludePublicChannels bool `json:"exclude_public_channels"` + EndsAt int64 `json:"ends_at"` } func (ulh UpdateLegalHold) IsValid() error { @@ -191,4 +196,5 @@ func (lh *LegalHold) ApplyUpdates(updates UpdateLegalHold) { lh.DisplayName = updates.DisplayName lh.UserIDs = updates.UserIDs lh.EndsAt = updates.EndsAt + lh.ExcludePublicChannels = updates.ExcludePublicChannels } diff --git a/server/store/sqlstore/legal_hold.go b/server/store/sqlstore/legal_hold.go index de37180..6257389 100644 --- a/server/store/sqlstore/legal_hold.go +++ b/server/store/sqlstore/legal_hold.go @@ -110,7 +110,7 @@ func (ss SQLStore) GetPostsBatch(channelID string, endTime int64, cursor model.L // GetChannelIDsForUserDuring gets the channel IDs for all channels that the user indicated by userID is // a member of during the time period from (and including) the startTime up until (but not including) the // endTime. -func (ss SQLStore) GetChannelIDsForUserDuring(userID string, startTime int64, endTime int64) ([]string, error) { +func (ss SQLStore) GetChannelIDsForUserDuring(userID string, startTime int64, endTime int64, excludePublic bool) ([]string, error) { query := ss.replicaBuilder. Select("distinct(cmh.channelid)"). From("channelmemberhistory as cmh"). @@ -118,6 +118,12 @@ func (ss SQLStore) GetChannelIDsForUserDuring(userID string, startTime int64, en Where(sq.Or{sq.Eq{"cmh.leavetime": nil}, sq.GtOrEq{"cmh.leavetime": startTime}}). Where(sq.Eq{"cmh.userid": userID}) + // Exclude all public channels from the results + if excludePublic { + query = query.Join("channels on cmh.channelid = channels.id"). + Where(sq.NotEq{"channels.type": mattermostModel.ChannelTypeOpen}) + } + rows, err := query.Query() if err != nil { ss.logger.Error("error fetching channels for user during time period", "err", err) diff --git a/server/store/sqlstore/legal_hold_test.go b/server/store/sqlstore/legal_hold_test.go index 2f8469a..d922827 100644 --- a/server/store/sqlstore/legal_hold_test.go +++ b/server/store/sqlstore/legal_hold_test.go @@ -19,7 +19,7 @@ func TestSQLStore_GetPostsBatch(t *testing.T) { // Test with an open channel first // create an open channel - channel, err := th.CreateChannel("stale-test", th.User1.Id, th.Team1.Id) + channel, err := th.CreateOpenChannel("stale-test", th.User1.Id, th.Team1.Id) require.NoError(t, err) var posts []*mattermostModel.Post @@ -119,7 +119,7 @@ func TestSQLStore_LegalHold_GetChannelIDsForUserDuring(t *testing.T) { require.NoError(t, th.mmStore.ChannelMemberHistory().LogLeaveEvent(th.User1.Id, channels[9].Id, endTwo-1000)) // Check channel IDs for first window. - firstWindowChannelIDs, err := th.Store.GetChannelIDsForUserDuring(th.User1.Id, startOne, endOne) + firstWindowChannelIDs, err := th.Store.GetChannelIDsForUserDuring(th.User1.Id, startOne, endOne, false) expectedOne := []string{ channels[1].Id, channels[2].Id, @@ -133,7 +133,7 @@ func TestSQLStore_LegalHold_GetChannelIDsForUserDuring(t *testing.T) { require.ElementsMatch(t, firstWindowChannelIDs, expectedOne) // Check channel IDs for second window. - secondWindowChannelIDs, err := th.Store.GetChannelIDsForUserDuring(th.User1.Id, startTwo, endTwo) + secondWindowChannelIDs, err := th.Store.GetChannelIDsForUserDuring(th.User1.Id, startTwo, endTwo, false) expectedTwo := []string{ channels[3].Id, channels[4].Id, @@ -145,6 +145,34 @@ func TestSQLStore_LegalHold_GetChannelIDsForUserDuring(t *testing.T) { require.ElementsMatch(t, secondWindowChannelIDs, expectedTwo) } +func TestLegalHold_GetChannelIDsForUserDuring_ExcludePublic(t *testing.T) { + th := SetupHelper(t).SetupBasic(t) + defer th.TearDown(t) + + timeReference := mattermostModel.GetMillis() + start := timeReference + 1000000 + end := start + 10000 + + openChannel, err := th.CreateChannel("public-channel", th.User1.Id, th.Team1.Id, mattermostModel.ChannelTypeOpen) + require.NoError(t, err) + privateChannel, err := th.CreateChannel("private-channel", th.User1.Id, th.Team1.Id, mattermostModel.ChannelTypePrivate) + require.NoError(t, err) + dmChannel, err := th.CreateDirectMessageChannel(th.User1, th.User2) + require.NoError(t, err) + groupDM, err := th.CreateChannel("group-dm", th.User1.Id, th.Team1.Id, mattermostModel.ChannelTypeGroup) + require.NoError(t, err) + + require.NoError(t, th.mmStore.ChannelMemberHistory().LogJoinEvent(th.User1.Id, openChannel.Id, start+1000)) + require.NoError(t, th.mmStore.ChannelMemberHistory().LogJoinEvent(th.User1.Id, privateChannel.Id, start+1000)) + require.NoError(t, th.mmStore.ChannelMemberHistory().LogJoinEvent(th.User1.Id, groupDM.Id, start+1000)) + require.NoError(t, th.mmStore.ChannelMemberHistory().LogJoinEvent(th.User1.Id, dmChannel.Id, start+1000)) + + // Check channel IDs + channelIDs, err := th.Store.GetChannelIDsForUserDuring(th.User1.Id, start, end, true) + require.NoError(t, err) + require.ElementsMatch(t, channelIDs, []string{privateChannel.Id, dmChannel.Id, groupDM.Id}) +} + func TestSQLStore_LegalHold_GetFileInfosByIDs(t *testing.T) { // TODO: Implement me! _ = t diff --git a/server/store/sqlstore/testhelper.go b/server/store/sqlstore/testhelper.go index 1d97fb5..30801a3 100644 --- a/server/store/sqlstore/testhelper.go +++ b/server/store/sqlstore/testhelper.go @@ -131,11 +131,15 @@ func (th *TestHelper) CreateTeams(num int, namePrefix string) ([]*model.Team, er return teams, nil } -func (th *TestHelper) CreateChannel(name string, userID string, teamID string) (*model.Channel, error) { +func (th *TestHelper) CreateOpenChannel(name string, userID string, teamID string) (*model.Channel, error) { + return th.CreateChannel(name, userID, teamID, model.ChannelTypeOpen) +} + +func (th *TestHelper) CreateChannel(name, userID, teamID string, channelType model.ChannelType) (*model.Channel, error) { channel := &model.Channel{ Name: name, DisplayName: name, - Type: model.ChannelTypeOpen, + Type: channelType, CreatorId: userID, TeamId: teamID, } diff --git a/webapp/src/components/create_legal_hold_form.scss b/webapp/src/components/create_legal_hold_form.scss index 9471051..5b3edce 100644 --- a/webapp/src/components/create_legal_hold_form.scss +++ b/webapp/src/components/create_legal_hold_form.scss @@ -2,5 +2,8 @@ border: none !important; } -.create-legal-hold-container { +.create-legal-hold-checkbox { + float: left; + width: 1em; + height: 1em; } diff --git a/webapp/src/components/create_legal_hold_form.tsx b/webapp/src/components/create_legal_hold_form.tsx index 9e6f72f..2f56333 100644 --- a/webapp/src/components/create_legal_hold_form.tsx +++ b/webapp/src/components/create_legal_hold_form.tsx @@ -21,6 +21,7 @@ const CreateLegalHoldForm = (props: CreateLegalHoldFormProps) => { const [startsAt, setStartsAt] = useState(''); const [endsAt, setEndsAt] = useState(''); const [saving, setSaving] = useState(false); + const [excludePublicChannels, setExcludePublicChannels] = useState(false); const [serverError, setServerError] = useState(''); const displayNameChanged = (e: React.ChangeEvent) => { @@ -35,6 +36,10 @@ const CreateLegalHoldForm = (props: CreateLegalHoldFormProps) => { setEndsAt(e.target.value); }; + const excludePublicChannelsChanged: (e: React.ChangeEvent) => void = (e) => { + setExcludePublicChannels(e.target.checked); + }; + const resetForm = () => { setDisplayName(''); setStartsAt(''); @@ -55,10 +60,11 @@ const CreateLegalHoldForm = (props: CreateLegalHoldFormProps) => { ends_at: (new Date(endsAt)).getTime(), starts_at: (new Date(startsAt)).getTime(), display_name: displayName, + exclude_public_channels: excludePublicChannels, name: slugify(displayName), }; - props.createLegalHold(data).then((response) => { + props.createLegalHold(data).then((_) => { resetForm(); props.onExited(); }).catch((error) => { @@ -138,6 +144,23 @@ const CreateLegalHoldForm = (props: CreateLegalHoldFormProps) => { onChange={setUsers} /> +
+ + +
{ }; export default CreateLegalHoldForm; - diff --git a/webapp/src/components/update_legal_hold_form/update_legal_hold_form.tsx b/webapp/src/components/update_legal_hold_form/update_legal_hold_form.tsx index df0bcd0..9dd87d9 100644 --- a/webapp/src/components/update_legal_hold_form/update_legal_hold_form.tsx +++ b/webapp/src/components/update_legal_hold_form/update_legal_hold_form.tsx @@ -25,6 +25,7 @@ const UpdateLegalHoldForm = (props: UpdateLegalHoldFormProps) => { const [startsAt, setStartsAt] = useState(''); const [endsAt, setEndsAt] = useState(''); const [saving, setSaving] = useState(false); + const [excludePublicChannels, setExcludePublicChannels] = useState(false); const [serverError, setServerError] = useState(''); const displayNameChanged = (e: React.ChangeEvent) => { @@ -35,6 +36,10 @@ const UpdateLegalHoldForm = (props: UpdateLegalHoldFormProps) => { setEndsAt(e.target.value); }; + const excludePublicChannelsChanged: (e: React.ChangeEvent) => void = (e) => { + setExcludePublicChannels(e.target.checked); + }; + const resetForm = () => { setDisplayName(''); setEndsAt(''); @@ -54,6 +59,7 @@ const UpdateLegalHoldForm = (props: UpdateLegalHoldFormProps) => { setId(props.legalHold.id); setDisplayName(props.legalHold?.display_name); setUsers(props.users); + setExcludePublicChannels(props.legalHold.exclude_public_channels); if (props.legalHold.starts_at) { const startsAtString = dayjs(props.legalHold.starts_at).format('YYYY-MM-DD'); @@ -81,10 +87,11 @@ const UpdateLegalHoldForm = (props: UpdateLegalHoldFormProps) => { id: props.legalHold.id, user_ids: users.map((user) => user.id), ends_at: (new Date(endsAt)).getTime(), + exclude_public_channels: excludePublicChannels, display_name: displayName, }; - props.updateLegalHold(data).then((response) => { + props.updateLegalHold(data).then(() => { resetForm(); props.onExited(); }).catch((error) => { @@ -164,6 +171,23 @@ const UpdateLegalHoldForm = (props: UpdateLegalHoldFormProps) => { onChange={setUsers} />
+
+ + +