snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
137
server/channels/jobs/export_users_to_csv/export_users_to_csv.go
Normal file
137
server/channels/jobs/export_users_to_csv/export_users_to_csv.go
Normal file
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package export_users_to_csv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/jobs"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
timeBetweenBatches = 1 * time.Second
|
||||
)
|
||||
|
||||
// csvExportColumns defines the header row of the user CSV export. Its order must
|
||||
// match the values returned by model.UserReport.ToReport.
|
||||
var csvExportColumns = []string{
|
||||
"Id",
|
||||
"Username",
|
||||
"Email",
|
||||
"CreateAt",
|
||||
"Name",
|
||||
"Roles",
|
||||
"LastLogin",
|
||||
"LastStatusAt",
|
||||
"LastPostDate",
|
||||
"DaysActive",
|
||||
"TotalPosts",
|
||||
"ChannelCount",
|
||||
"Teams",
|
||||
"DeletedAt",
|
||||
}
|
||||
|
||||
type ExportUsersToCSVAppIFace interface {
|
||||
jobs.BatchReportWorkerAppIFace
|
||||
GetUsersForReporting(filter *model.UserReportOptions) ([]*model.UserReport, *model.AppError)
|
||||
}
|
||||
|
||||
// MakeWorker creates a batch report worker to generate CSV user reports.
|
||||
func MakeWorker(jobServer *jobs.JobServer, store store.Store, app ExportUsersToCSVAppIFace) model.Worker {
|
||||
return jobs.MakeBatchReportWorker(
|
||||
jobServer,
|
||||
store,
|
||||
app,
|
||||
timeBetweenBatches,
|
||||
"csv",
|
||||
csvExportColumns,
|
||||
getData(app),
|
||||
)
|
||||
}
|
||||
|
||||
// parseJobMetadata parses the opaque job metadata to return the information needed to decide which
|
||||
// batch to process next.
|
||||
func parseJobMetadata(data model.StringMap) (*model.UserReportOptions, error) {
|
||||
startAt, err := strconv.ParseInt(data["start_at"], 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endAt, err := strconv.ParseInt(data["end_at"], 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hideInactive := false
|
||||
if val, ok := data["hide_inactive"]; ok && val != "" {
|
||||
hideInactive, err = strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse hide_inactive: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
hideActive := false
|
||||
if val, ok := data["hide_active"]; ok && val != "" {
|
||||
hideActive, err = strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse hide_active: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
options := model.UserReportOptions{
|
||||
ReportingBaseOptions: model.ReportingBaseOptions{
|
||||
SortColumn: "Username",
|
||||
PageSize: 100,
|
||||
FromColumnValue: data["last_column_value"],
|
||||
FromId: data["last_user_id"],
|
||||
StartAt: startAt,
|
||||
EndAt: endAt,
|
||||
},
|
||||
HideInactive: hideInactive,
|
||||
HideActive: hideActive,
|
||||
Role: data["role"],
|
||||
Team: data["team"],
|
||||
GuestFilter: data["guest_filter"],
|
||||
SearchTerm: data["search_term"],
|
||||
}
|
||||
|
||||
return &options, nil
|
||||
}
|
||||
|
||||
// makeJobMetadata encodes the information needed to decide which batch to process next back into
|
||||
// the opaque job metadata.
|
||||
func makeJobMetadata(jobData model.StringMap, lastColumnValue string, userID string) model.StringMap {
|
||||
jobData["last_column_value"] = lastColumnValue
|
||||
jobData["last_user_id"] = userID
|
||||
return jobData
|
||||
}
|
||||
|
||||
func getData(app ExportUsersToCSVAppIFace) func(jobData model.StringMap) ([]model.ReportableObject, model.StringMap, bool, error) {
|
||||
return func(jobData model.StringMap) ([]model.ReportableObject, model.StringMap, bool, error) {
|
||||
filter, err := parseJobMetadata(jobData)
|
||||
if err != nil {
|
||||
return nil, nil, false, errors.Wrap(err, "failed to parse job metadata")
|
||||
}
|
||||
|
||||
users, appErr := app.GetUsersForReporting(filter)
|
||||
if appErr != nil {
|
||||
return nil, nil, false, errors.Wrapf(appErr, "failed to get the next batch (column_value=%v, user_id=%v)", filter.FromColumnValue, filter.FromId)
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
return nil, nil, true, nil
|
||||
}
|
||||
|
||||
reportableObjects := []model.ReportableObject{}
|
||||
for i := range users {
|
||||
reportableObjects = append(reportableObjects, users[i])
|
||||
}
|
||||
|
||||
return reportableObjects, makeJobMetadata(jobData, users[len(users)-1].Username, users[len(users)-1].Id), false, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package export_users_to_csv
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
)
|
||||
|
||||
func TestCSVExportColumns(t *testing.T) {
|
||||
t.Run("header has the expected columns in order", func(t *testing.T) {
|
||||
// Pin the exact header so a rename or reorder is caught. Teams sits
|
||||
// between ChannelCount and DeletedAt.
|
||||
require.Equal(t, []string{
|
||||
"Id",
|
||||
"Username",
|
||||
"Email",
|
||||
"CreateAt",
|
||||
"Name",
|
||||
"Roles",
|
||||
"LastLogin",
|
||||
"LastStatusAt",
|
||||
"LastPostDate",
|
||||
"DaysActive",
|
||||
"TotalPosts",
|
||||
"ChannelCount",
|
||||
"Teams",
|
||||
"DeletedAt",
|
||||
}, csvExportColumns)
|
||||
})
|
||||
|
||||
t.Run("header column count matches the report row values", func(t *testing.T) {
|
||||
// The CSV header is written separately from the data rows, so a count
|
||||
// mismatch between the header and model.UserReport.ToReport would
|
||||
// silently shift every column. Keep them in lock-step.
|
||||
report := (&model.UserReport{}).ToReport()
|
||||
require.Equal(t, len(csvExportColumns), len(report))
|
||||
})
|
||||
}
|
||||
|
||||
// recordingExportApp records the filters the export queries users with.
|
||||
type recordingExportApp struct {
|
||||
filters []*model.UserReportOptions
|
||||
users []*model.UserReport
|
||||
}
|
||||
|
||||
func (a *recordingExportApp) SaveReportChunk(format string, prefix string, count int, reportData []model.ReportableObject) *model.AppError {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *recordingExportApp) CompileReportChunks(format string, prefix string, numberOfChunks int, headers []string) *model.AppError {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *recordingExportApp) SendReportToUser(rctx request.CTX, job *model.Job, format string) *model.AppError {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *recordingExportApp) CleanupReportChunks(format string, prefix string, numberOfChunks int) *model.AppError {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *recordingExportApp) GetUsersForReporting(filter *model.UserReportOptions) ([]*model.UserReport, *model.AppError) {
|
||||
a.filters = append(a.filters, filter)
|
||||
return a.users, nil
|
||||
}
|
||||
|
||||
func TestGetData(t *testing.T) {
|
||||
jobData := func() model.StringMap {
|
||||
return model.StringMap{
|
||||
"requesting_user_id": "cxzhnzbpqbnufc5qhnr63zcpdw",
|
||||
"date_range": model.ReportDurationAllTime,
|
||||
"start_at": "100",
|
||||
"end_at": "200",
|
||||
"role": "system_user",
|
||||
"team": "hn6jhbbutbyd8mmwbtkr3sozbo",
|
||||
"hide_active": "false",
|
||||
"hide_inactive": "true",
|
||||
"guest_filter": "all",
|
||||
"search_term": "alice",
|
||||
}
|
||||
}
|
||||
|
||||
batchOfOne := func() []*model.UserReport {
|
||||
return []*model.UserReport{{
|
||||
User: model.User{Id: "wbtkr3sozbohn6jhbbutbyd8mm", Username: "alice-smith"},
|
||||
}}
|
||||
}
|
||||
|
||||
t.Run("should query users with every filter the export was started with", func(t *testing.T) {
|
||||
app := &recordingExportApp{users: batchOfOne()}
|
||||
|
||||
reportData, nextData, done, err := getData(app)(jobData())
|
||||
require.NoError(t, err)
|
||||
require.False(t, done)
|
||||
require.Len(t, reportData, 1)
|
||||
|
||||
require.Len(t, app.filters, 1)
|
||||
filter := app.filters[0]
|
||||
require.Equal(t, "alice", filter.SearchTerm)
|
||||
require.Equal(t, "system_user", filter.Role)
|
||||
require.Equal(t, "hn6jhbbutbyd8mmwbtkr3sozbo", filter.Team)
|
||||
require.Equal(t, "all", filter.GuestFilter)
|
||||
require.False(t, filter.HideActive)
|
||||
require.True(t, filter.HideInactive)
|
||||
require.Equal(t, int64(100), filter.StartAt)
|
||||
require.Equal(t, int64(200), filter.EndAt)
|
||||
|
||||
require.Equal(t, "alice-smith", nextData["last_column_value"])
|
||||
require.Equal(t, "wbtkr3sozbohn6jhbbutbyd8mm", nextData["last_user_id"])
|
||||
})
|
||||
|
||||
t.Run("should keep filtering on the search term once the cursor advances", func(t *testing.T) {
|
||||
app := &recordingExportApp{users: batchOfOne()}
|
||||
|
||||
_, nextData, _, err := getData(app)(jobData())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, _, err = getData(app)(nextData)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, app.filters, 2)
|
||||
require.Equal(t, "alice", app.filters[1].SearchTerm)
|
||||
require.Equal(t, "alice-smith", app.filters[1].FromColumnValue)
|
||||
require.Equal(t, "wbtkr3sozbohn6jhbbutbyd8mm", app.filters[1].FromId)
|
||||
})
|
||||
|
||||
t.Run("should export everything for jobs queued before search terms were recorded", func(t *testing.T) {
|
||||
app := &recordingExportApp{users: batchOfOne()}
|
||||
|
||||
data := jobData()
|
||||
delete(data, "search_term")
|
||||
|
||||
_, _, _, err := getData(app)(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, app.filters, 1)
|
||||
require.Empty(t, app.filters[0].SearchTerm)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user