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

System Console button to manually run a legal hold immediately (#89) #90

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
32 changes: 32 additions & 0 deletions server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func (p *Plugin) ServeHTTP(_ *plugin.Context, w http.ResponseWriter, r *http.Req
router.HandleFunc("/api/v1/legalholds/{legalhold_id:[A-Za-z0-9]+}", p.updateLegalHold).Methods(http.MethodPut)
router.HandleFunc("/api/v1/legalholds/{legalhold_id:[A-Za-z0-9]+}/download", p.downloadLegalHold).Methods(http.MethodGet)
router.HandleFunc("/api/v1/test_amazon_s3_connection", p.testAmazonS3Connection).Methods(http.MethodPost)
router.HandleFunc("/api/v1/legalhold/{legalhold_id:[A-Za-z0-9]+}/run", p.runLegalHoldNow).Methods(http.MethodPost)

// Other routes
router.HandleFunc("/api/v1/legalhold/run", p.runJobFromAPI).Methods(http.MethodPost)
Expand Down Expand Up @@ -320,6 +321,37 @@ func (p *Plugin) runJobFromAPI(w http.ResponseWriter, _ *http.Request) {
go p.legalHoldJob.RunFromAPI()
}

func (p *Plugin) runLegalHoldNow(w http.ResponseWriter, r *http.Request) {
legalholdID, err := RequireLegalHoldID(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

// Fetch the LegalHold
lh, err := p.KVStore.GetLegalHoldByID(legalholdID)
if err != nil {
p.API.LogError("Failed to release legal hold - retrieve legal hold from kvstore", err.Error())
http.Error(w, "failed to release legal hold", http.StatusInternalServerError)
return
}

p.legalHoldJob.RunSingleHoldNow(lh)

b, jsonErr := json.Marshal(make(map[string]interface{}))
if jsonErr != nil {
http.Error(w, "Error encoding json", http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
_, err = w.Write(b)
if err != nil {
p.API.LogError("failed to write http response", err.Error())
return
}
}

// testAmazonS3Connection tests the plugin's custom Amazon S3 connection
func (p *Plugin) testAmazonS3Connection(w http.ResponseWriter, _ *http.Request) {
type messageResponse struct {
Expand Down
95 changes: 82 additions & 13 deletions server/api_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package main

import (
"fmt"
"net/http"
"net/http/httptest"
"testing"

pluginapi "github.com/mattermost/mattermost-plugin-api"
"github.com/mattermost/mattermost-plugin-legal-hold/server/config"
"github.com/mattermost/mattermost-server/v6/model"
mattermostModel "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin/plugintest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"

"github.com/mattermost/mattermost-plugin-legal-hold/server/config"
"github.com/mattermost/mattermost-plugin-legal-hold/server/store/kvstore"
)

func TestTestAmazonS3Connection(t *testing.T) {
Expand All @@ -20,7 +24,7 @@ func TestTestAmazonS3Connection(t *testing.T) {
p.SetAPI(api)
p.Client = pluginapi.NewClient(p.API, p.Driver)

api.On("HasPermissionTo", "test_user_id", model.PermissionManageSystem).Return(true)
api.On("HasPermissionTo", "test_user_id", mattermostModel.PermissionManageSystem).Return(true)
api.On("LogInfo", mock.Anything).Maybe()

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -37,16 +41,16 @@ func TestTestAmazonS3Connection(t *testing.T) {
TimeOfDay: "10:00pm -0500h",
AmazonS3BucketSettings: config.AmazonS3BucketSettings{
Enable: true,
Settings: model.FileSettings{
DriverName: model.NewString("amazons3"),
AmazonS3Bucket: model.NewString("bucket"),
AmazonS3AccessKeyId: model.NewString("access_key_id"),
AmazonS3SecretAccessKey: model.NewString("secret_access_key"),
AmazonS3RequestTimeoutMilliseconds: model.NewInt64(5000),
AmazonS3Endpoint: model.NewString(server.Listener.Addr().String()),
AmazonS3Region: model.NewString("us-east-1"),
AmazonS3SSL: model.NewBool(false),
AmazonS3SSE: model.NewBool(false),
Settings: mattermostModel.FileSettings{
DriverName: mattermostModel.NewString("amazons3"),
AmazonS3Bucket: mattermostModel.NewString("bucket"),
AmazonS3AccessKeyId: mattermostModel.NewString("access_key_id"),
AmazonS3SecretAccessKey: mattermostModel.NewString("secret_access_key"),
AmazonS3RequestTimeoutMilliseconds: mattermostModel.NewInt64(5000),
AmazonS3Endpoint: mattermostModel.NewString(server.Listener.Addr().String()),
AmazonS3Region: mattermostModel.NewString("us-east-1"),
AmazonS3SSL: mattermostModel.NewBool(false),
AmazonS3SSE: mattermostModel.NewBool(false),
},
},
})
Expand All @@ -60,3 +64,68 @@ func TestTestAmazonS3Connection(t *testing.T) {
p.ServeHTTP(nil, recorder, req)
require.Equal(t, http.StatusOK, recorder.Code)
}

func TestRunLegalHoldNow(t *testing.T) {
validID := mattermostModel.NewId()

tests := []struct {
name string
legalHoldID string
wantErr bool
expectCode int
}{
{
name: "Invalid Legal Hold ID",
legalHoldID: "malformedid",
wantErr: true,
expectCode: http.StatusBadRequest,
},
{
name: "Unknown Legal Hold ID",
legalHoldID: mattermostModel.NewId(),
wantErr: true,
expectCode: http.StatusInternalServerError,
},
// TODO: To test the successful case, we need to put Plugin.LegalHoldJob behind
// an interface so that it can be mocked out.
// {
// name: "Valid Legal Hold",
// legalHoldID: validID,
// wantErr: false,
// expectCode: http.StatusOK,
// },
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &Plugin{}
api := &plugintest.API{}
p.SetDriver(&plugintest.Driver{})
p.SetAPI(api)
p.Client = pluginapi.NewClient(p.API, p.Driver)
p.KVStore = kvstore.NewKVStore(p.Client)

api.On("HasPermissionTo", "test_user_id", mattermostModel.PermissionManageSystem).Return(true)
api.On("LogInfo", mock.Anything).Maybe()
api.On("LogError", mock.Anything, mock.Anything).Maybe()
api.On("KVGet", fmt.Sprintf("kvstore_legal_hold_%s", validID)).
Return([]uint8("{}"), nil)
api.On("KVGet", mock.Anything).
Return(nil, mattermostModel.NewAppError("things", "stuff.stuff", nil, "", http.StatusNotFound))

recorder := httptest.NewRecorder()
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/legalhold/%s/run", tt.legalHoldID), nil)
require.NoError(t, err)

req.Header.Add("Mattermost-User-Id", "test_user_id")

p.ServeHTTP(nil, recorder, req)
require.Equal(t, tt.expectCode, recorder.Code)
if tt.wantErr {
assert.NotEmpty(t, recorder.Body)
} else {
assert.Empty(t, recorder.Body)
}
})
}
}
100 changes: 78 additions & 22 deletions server/jobs/legal_hold_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package jobs
import (
"context"
"fmt"

"sync"
"time"

Expand All @@ -16,6 +15,7 @@ import (

"github.com/mattermost/mattermost-plugin-legal-hold/server/config"
"github.com/mattermost/mattermost-plugin-legal-hold/server/legalhold"
"github.com/mattermost/mattermost-plugin-legal-hold/server/model"
"github.com/mattermost/mattermost-plugin-legal-hold/server/store/kvstore"
"github.com/mattermost/mattermost-plugin-legal-hold/server/store/sqlstore"
)
Expand Down Expand Up @@ -152,7 +152,15 @@ func (j *LegalHoldJob) RunFromAPI() {
j.run()
}

func (j *LegalHoldJob) RunSingleHoldNow(legalHold *model.LegalHold) {
j.runWithHold(legalHold)
}

func (j *LegalHoldJob) run() {
j.runWithHold(nil)
}

func (j *LegalHoldJob) runWithHold(legalHold *model.LegalHold) {
j.mux.Lock()
oldRunner := j.runner
j.mux.Unlock()
Expand Down Expand Up @@ -186,49 +194,97 @@ func (j *LegalHoldJob) run() {
settings = j.settings.Clone()
j.mux.Unlock()

j.client.Log.Info("Processing all Legal Holds")
// No legal hold specified, run as scheduled for all holds.
if legalHold == nil {
j.client.Log.Info("Processing all Legal Holds")
// Retrieve the legal holds from the store.
legalHolds, err := j.kvstore.GetAllLegalHolds()
if err != nil {
j.client.Log.Error("Failed to fetch legal holds from store", err)
}

// Retrieve the legal holds from the store.
legalHolds, err := j.kvstore.GetAllLegalHolds()
if err != nil {
j.client.Log.Error("Failed to fetch legal holds from store", err)
}
for _, lh := range legalHolds {
for {
if lh.IsFinished() {
j.client.Log.Debug(fmt.Sprintf("Legal Hold %s has ended and therefore does not executing.", lh.ID))
break
}

if !lh.NeedsExecuting(mattermostModel.GetMillis()) {
j.client.Log.Debug(fmt.Sprintf("Legal Hold %s is not yet ready to be executed again.", lh.ID))
break
}

j.client.Log.Debug(fmt.Sprintf("Creating Legal Hold Execution for legal hold: %s", lh.ID))
lhe := legalhold.NewExecution(lh, j.papi, j.sqlstore, j.filebackend)

if end, err := lhe.Execute(); err != nil {
j.client.Log.Error("An error occurred executing the legal hold.", err)
} else {
old, err := j.kvstore.GetLegalHoldByID(lh.ID)
if err != nil {
j.client.Log.Error("Failed to fetch the LegalHold prior to updating", err)
continue
}
lh = *old
lh.LastExecutionEndedAt = end
newLH, err := j.kvstore.UpdateLegalHold(lh, *old)
if err != nil {
j.client.Log.Error("Failed to update legal hold", err)
continue
}
lh = *newLH
j.client.Log.Info(fmt.Sprintf("%v", lh))
}
j.client.Log.Info("legal hold executed", "legal_hold_id", lh.ID, "legal_hold_name", lh.Name)
}
}
} else {
// Just one legal hold specified - force run it now.
j.client.Log.Debug(fmt.Sprintf("Manually triggered legal hold run for legal hold: %s", legalHold.ID))
now := mattermostModel.GetMillis()

for _, lh := range legalHolds {
for {
if lh.IsFinished() {
j.client.Log.Debug(fmt.Sprintf("Legal Hold %s has ended and therefore does not executing.", lh.ID))
break
if legalHold.IsFinished() {
j.client.Log.Debug(fmt.Sprintf("Legal Hold %s has ended and therefore does not executing.", legalHold.ID))
return
}

if !lh.NeedsExecuting(mattermostModel.GetMillis()) {
j.client.Log.Debug(fmt.Sprintf("Legal Hold %s is not yet ready to be executed again.", lh.ID))
break
if legalHold.LastExecutionEndedAt >= now {
j.client.Log.Debug(fmt.Sprintf("Legal Hold %s is not yet ready to be executed again.", legalHold.ID))
return
}
j.client.Log.Debug(fmt.Sprintf("Last Execution Ended At: %d, now: %d", legalHold.LastExecutionEndedAt, now))

j.client.Log.Debug(fmt.Sprintf("Creating manually triggered Legal Hold Execution for legal hold: %s", legalHold.ID))
lhe := legalhold.NewExecution(*legalHold, j.papi, j.sqlstore, j.filebackend)

j.client.Log.Debug(fmt.Sprintf("Creating Legal Hold Execution for legal hold: %s", lh.ID))
lhe := legalhold.NewExecution(lh, j.papi, j.sqlstore, j.filebackend)
if lhe.ExecutionEndTime > now {
j.client.Log.Info(fmt.Sprintf("Legal Hold %s: limiting execution end time to ensure it is not in the future.", legalHold.ID))
lhe.ExecutionEndTime = now
}

if end, err := lhe.Execute(); err != nil {
j.client.Log.Error("An error occurred executing the legal hold.", err)
} else {
old, err := j.kvstore.GetLegalHoldByID(lh.ID)
old, err := j.kvstore.GetLegalHoldByID(legalHold.ID)
if err != nil {
j.client.Log.Error("Failed to fetch the LegalHold prior to updating", err)
continue
return
}
lh = *old
lh := *old
lh.LastExecutionEndedAt = end
newLH, err := j.kvstore.UpdateLegalHold(lh, *old)
if err != nil {
j.client.Log.Error("Failed to update legal hold", err)
continue
return
}
lh = *newLH
j.client.Log.Info("legal hold executed", "legal_hold_id", lh.ID, "legal_hold_name", lh.Name)
legalHold = newLH
j.client.Log.Info(fmt.Sprintf("%v", legalHold))
}
}
}

_ = ctx
_ = settings
}
Expand Down
5 changes: 5 additions & 0 deletions webapp/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ class APIClient {
return this.doWithBody(url, 'post', {});
};

runLegalHold = (id: string) => {
const url = `${this.url}/legalhold/${id}/run`;
return this.doWithBody(url, 'post', {});
};

updateLegalHold = (id: string, data: UpdateLegalHold) => {
const url = `${this.url}/legalholds/${id}`;
return this.doWithBody(url, 'put', data);
Expand Down
Loading
Loading