Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow excluding public channels from a legal hold #59

Merged
merged 9 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion server/legalhold/legal_hold.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
82 changes: 44 additions & 38 deletions server/model/legal_hold.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
8 changes: 7 additions & 1 deletion server/store/sqlstore/legal_hold.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,20 @@ 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").
Where(sq.Lt{"cmh.jointime": endTime}).
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})
}
fmartingr marked this conversation as resolved.
Show resolved Hide resolved

rows, err := query.Query()
if err != nil {
ss.logger.Error("error fetching channels for user during time period", "err", err)
Expand Down
34 changes: 31 additions & 3 deletions server/store/sqlstore/legal_hold_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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
Expand Down
8 changes: 6 additions & 2 deletions server/store/sqlstore/testhelper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
5 changes: 4 additions & 1 deletion webapp/src/components/create_legal_hold_form.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@
border: none !important;
}

.create-legal-hold-container {
.create-legal-hold-checkbox {
float: left;
width: 1em;
height: 1em;
}
26 changes: 24 additions & 2 deletions webapp/src/components/create_legal_hold_form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLInputElement>) => {
Expand All @@ -35,6 +36,10 @@ const CreateLegalHoldForm = (props: CreateLegalHoldFormProps) => {
setEndsAt(e.target.value);
};

const excludePublicChannelsChanged: (e: React.ChangeEvent<HTMLInputElement>) => void = (e) => {
setExcludePublicChannels(e.target.checked);
};

const resetForm = () => {
setDisplayName('');
setStartsAt('');
Expand All @@ -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) => {
Expand Down Expand Up @@ -138,6 +144,23 @@ const CreateLegalHoldForm = (props: CreateLegalHoldFormProps) => {
onChange={setUsers}
/>
</div>
<div
style={{
display: 'flex',
columnGap: '20px',
}}
>
<input
type='checkbox'
id='legal-hold-exclude-public-channels'
checked={excludePublicChannels}
onChange={excludePublicChannelsChanged}
className={'create-legal-hold-checkbox'}
/>
<label htmlFor={'legal-hold-exclude-public-channels'}>
{'Exclude public channels'}
</label>
</div>
<div
style={{
display: 'flex',
Expand Down Expand Up @@ -189,4 +212,3 @@ const slugify = (data: string) => {
};

export default CreateLegalHoldForm;

Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLInputElement>) => {
Expand All @@ -35,6 +36,10 @@ const UpdateLegalHoldForm = (props: UpdateLegalHoldFormProps) => {
setEndsAt(e.target.value);
};

const excludePublicChannelsChanged: (e: React.ChangeEvent<HTMLInputElement>) => void = (e) => {
setExcludePublicChannels(e.target.checked);
};

const resetForm = () => {
setDisplayName('');
setEndsAt('');
Expand All @@ -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');
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -164,6 +171,23 @@ const UpdateLegalHoldForm = (props: UpdateLegalHoldFormProps) => {
onChange={setUsers}
/>
</div>
<div
style={{
display: 'flex',
columnGap: '20px',
}}
>
<input
type='checkbox'
id='legal-hold-exclude-public-channels'
checked={excludePublicChannels}
onChange={excludePublicChannelsChanged}
className={'create-legal-hold-checkbox'}
/>
<label htmlFor={'legal-hold-exclude-public-channels'}>
{'Exclude public channels'}
</label>
</div>
<div
style={{
display: 'flex',
Expand Down
1 change: 1 addition & 0 deletions webapp/src/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface LegalHold {
starts_at: number;
ends_at: number;
user_ids: string[];
exclude_public_channels: boolean;
}

export interface CreateLegalHold {
Expand Down
Loading