Merge branch 'master' into mark-as-unread

Этот коммит содержится в:
Harrison Healey
2019-09-20 10:11:31 -04:00
родитель e6f67c664c 32bc585446
Коммит 05c2a134ac
15 изменённых файлов: 928 добавлений и 547 удалений

Просмотреть файл

@@ -1,8 +1,224 @@
version: 2.1 version: 2.1
orbs:
aws-s3: circleci/aws-s3@1.0.11
executors:
ubuntu:
working_directory: ~/go/src/github.com/mattermost/
machine:
image: "ubuntu-1604:201903-01"
environment:
COMPOSE_PROJECT_NAME: "circleci"
jobs: jobs:
setup:
working_directory: /go/src/github.com/mattermost/mattermost-server
docker:
- image: mattermost/mattermost-build-webapp:oct-2-2018
steps:
- checkout
- run: |
cd ../
mkdir -p ~/.ssh/
echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config
git clone git@github.com:mattermost/mattermost-webapp.git
cd mattermost-webapp
git checkout $CIRCLE_BRANCH || git checkout master
export WEBAPP_GIT_COMMIT=$(git rev-parse HEAD)
echo "$WEBAPP_GIT_COMMIT"
curl -f -o ./dist.tar.gz https://releases.mattermost.com/mattermost-webapp/commit/${WEBAPP_GIT_COMMIT}/mattermost-webapp.tar.gz && mkdir ./dist && tar -xvf ./dist.tar.gz -C ./dist --strip-components=1 || make node_modules test build
- persist_to_workspace:
root: /go/src/github.com/mattermost
paths:
- mattermost-webapp
- mattermost-server
# TODO: enable this step when the i18n-extract works with mattermost-server only
# and not depend on both mm-server/enterprise
# check-i18n:
# docker:
# - image: circleci/golang:1.12
# working_directory: /go/src/github.com/mattermost/
# steps:
# - attach_workspace:
# at: /go/src/github.com/mattermost/
# - run:
# command: |
# cd mattermost-server
# cp i18n/en.json /tmp/en.json
# make i18n-extract
# diff /tmp/en.json i18n/en.json
build: build:
docker: docker:
- image: circleci/golang:1.12 - image: mattermost/mattermost-build-server:feb-28-2019
working_directory: /go/src/github.com/mattermost
steps: steps:
- run: echo "skipping build. PR \#11978 in progress." - attach_workspace:
at: /go/src/github.com/mattermost/
- run:
command: |
cd mattermost-server
make config-reset
make check-style BUILD_NUMBER='${CIRCLE_BRANCH}-${CIRCLE_BUILD_NUM}'
GOFLAGS=-p=8 make build BUILD_NUMBER='${CIRCLE_BRANCH}-${CIRCLE_BUILD_NUM}'
GOFLAGS=-p=8 make package BUILD_NUMBER='${CIRCLE_BRANCH}-${CIRCLE_BUILD_NUM}'
- store_artifacts:
path: /go/src/github.com/mattermost/mattermost-server/dist/mattermost-team-linux-amd64.tar.gz
- store_artifacts:
path: /go/src/github.com/mattermost/mattermost-server/dist/mattermost-team-osx-amd64.tar.gz
- store_artifacts:
path: /go/src/github.com/mattermost/mattermost-server/dist/mattermost-team-windows-amd64.zip
- persist_to_workspace:
root: /go/src/github.com/mattermost
paths:
- mattermost-server
- mattermost-webapp
test:
executor:
name: ubuntu
steps:
- attach_workspace:
at: ~/go/src/github.com/mattermost
- run:
name: Run Docker compose
command: |
cd mattermost-server/build
docker-compose --no-ansi run --rm start_dependencies
cat ../tests/test-data.ldif | docker-compose --no-ansi exec -T openldap bash -c 'ldapadd -x -D "cn=admin,dc=mm,dc=test,dc=com" -w mostest';
docker-compose --no-ansi exec -T minio sh -c 'mkdir -p /data/mattermost-test';
docker-compose --no-ansi ps
background: true
- run:
name: Wait for docker compose
command: |
sleep 5
docker run --net circleci_mm-test appropriate/curl:latest sh -c "until curl --max-time 5 --output - http://mysql:3306; do echo waiting for mysql; sleep 5; done;"
docker run --net circleci_mm-test appropriate/curl:latest sh -c "until curl --max-time 5 --output - http://elasticsearch:9200; do echo waiting for elasticsearch; sleep 5; done;"
- run:
name: Run Tests
command: |
ulimit -n 8096
mkdir -p mattermost-server/client/plugins
docker run -it --net circleci_mm-test \
--env TEST_DATABASE_MYSQL_DSN="mmuser:mostest@tcp(mysql:3306)/mattermost_test?charset=utf8mb4,utf8\u0026readTimeout=30s\u0026writeTimeout=30s" \
--env TEST_DATABASE_POSTGRESQL_DSN="postgres://mmuser:mostest@postgres:5432/mattermost_test?sslmode=disable&connect_timeout=10" \
--env TEST_DATABASE_MYSQL_ROOT_PASSWD=mostest \
--env CI_INBUCKET_HOST=inbucket \
--env CI_MINIO_HOST=minio \
--env CI_INBUCKET_PORT=10080 \
--env CI_MINIO_PORT=9000 \
--env CI_INBUCKET_SMTP_PORT=10025 \
--env CI_LDAP_HOST=openldap \
--env IS_CI="true" \
--env MM_SQLSETTINGS_DATASOURCE="mmuser:mostest@tcp(mysql:3306)/mattermost_test?charset=utf8mb4,utf8" \
--env MM_EMAILSETTINGS_SMTPSERVER=inbucket \
--env MM_EMAILSETTINGS_SMTPPORT=10025 \
--env MM_ELASTICSEARCHSETTINGS_CONNECTIONURL=http://elasticsearch:9200 \
-v ~/go/src:/go/src \
-w /go/src/github.com/mattermost/mattermost-server \
mattermost/mattermost-build-server:feb-28-2019 \
bash -c 'ulimit -n 8096; make test-server BUILD_NUMBER="$CIRCLE_BRANCH-$CIRCLE_PREVIOUS_BUILD_NUM" TESTFLAGS= TESTFLAGSEE='
no_output_timeout: 1h
- run:
name: Capture docker logs
when: always
command: |
cd mattermost-server/build
# Capture docker logs
mkdir -p logs
docker-compose logs --tail="all" -t --no-color > logs/docker-compose_logs
docker ps -a --no-trunc > logs/docker_ps
docker stats -a --no-stream > logs/docker_stats
tar -czvf logs/docker_logs.tar.gz logs/docker-compose_logs logs/docker_ps logs/docker_stats
- store_artifacts:
path: ~/go/src/github.com/mattermost/mattermost-server/build/logs
- run:
when: always
command: |
cd mattermost-server
mkdir -p test-results
cp report.xml test-results
- store_test_results:
path: ~/go/src/github.com/mattermost/mattermost-server/test-results/
- store_artifacts:
path: ~/go/src/github.com/mattermost/mattermost-server/test-results/
upload-s3-sha:
docker:
- image: 'circleci/python:2.7'
working_directory: ~/go/src/github.com/mattermost/enterprise
steps:
- attach_workspace:
at: ~/go/src/github.com/mattermost/
- run:
command: |
cd ~/go/src/github.com/mattermost/mattermost-server/dist/
rm -rf mattermost
- aws-s3/copy:
from: ~/go/src/github.com/mattermost/mattermost-server/dist/
to: "s3://releases.mattermost.com/mattermost-platform-pr/commit/${CIRCLE_SHA1}/"
arguments: --acl public-read --cache-control "no-cache" --recursive
upload-s3:
docker:
- image: 'circleci/python:2.7'
working_directory: ~/go/src/github.com/mattermost/enterprise
steps:
- attach_workspace:
at: ~/go/src/github.com/mattermost/
- run:
command: |
cd ~/go/src/github.com/mattermost/mattermost-server/dist/
rm -rf mattermost
- aws-s3/copy:
from: ~/go/src/github.com/mattermost/mattermost-server/dist/
to: s3://releases.mattermost.com/mattermost-platform-pr/$(echo "${CIRCLE_BRANCH}" | sed 's/pull\//PR-/g')/
arguments: --acl public-read --cache-control "no-cache" --recursive
build-docker:
working_directory: ~/
docker:
- image: circleci/buildpack-deps:stretch
steps:
- attach_workspace:
at: .
- setup_remote_docker
- run:
command: |
export TAG="${CIRCLE_SHA1:0:7}"
cd mattermost-server
export MM_PACKAGE=https://releases.mattermost.com/mattermost-platform-pr/commit/${CIRCLE_SHA1}/mattermost-team-linux-amd64.tar.gz
docker build --build-arg MM_PACKAGE=$MM_PACKAGE -t mattermost/mattermost-team-edition:${TAG} build
echo $DOCKER_PASSWORD | docker login --username $DOCKER_USERNAME --password-stdin
docker push mattermost/mattermost-team-edition:${TAG}
workflows:
version: 2
untagged-build:
jobs:
- setup
# - check-i18n:
# requires:
# - setup
- test:
requires:
- setup
- build:
requires:
- test
- upload-s3-sha:
context: mattermost-ci-s3
requires:
- build
- upload-s3:
context: mattermost-ci-s3
requires:
- build
- build-docker:
context: matterbuild-docker
requires:
- upload-s3-sha

Просмотреть файл

@@ -18,6 +18,7 @@ func (api *API) ApiHandler(h func(*Context, http.ResponseWriter, *http.Request))
handler := &web.Handler{ handler := &web.Handler{
GetGlobalAppOptions: api.GetGlobalAppOptions, GetGlobalAppOptions: api.GetGlobalAppOptions,
HandleFunc: h, HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: false, RequireSession: false,
TrustRequester: false, TrustRequester: false,
RequireMfa: false, RequireMfa: false,
@@ -35,6 +36,7 @@ func (api *API) ApiSessionRequired(h func(*Context, http.ResponseWriter, *http.R
handler := &web.Handler{ handler := &web.Handler{
GetGlobalAppOptions: api.GetGlobalAppOptions, GetGlobalAppOptions: api.GetGlobalAppOptions,
HandleFunc: h, HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: true, RequireSession: true,
TrustRequester: false, TrustRequester: false,
RequireMfa: true, RequireMfa: true,
@@ -54,6 +56,7 @@ func (api *API) ApiSessionRequiredMfa(h func(*Context, http.ResponseWriter, *htt
handler := &web.Handler{ handler := &web.Handler{
GetGlobalAppOptions: api.GetGlobalAppOptions, GetGlobalAppOptions: api.GetGlobalAppOptions,
HandleFunc: h, HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: true, RequireSession: true,
TrustRequester: false, TrustRequester: false,
RequireMfa: false, RequireMfa: false,
@@ -73,6 +76,7 @@ func (api *API) ApiHandlerTrustRequester(h func(*Context, http.ResponseWriter, *
handler := &web.Handler{ handler := &web.Handler{
GetGlobalAppOptions: api.GetGlobalAppOptions, GetGlobalAppOptions: api.GetGlobalAppOptions,
HandleFunc: h, HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: false, RequireSession: false,
TrustRequester: true, TrustRequester: true,
RequireMfa: false, RequireMfa: false,
@@ -91,6 +95,7 @@ func (api *API) ApiSessionRequiredTrustRequester(h func(*Context, http.ResponseW
handler := &web.Handler{ handler := &web.Handler{
GetGlobalAppOptions: api.GetGlobalAppOptions, GetGlobalAppOptions: api.GetGlobalAppOptions,
HandleFunc: h, HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: true, RequireSession: true,
TrustRequester: true, TrustRequester: true,
RequireMfa: true, RequireMfa: true,

Просмотреть файл

@@ -806,6 +806,13 @@ func autocompleteUsers(c *Context, w http.ResponseWriter, r *http.Request) {
var autocomplete model.UserAutocomplete var autocomplete model.UserAutocomplete
var err *model.AppError
options, err = c.App.RestrictUsersSearchByPermissions(c.App.Session.UserId, options)
if err != nil {
c.Err = err
return
}
if len(channelId) > 0 { if len(channelId) > 0 {
// Applying the provided teamId here is useful for DMs and GMs which don't belong // Applying the provided teamId here is useful for DMs and GMs which don't belong
// to a team. Applying it when the channel does belong to a team makes less sense, // to a team. Applying it when the channel does belong to a team makes less sense,
@@ -819,13 +826,6 @@ func autocompleteUsers(c *Context, w http.ResponseWriter, r *http.Request) {
autocomplete.Users = result.InChannel autocomplete.Users = result.InChannel
autocomplete.OutOfChannel = result.OutOfChannel autocomplete.OutOfChannel = result.OutOfChannel
} else if len(teamId) > 0 { } else if len(teamId) > 0 {
var err *model.AppError
options, err = c.App.RestrictUsersSearchByPermissions(c.App.Session.UserId, options)
if err != nil {
c.Err = err
return
}
result, err := c.App.AutocompleteUsersInTeam(teamId, name, options) result, err := c.App.AutocompleteUsersInTeam(teamId, name, options)
if err != nil { if err != nil {
c.Err = err c.Err = err
@@ -834,13 +834,6 @@ func autocompleteUsers(c *Context, w http.ResponseWriter, r *http.Request) {
autocomplete.Users = result.InTeam autocomplete.Users = result.InTeam
} else { } else {
var err *model.AppError
options, err = c.App.RestrictUsersSearchByPermissions(c.App.Session.UserId, options)
if err != nil {
c.Err = err
return
}
result, err := c.App.SearchUsersInTeam("", name, options) result, err := c.App.SearchUsersInTeam("", name, options)
if err != nil { if err != nil {
c.Err = err c.Err = err

Просмотреть файл

@@ -997,133 +997,262 @@ func findUserInList(id string, users []*model.User) bool {
return false return false
} }
func TestAutocompleteUsers(t *testing.T) { func TestAutocompleteUsersInChannel(t *testing.T) {
th := Setup().InitBasic() th := Setup().InitBasic()
defer th.TearDown() defer th.TearDown()
teamId := th.BasicTeam.Id teamId := th.BasicTeam.Id
channelId := th.BasicChannel.Id channelId := th.BasicChannel.Id
username := th.BasicUser.Username username := th.BasicUser.Username
newUser := th.CreateUser()
rusers, resp := th.Client.AutocompleteUsersInChannel(teamId, channelId, username, model.USER_SEARCH_DEFAULT_LIMIT, "") tt := []struct {
CheckNoError(t, resp) Name string
TeamId string
if len(rusers.Users) != 1 { ChannelId string
t.Fatal("should have returned 1 user") Username string
ExpectedResults int
MoreThan bool
}{
{
"Autocomplete in channel for specific username",
teamId,
channelId,
username,
1,
false,
},
{
"Search for not valid username",
teamId,
channelId,
"amazonses",
0,
false,
},
{
"Search for all users",
teamId,
channelId,
"",
2,
true,
},
{
"Search all in specific channel",
"",
channelId,
"",
2,
true,
},
} }
rusers, resp = th.Client.AutocompleteUsersInChannel(teamId, channelId, "amazonses", model.USER_SEARCH_DEFAULT_LIMIT, "") for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
th.LoginBasic()
rusers, resp := th.Client.AutocompleteUsersInChannel(tc.TeamId, tc.ChannelId, tc.Username, model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckNoError(t, resp) CheckNoError(t, resp)
if len(rusers.Users) != 0 { if tc.MoreThan {
t.Fatal("should have returned 0 users") assert.True(t, len(rusers.Users) >= tc.ExpectedResults)
} else {
assert.Len(t, rusers.Users, tc.ExpectedResults)
} }
rusers, resp = th.Client.AutocompleteUsersInChannel(teamId, channelId, "", model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckNoError(t, resp)
if len(rusers.Users) < 2 {
t.Fatal("should have many users")
}
rusers, resp = th.Client.AutocompleteUsersInChannel("", channelId, "", model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckNoError(t, resp)
if len(rusers.Users) < 2 {
t.Fatal("should have many users")
}
rusers, resp = th.Client.AutocompleteUsersInTeam(teamId, username, model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckNoError(t, resp)
if len(rusers.Users) != 1 {
t.Fatal("should have returned 1 user")
}
rusers, resp = th.Client.AutocompleteUsers(username, model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckNoError(t, resp)
if len(rusers.Users) != 1 {
t.Fatal("should have returned 1 users")
}
rusers, resp = th.Client.AutocompleteUsers("", model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckNoError(t, resp)
if len(rusers.Users) < 2 {
t.Fatal("should have returned many users")
}
rusers, resp = th.Client.AutocompleteUsersInTeam(teamId, "amazonses", model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckNoError(t, resp)
if len(rusers.Users) != 0 {
t.Fatal("should have returned 0 users")
}
rusers, resp = th.Client.AutocompleteUsersInTeam(teamId, "", model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckNoError(t, resp)
if len(rusers.Users) < 2 {
t.Fatal("should have many users")
}
th.Client.Logout() th.Client.Logout()
_, resp = th.Client.AutocompleteUsersInChannel(teamId, channelId, username, model.USER_SEARCH_DEFAULT_LIMIT, "") _, resp = th.Client.AutocompleteUsersInChannel(tc.TeamId, tc.ChannelId, tc.Username, model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckUnauthorizedStatus(t, resp) CheckUnauthorizedStatus(t, resp)
_, resp = th.Client.AutocompleteUsersInTeam(teamId, username, model.USER_SEARCH_DEFAULT_LIMIT, "") th.Client.Login(newUser.Email, newUser.Password)
CheckUnauthorizedStatus(t, resp) _, resp = th.Client.AutocompleteUsersInChannel(tc.TeamId, tc.ChannelId, tc.Username, model.USER_SEARCH_DEFAULT_LIMIT, "")
_, resp = th.Client.AutocompleteUsers(username, model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckUnauthorizedStatus(t, resp)
user := th.CreateUser()
th.Client.Login(user.Email, user.Password)
_, resp = th.Client.AutocompleteUsersInChannel(teamId, channelId, username, model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckForbiddenStatus(t, resp) CheckForbiddenStatus(t, resp)
})
}
_, resp = th.Client.AutocompleteUsersInTeam(teamId, username, model.USER_SEARCH_DEFAULT_LIMIT, "") t.Run("Check against privacy config settings", func(t *testing.T) {
CheckForbiddenStatus(t, resp)
_, resp = th.Client.AutocompleteUsers(username, model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckNoError(t, resp)
_, resp = th.SystemAdminClient.AutocompleteUsersInChannel(teamId, channelId, username, model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckNoError(t, resp)
_, resp = th.SystemAdminClient.AutocompleteUsersInTeam(teamId, username, model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckNoError(t, resp)
_, resp = th.SystemAdminClient.AutocompleteUsers(username, model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckNoError(t, resp)
// Check against privacy config settings
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.ShowFullName = false }) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.ShowFullName = false })
th.LoginBasic() th.LoginBasic()
rusers, resp := th.Client.AutocompleteUsersInChannel(teamId, channelId, username, model.USER_SEARCH_DEFAULT_LIMIT, "")
rusers, resp = th.Client.AutocompleteUsers(username, model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckNoError(t, resp) CheckNoError(t, resp)
if rusers.Users[0].FirstName != "" || rusers.Users[0].LastName != "" { assert.Equal(t, rusers.Users[0].FirstName, "", "should not show first/last name")
t.Fatal("should not show first/last name") assert.Equal(t, rusers.Users[0].LastName, "", "should not show first/last name")
} })
rusers, resp = th.Client.AutocompleteUsersInChannel(teamId, channelId, username, model.USER_SEARCH_DEFAULT_LIMIT, "") t.Run("Check OutOfChannel results with/without VIEW_MEMBERS permissions", func(t *testing.T) {
permissionsUser := th.CreateUser()
th.SystemAdminClient.DemoteUserToGuest(permissionsUser.Id)
permissionsUser.Roles = "system_guest"
th.LinkUserToTeam(permissionsUser, th.BasicTeam)
th.AddUserToChannel(permissionsUser, th.BasicChannel)
otherUser := th.CreateUser()
th.LinkUserToTeam(otherUser, th.BasicTeam)
th.Client.Login(permissionsUser.Email, permissionsUser.Password)
rusers, resp := th.Client.AutocompleteUsersInChannel(teamId, channelId, "", model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckNoError(t, resp) CheckNoError(t, resp)
assert.Len(t, rusers.OutOfChannel, 1)
if rusers.Users[0].FirstName != "" || rusers.Users[0].LastName != "" { defaultRolePermissions := th.SaveDefaultRolePermissions()
t.Fatal("should not show first/last name") defer func() {
} th.RestoreDefaultRolePermissions(defaultRolePermissions)
}()
rusers, resp = th.Client.AutocompleteUsersInTeam(teamId, username, model.USER_SEARCH_DEFAULT_LIMIT, "") th.RemovePermissionFromRole(model.PERMISSION_VIEW_MEMBERS.Id, model.SYSTEM_USER_ROLE_ID)
th.RemovePermissionFromRole(model.PERMISSION_VIEW_MEMBERS.Id, model.TEAM_USER_ROLE_ID)
rusers, resp = th.Client.AutocompleteUsersInChannel(teamId, channelId, "", model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckNoError(t, resp) CheckNoError(t, resp)
assert.Len(t, rusers.OutOfChannel, 0)
if rusers.Users[0].FirstName != "" || rusers.Users[0].LastName != "" { th.App.GetOrCreateDirectChannel(permissionsUser.Id, otherUser.Id)
t.Fatal("should not show first/last name")
} rusers, resp = th.Client.AutocompleteUsersInChannel(teamId, channelId, "", model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckNoError(t, resp)
assert.Len(t, rusers.OutOfChannel, 1)
})
t.Run("user must have access to team id, especially when it does not match channel's team id", func(t *testing.T) { t.Run("user must have access to team id, especially when it does not match channel's team id", func(t *testing.T) {
rusers, resp = th.Client.AutocompleteUsersInChannel("otherTeamId", channelId, username, model.USER_SEARCH_DEFAULT_LIMIT, "") _, resp := th.Client.AutocompleteUsersInChannel("otherTeamId", channelId, username, model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckErrorMessage(t, resp, "api.context.permissions.app_error") CheckErrorMessage(t, resp, "api.context.permissions.app_error")
}) })
} }
func TestAutocompleteUsersInTeam(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
teamId := th.BasicTeam.Id
username := th.BasicUser.Username
newUser := th.CreateUser()
tt := []struct {
Name string
TeamId string
Username string
ExpectedResults int
MoreThan bool
}{
{
"specific username",
teamId,
username,
1,
false,
},
{
"not valid username",
teamId,
"amazonses",
0,
false,
},
{
"all users in team",
teamId,
"",
2,
true,
},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
th.LoginBasic()
rusers, resp := th.Client.AutocompleteUsersInTeam(tc.TeamId, tc.Username, model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckNoError(t, resp)
if tc.MoreThan {
assert.True(t, len(rusers.Users) >= tc.ExpectedResults)
} else {
assert.Len(t, rusers.Users, tc.ExpectedResults)
}
th.Client.Logout()
_, resp = th.Client.AutocompleteUsersInTeam(tc.TeamId, tc.Username, model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckUnauthorizedStatus(t, resp)
th.Client.Login(newUser.Email, newUser.Password)
_, resp = th.Client.AutocompleteUsersInTeam(tc.TeamId, tc.Username, model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckForbiddenStatus(t, resp)
})
}
t.Run("Check against privacy config settings", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.ShowFullName = false })
th.LoginBasic()
rusers, resp := th.Client.AutocompleteUsersInTeam(teamId, username, model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckNoError(t, resp)
assert.Equal(t, rusers.Users[0].FirstName, "", "should not show first/last name")
assert.Equal(t, rusers.Users[0].LastName, "", "should not show first/last name")
})
}
func TestAutocompleteUsers(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
username := th.BasicUser.Username
newUser := th.CreateUser()
tt := []struct {
Name string
Username string
ExpectedResults int
MoreThan bool
}{
{
"specific username",
username,
1,
false,
},
{
"not valid username",
"amazonses",
0,
false,
},
{
"all users in team",
"",
2,
true,
},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
th.LoginBasic()
rusers, resp := th.Client.AutocompleteUsers(tc.Username, model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckNoError(t, resp)
if tc.MoreThan {
assert.True(t, len(rusers.Users) >= tc.ExpectedResults)
} else {
assert.Len(t, rusers.Users, tc.ExpectedResults)
}
th.Client.Logout()
_, resp = th.Client.AutocompleteUsers(tc.Username, model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckUnauthorizedStatus(t, resp)
th.Client.Login(newUser.Email, newUser.Password)
_, resp = th.Client.AutocompleteUsers(tc.Username, model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckNoError(t, resp)
})
}
t.Run("Check against privacy config settings", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.ShowFullName = false })
th.LoginBasic()
rusers, resp := th.Client.AutocompleteUsers(username, model.USER_SEARCH_DEFAULT_LIMIT, "")
CheckNoError(t, resp)
assert.Equal(t, rusers.Users[0].FirstName, "", "should not show first/last name")
assert.Equal(t, rusers.Users[0].LastName, "", "should not show first/last name")
})
}
func TestGetProfileImage(t *testing.T) { func TestGetProfileImage(t *testing.T) {
th := Setup().InitBasic() th := Setup().InitBasic()
defer th.TearDown() defer th.TearDown()

Просмотреть файл

@@ -4,7 +4,6 @@
package app package app
import ( import (
"fmt"
"net/http" "net/http"
"strings" "strings"
@@ -192,8 +191,7 @@ func (a *App) RolesGrantPermission(roleNames []string, permissionId string) bool
if err != nil { if err != nil {
// This should only happen if something is very broken. We can't realistically // This should only happen if something is very broken. We can't realistically
// recover the situation, so deny permission and log an error. // recover the situation, so deny permission and log an error.
mlog.Error("Failed to get roles from database with role names: " + strings.Join(roleNames, ",")) mlog.Error("Failed to get roles from database with role names: "+strings.Join(roleNames, ",")+" ", mlog.Err(err))
mlog.Error(fmt.Sprint(err))
return false return false
} }

Просмотреть файл

@@ -23,13 +23,13 @@ func TestMuteCommandNoChannel(t *testing.T) {
channel1 := th.BasicChannel channel1 := th.BasicChannel
channel1M, channel1MError := th.App.GetChannelMember(channel1.Id, th.BasicUser.Id) channel1M, channel1MError := th.App.GetChannelMember(channel1.Id, th.BasicUser.Id)
if channel1MError != nil { assert.Nil(t, channel1MError, "User is not a member of channel 1")
t.Fatal("User is not a member of channel 1") assert.NotEqual(
} t,
channel1M.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP],
if channel1M.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP] == model.CHANNEL_NOTIFY_MENTION { model.CHANNEL_NOTIFY_MENTION,
t.Fatal("channel shouldn't be muted on initial setup") "Channel shouldn't be muted on initial setup",
} )
cmd := &MuteProvider{} cmd := &MuteProvider{}
resp := cmd.DoCommand(th.App, &model.CommandArgs{ resp := cmd.DoCommand(th.App, &model.CommandArgs{

Просмотреть файл

@@ -340,13 +340,11 @@ func TestPluginAPISavePluginConfig(t *testing.T) {
pluginConfigJsonString := `{"mystringsetting": "str", "MyIntSetting": 32, "myboolsetting": true}` pluginConfigJsonString := `{"mystringsetting": "str", "MyIntSetting": 32, "myboolsetting": true}`
var pluginConfig map[string]interface{} var pluginConfig map[string]interface{}
if err := json.Unmarshal([]byte(pluginConfigJsonString), &pluginConfig); err != nil { err := json.Unmarshal([]byte(pluginConfigJsonString), &pluginConfig)
t.Fatal(err) require.NoError(t, err)
}
if err := api.SavePluginConfig(pluginConfig); err != nil { appErr := api.SavePluginConfig(pluginConfig)
t.Fatal(err) require.Nil(t, appErr)
}
type Configuration struct { type Configuration struct {
MyStringSetting string MyStringSetting string
@@ -355,14 +353,12 @@ func TestPluginAPISavePluginConfig(t *testing.T) {
} }
savedConfiguration := new(Configuration) savedConfiguration := new(Configuration)
if err := api.LoadPluginConfiguration(savedConfiguration); err != nil { err = api.LoadPluginConfiguration(savedConfiguration)
t.Fatal(err) require.NoError(t, err)
}
expectedConfiguration := new(Configuration) expectedConfiguration := new(Configuration)
if err := json.Unmarshal([]byte(pluginConfigJsonString), &expectedConfiguration); err != nil { err = json.Unmarshal([]byte(pluginConfigJsonString), &expectedConfiguration)
t.Fatal(err) require.NoError(t, err)
}
assert.Equal(t, expectedConfiguration, savedConfiguration) assert.Equal(t, expectedConfiguration, savedConfiguration)
} }
@@ -387,9 +383,9 @@ func TestPluginAPIGetPluginConfig(t *testing.T) {
pluginConfigJsonString := `{"mystringsetting": "str", "myintsetting": 32, "myboolsetting": true}` pluginConfigJsonString := `{"mystringsetting": "str", "myintsetting": 32, "myboolsetting": true}`
var pluginConfig map[string]interface{} var pluginConfig map[string]interface{}
if err := json.Unmarshal([]byte(pluginConfigJsonString), &pluginConfig); err != nil { err := json.Unmarshal([]byte(pluginConfigJsonString), &pluginConfig)
t.Fatal(err) require.NoError(t, err)
}
th.App.UpdateConfig(func(cfg *model.Config) { th.App.UpdateConfig(func(cfg *model.Config) {
cfg.PluginSettings.Plugins["pluginid"] = pluginConfig cfg.PluginSettings.Plugins["pluginid"] = pluginConfig
}) })
@@ -403,9 +399,9 @@ func TestPluginAPILoadPluginConfiguration(t *testing.T) {
defer th.TearDown() defer th.TearDown()
var pluginJson map[string]interface{} var pluginJson map[string]interface{}
if err := json.Unmarshal([]byte(`{"mystringsetting": "str", "MyIntSetting": 32, "myboolsetting": true}`), &pluginJson); err != nil { err := json.Unmarshal([]byte(`{"mystringsetting": "str", "MyIntSetting": 32, "myboolsetting": true}`), &pluginJson)
t.Fatal(err) require.NoError(t, err)
}
th.App.UpdateConfig(func(cfg *model.Config) { th.App.UpdateConfig(func(cfg *model.Config) {
cfg.PluginSettings.Plugins["testloadpluginconfig"] = pluginJson cfg.PluginSettings.Plugins["testloadpluginconfig"] = pluginJson
}) })
@@ -474,9 +470,9 @@ func TestPluginAPILoadPluginConfigurationDefaults(t *testing.T) {
defer th.TearDown() defer th.TearDown()
var pluginJson map[string]interface{} var pluginJson map[string]interface{}
if err := json.Unmarshal([]byte(`{"mystringsetting": "override"}`), &pluginJson); err != nil { err := json.Unmarshal([]byte(`{"mystringsetting": "override"}`), &pluginJson)
t.Fatal(err) require.NoError(t, err)
}
th.App.UpdateConfig(func(cfg *model.Config) { th.App.UpdateConfig(func(cfg *model.Config) {
cfg.PluginSettings.Plugins["testloadpluginconfig"] = pluginJson cfg.PluginSettings.Plugins["testloadpluginconfig"] = pluginJson
}) })

Просмотреть файл

@@ -110,6 +110,126 @@ pipeline {
} }
} }
stage('Push to S3') {
stages {
stage('Pull request') {
when {
allOf {
expression { env.CHANGE_ID != null }
expression { env.CHANGE_TARGET != null }
}
}
steps {
dir('src/github.com/mattermost/mattermost-server/dist') {
step([$class: 'S3BucketPublisher', dontWaitForConcurrentBuildCompletion: false, entries: [[
bucket: "releases.mattermost.com/mattermost-platform-pr/${CHANGE_ID}",
excludedFile: '',
flatten: true,
gzipFiles: false,
keepForever: false,
managedArtifacts: false,
noUploadOnFailure: true,
selectedRegion: 'us-east-1',
showDirectlyInBrowser: false,
sourceFile: '*.tar.gz',
storageClass: 'STANDARD',
uploadFromSlave: false,
useServerSideEncryption: false,
userMetadata: [[key: 'Cache-Control', value: 'no-cache']]
], [
bucket: "releases.mattermost.com/mattermost-platform-pr/${CHANGE_BRANCH}",
excludedFile: '',
flatten: true,
gzipFiles: false,
keepForever: false,
managedArtifacts: false,
noUploadOnFailure: true,
selectedRegion: 'us-east-1',
showDirectlyInBrowser: false,
sourceFile: '*.tar.gz',
storageClass: 'STANDARD',
uploadFromSlave: false,
useServerSideEncryption: false,
userMetadata: [[key: 'Cache-Control', value: 'no-cache']]
]], profileName: 'Releases', userMetadAta: []])
}
}
}
stage('Branch') {
when {
expression { env.CHANGE_ID == null }
}
steps {
dir('src/github.com/mattermost/mattermost-server/dist') {
step([$class: 'S3BucketPublisher', dontWaitForConcurrentBuildCompletion: false, entries: [[
bucket: "releases.mattermost.com/mattermost-platform-pr/${BRANCH_NAME}",
excludedFile: '',
flatten: true,
gzipFiles: false,
keepForever: false,
managedArtifacts: false,
noUploadOnFailure: true,
selectedRegion: 'us-east-1',
showDirectlyInBrowser: false,
sourceFile: '*.tar.gz',
storageClass: 'STANDARD',
uploadFromSlave: false,
useServerSideEncryption: false,
userMetadata: [[key: 'Cache-Control', value: 'no-cache']]
]], profileName: 'Releases', userMetadAta: []])
}
}
}
}
}
stage('Clean checkout') {
when {
expression { env.CHANGE_ID != null }
}
// We need to perform a clean checkout here to ge the original git commit hash from the PR
// Jenkins now merges master in top of the PR and this generate a new git hash
// We need to do that to build the docker image based on the original git commit and then this will be used by
// mattermod to update the test server.
steps {
sh """
mkdir -p /tmp/mattermost-server
"""
dir('/tmp/mattermost-server') {
checkout([$class: 'GitSCM', branches: [[name: 'FETCH_HEAD']],
doGenerateSubmoduleConfigurations: false, extensions: [],
submoduleCfg: [], userRemoteConfigs: [
[refspec: "+refs/pull/${CHANGE_ID}/head:refs/remotes/origin/PR-${CHANGE_ID}",
credentialsId: "310159d3-f7c5-4f5d-bfa1-151e3ef2db57",url: "https://github.com/mattermost/mattermost-server.git"]]])
sh 'git rev-parse --short HEAD'
}
}
}
stage('Build Docker Image') {
environment {
GIT_COMMIT_SHORT = sh(
script: "cd /tmp/mattermost-server && printf \$(git rev-parse --short HEAD)",
returnStdout: true
)
}
when {
expression { env.CHANGE_ID != null }
}
steps {
dir('src/github.com/mattermost/mattermost-server') {
withCredentials([usernamePassword(credentialsId: 'matterbuild-docker-hub', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
sh 'docker login --username ${DOCKER_USER} --password ${DOCKER_PASS}'
sh """
docker build --no-cache --build-arg MM_PACKAGE=https://releases.mattermost.com/mattermost-platform-pr/${CHANGE_ID}/mattermost-enterprise-linux-amd64.tar.gz -t mattermost/mattermost-enterprise-edition:${GIT_COMMIT_SHORT} build
docker push mattermost/mattermost-enterprise-edition:${GIT_COMMIT_SHORT}
docker logout
"""
}
}
}
}
stage('Test') { stage('Test') {
environment { environment {
GOPATH = "/go" GOPATH = "/go"
@@ -269,126 +389,6 @@ pipeline {
} }
} }
} }
stage('Push to S3') {
stages {
stage('Pull request') {
when {
allOf {
expression { env.CHANGE_ID != null }
expression { env.CHANGE_TARGET != null }
}
}
steps {
dir('src/github.com/mattermost/mattermost-server/dist') {
step([$class: 'S3BucketPublisher', dontWaitForConcurrentBuildCompletion: false, entries: [[
bucket: "releases.mattermost.com/mattermost-platform-pr/${CHANGE_ID}",
excludedFile: '',
flatten: true,
gzipFiles: false,
keepForever: false,
managedArtifacts: false,
noUploadOnFailure: true,
selectedRegion: 'us-east-1',
showDirectlyInBrowser: false,
sourceFile: '*.tar.gz',
storageClass: 'STANDARD',
uploadFromSlave: false,
useServerSideEncryption: false,
userMetadata: [[key: 'Cache-Control', value: 'no-cache']]
], [
bucket: "releases.mattermost.com/mattermost-platform-pr/${CHANGE_BRANCH}",
excludedFile: '',
flatten: true,
gzipFiles: false,
keepForever: false,
managedArtifacts: false,
noUploadOnFailure: true,
selectedRegion: 'us-east-1',
showDirectlyInBrowser: false,
sourceFile: '*.tar.gz',
storageClass: 'STANDARD',
uploadFromSlave: false,
useServerSideEncryption: false,
userMetadata: [[key: 'Cache-Control', value: 'no-cache']]
]], profileName: 'Releases', userMetadAta: []])
}
}
}
stage('Branch') {
when {
expression { env.CHANGE_ID == null }
}
steps {
dir('src/github.com/mattermost/mattermost-server/dist') {
step([$class: 'S3BucketPublisher', dontWaitForConcurrentBuildCompletion: false, entries: [[
bucket: "releases.mattermost.com/mattermost-platform-pr/${BRANCH_NAME}",
excludedFile: '',
flatten: true,
gzipFiles: false,
keepForever: false,
managedArtifacts: false,
noUploadOnFailure: true,
selectedRegion: 'us-east-1',
showDirectlyInBrowser: false,
sourceFile: '*.tar.gz',
storageClass: 'STANDARD',
uploadFromSlave: false,
useServerSideEncryption: false,
userMetadata: [[key: 'Cache-Control', value: 'no-cache']]
]], profileName: 'Releases', userMetadAta: []])
}
}
}
}
}
stage('Clean checkout') {
when {
expression { env.CHANGE_ID != null }
}
// We need to perform a clean checkout here to ge the original git commit hash from the PR
// Jenkins now merges master in top of the PR and this generate a new git hash
// We need to do that to build the docker image based on the original git commit and then this will be used by
// mattermod to update the test server.
steps {
sh """
mkdir -p /tmp/mattermost-server
"""
dir('/tmp/mattermost-server') {
checkout([$class: 'GitSCM', branches: [[name: 'FETCH_HEAD']],
doGenerateSubmoduleConfigurations: false, extensions: [],
submoduleCfg: [], userRemoteConfigs: [
[refspec: "+refs/pull/${CHANGE_ID}/head:refs/remotes/origin/PR-${CHANGE_ID}",
credentialsId: "310159d3-f7c5-4f5d-bfa1-151e3ef2db57",url: "https://github.com/mattermost/mattermost-server.git"]]])
sh 'git rev-parse --short HEAD'
}
}
}
stage('Build Docker Image') {
environment {
GIT_COMMIT_SHORT = sh(
script: "cd /tmp/mattermost-server && printf \$(git rev-parse --short HEAD)",
returnStdout: true
)
}
when {
expression { env.CHANGE_ID != null }
}
steps {
dir('src/github.com/mattermost/mattermost-server') {
withCredentials([usernamePassword(credentialsId: 'matterbuild-docker-hub', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
sh 'docker login --username ${DOCKER_USER} --password ${DOCKER_PASS}'
sh """
docker build --no-cache --build-arg MM_PACKAGE=https://releases.mattermost.com/mattermost-platform-pr/${CHANGE_ID}/mattermost-enterprise-linux-amd64.tar.gz -t mattermost/mattermost-enterprise-edition:${GIT_COMMIT_SHORT} build
docker push mattermost/mattermost-enterprise-edition:${GIT_COMMIT_SHORT}
docker logout
"""
}
}
}
}
} }
post { post {

Просмотреть файл

@@ -21,6 +21,8 @@ import (
"github.com/mattermost/viper" "github.com/mattermost/viper"
) )
const noSettingsNamed = "unable to find a setting named: %s"
var ConfigCmd = &cobra.Command{ var ConfigCmd = &cobra.Command{
Use: "config", Use: "config",
Short: "Configuration", Short: "Configuration",
@@ -296,7 +298,7 @@ func updateConfigValue(configSetting string, newVal []string, oldConfig, newConf
func UpdateMap(configMap map[string]interface{}, configSettings []string, newVal []string) error { func UpdateMap(configMap map[string]interface{}, configSettings []string, newVal []string) error {
res, ok := configMap[configSettings[0]] res, ok := configMap[configSettings[0]]
if !ok { if !ok {
return fmt.Errorf("unable to find a setting with that name %s", configSettings[0]) return fmt.Errorf(noSettingsNamed, configSettings[0])
} }
value := reflect.ValueOf(res) value := reflect.ValueOf(res)
@@ -308,6 +310,9 @@ func UpdateMap(configMap map[string]interface{}, configSettings []string, newVal
if len(configSettings) == 1 { if len(configSettings) == 1 {
return errors.New("unable to set multiple settings at once") return errors.New("unable to set multiple settings at once")
} }
if value.Len() == 0 {
return fmt.Errorf(noSettingsNamed, configSettings[1])
}
return UpdateMap(res.(map[string]interface{}), configSettings[1:], newVal) return UpdateMap(res.(map[string]interface{}), configSettings[1:], newVal)
case reflect.Int: case reflect.Int:
@@ -319,7 +324,7 @@ func UpdateMap(configMap map[string]interface{}, configSettings []string, newVal
configMap[configSettings[0]] = val configMap[configSettings[0]] = val
return nil return nil
} }
return fmt.Errorf("unable to find a setting with that name %s", configSettings[0]) return fmt.Errorf(noSettingsNamed, configSettings[0])
case reflect.Int64: case reflect.Int64:
if len(configSettings) == 1 { if len(configSettings) == 1 {
@@ -330,7 +335,7 @@ func UpdateMap(configMap map[string]interface{}, configSettings []string, newVal
configMap[configSettings[0]] = int64(val) configMap[configSettings[0]] = int64(val)
return nil return nil
} }
return fmt.Errorf("unable to find a setting with that name %s", configSettings[0]) return fmt.Errorf(noSettingsNamed, configSettings[0])
case reflect.Bool: case reflect.Bool:
if len(configSettings) == 1 { if len(configSettings) == 1 {
@@ -341,21 +346,21 @@ func UpdateMap(configMap map[string]interface{}, configSettings []string, newVal
configMap[configSettings[0]] = val configMap[configSettings[0]] = val
return nil return nil
} }
return fmt.Errorf("unable to find a setting with that name %s", configSettings[0]) return fmt.Errorf(noSettingsNamed, configSettings[0])
case reflect.String: case reflect.String:
if len(configSettings) == 1 { if len(configSettings) == 1 {
configMap[configSettings[0]] = newVal[0] configMap[configSettings[0]] = newVal[0]
return nil return nil
} }
return fmt.Errorf("unable to find a setting with that name %s", configSettings[0]) return fmt.Errorf(noSettingsNamed, configSettings[0])
case reflect.Slice: case reflect.Slice:
if len(configSettings) == 1 { if len(configSettings) == 1 {
configMap[configSettings[0]] = newVal configMap[configSettings[0]] = newVal
return nil return nil
} }
return fmt.Errorf("unable to find a setting with that name %s", configSettings[0]) return fmt.Errorf(noSettingsNamed, configSettings[0])
default: default:
return errors.New("type not supported yet") return errors.New("type not supported yet")

Просмотреть файл

@@ -141,6 +141,12 @@ func TestConfigSet(t *testing.T) {
assert.NotContains(t, string(output), "invalid-key") assert.NotContains(t, string(output), "invalid-key")
}) })
t.Run("Error when the parameter of an unknown plugin is set", func(t *testing.T) {
output, err := th.RunCommandWithOutput(t, "config", "set", "PluginSettings.Plugins.someplugin", "true")
assert.Error(t, err)
assert.NotContains(t, string(output), "panic")
})
t.Run("Error when the wrong locale is set", func(t *testing.T) { t.Run("Error when the wrong locale is set", func(t *testing.T) {
th.CheckCommand(t, "config", "set", "LocalizationSettings.DefaultServerLocale", "es") th.CheckCommand(t, "config", "set", "LocalizationSettings.DefaultServerLocale", "es")
assert.Error(t, th.RunCommand(t, "config", "set", "LocalizationSettings.DefaultServerLocale", "invalid-key")) assert.Error(t, th.RunCommand(t, "config", "set", "LocalizationSettings.DefaultServerLocale", "invalid-key"))

Просмотреть файл

@@ -8,6 +8,7 @@ import (
"database/sql" "database/sql"
"io/ioutil" "io/ioutil"
"net/url" "net/url"
"regexp"
"strings" "strings"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
@@ -22,6 +23,8 @@ import (
_ "github.com/lib/pq" _ "github.com/lib/pq"
) )
var tcpStripper = regexp.MustCompile(`@tcp\((.*)\)`)
// DatabaseStore is a config store backed by a database. // DatabaseStore is a config store backed by a database.
type DatabaseStore struct { type DatabaseStore struct {
commonStore commonStore
@@ -101,23 +104,22 @@ func initializeConfigurationsTable(db *sqlx.DB) error {
// By contrast, a Postgres DSN is returned unmodified. // By contrast, a Postgres DSN is returned unmodified.
func parseDSN(dsn string) (string, string, error) { func parseDSN(dsn string) (string, string, error) {
// Treat the DSN as the URL that it is. // Treat the DSN as the URL that it is.
u, err := url.Parse(dsn) s := strings.SplitN(dsn, "://", 2)
if err != nil { if len(s) != 2 {
return "", "", errors.Wrap(err, "failed to parse DSN as URL") errors.New("failed to parse DSN as URL")
} }
scheme := u.Scheme scheme := s[0]
switch scheme { switch scheme {
case "mysql": case "mysql":
// Strip off the mysql:// for the dsn with which to connect. // Strip off the mysql:// for the dsn with which to connect.
u.Scheme = "" dsn = s[1]
dsn = strings.TrimPrefix(u.String(), "//")
case "postgres": case "postgres":
// No changes required // No changes required
default: default:
return "", "", errors.Wrapf(err, "unsupported scheme %s", scheme) return "", "", errors.Errorf("unsupported scheme %s", scheme)
} }
return scheme, dsn, nil return scheme, dsn, nil
@@ -293,7 +295,11 @@ func (ds *DatabaseStore) RemoveFile(name string) error {
// String returns the path to the database backing the config, masking the password. // String returns the path to the database backing the config, masking the password.
func (ds *DatabaseStore) String() string { func (ds *DatabaseStore) String() string {
u, _ := url.Parse(ds.originalDsn) // Remove @tcp and the parentheses from the host and parse the rest as a URL
u, err := url.Parse(tcpStripper.ReplaceAllString(ds.originalDsn, `@$1`))
if err != nil {
return "(omitted due to error parsing the DSN)"
}
// Strip out the password to avoid leaking in logs. // Strip out the password to avoid leaking in logs.
u.User = url.User(u.User.Username()) u.User = url.User(u.User.Username())

Просмотреть файл

@@ -6,7 +6,6 @@ package config_test
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"net/url"
"os" "os"
"strings" "strings"
"testing" "testing"
@@ -137,6 +136,11 @@ func TestDatabaseStoreNew(t *testing.T) {
_, err := config.NewDatabaseStore("invalid") _, err := config.NewDatabaseStore("invalid")
require.Error(t, err) require.Error(t, err)
}) })
t.Run("unsupported scheme with valid data source", func(t *testing.T) {
_, err := config.NewDatabaseStore(fmt.Sprintf("invalid://%s", *sqlSettings.DataSource))
require.Error(t, err)
})
} }
func TestDatabaseStoreGet(t *testing.T) { func TestDatabaseStoreGet(t *testing.T) {
@@ -930,14 +934,11 @@ func TestDatabaseStoreString(t *testing.T) {
sqlSettings := mainHelper.GetSqlSettings() sqlSettings := mainHelper.GetSqlSettings()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource)) ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource))
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, ds)
defer ds.Close() defer ds.Close()
actualStringURL, err := url.Parse(ds.String()) maskedDSN := ds.String()
require.NoError(t, err) assert.True(t, strings.HasPrefix(maskedDSN, "mysql://"))
assert.True(t, strings.Contains(maskedDSN, "mmuser"))
assert.Equal(t, *sqlSettings.DriverName, actualStringURL.Scheme) assert.False(t, strings.Contains(maskedDSN, "mostest"))
actualUsername := actualStringURL.User.Username()
actualPassword, _ := actualStringURL.User.Password()
assert.NotEmpty(t, actualUsername)
assert.Empty(t, actualPassword, "should mask password")
} }

Просмотреть файл

@@ -44,4 +44,5 @@ type MetricsInterface interface {
IncrementPostsSearchCounter() IncrementPostsSearchCounter()
ObservePostsSearchDuration(elapsed float64) ObservePostsSearchDuration(elapsed float64)
ObserveStoreMethodDuration(method string, success string, elapsed float64) ObserveStoreMethodDuration(method string, success string, elapsed float64)
ObserveApiEndpointDuration(endpoint string, elapsed float64)
} }

Просмотреть файл

@@ -395,26 +395,31 @@ func (g *hooksRPCClient) FileWillBeUploaded(c *Context, info *model.FileInfo, fi
serveIOReader(file, uploadedFileConnection) serveIOReader(file, uploadedFileConnection)
}() }()
replacementDone := make(chan bool)
replacementFileStreamId := g.muxBroker.NextId() replacementFileStreamId := g.muxBroker.NextId()
go func() { go func() {
defer close(replacementDone)
replacementFileConnection, err := g.muxBroker.Accept(replacementFileStreamId) replacementFileConnection, err := g.muxBroker.Accept(replacementFileStreamId)
if err != nil { if err != nil {
g.log.Error("Plugin failed to serve replacement file stream. MuxBroker could not Accept connection", mlog.Err(err)) g.log.Error("Plugin failed to serve replacement file stream. MuxBroker could not Accept connection", mlog.Err(err))
return return
} }
defer replacementFileConnection.Close() defer replacementFileConnection.Close()
if _, err := io.Copy(output, replacementFileConnection); err != nil && err != io.EOF { if _, err := io.Copy(output, replacementFileConnection); err != nil {
g.log.Error("Error reading replacement file.", mlog.Err(err)) g.log.Error("Error reading replacement file.", mlog.Err(err))
} }
}() }()
_args := &Z_FileWillBeUploadedArgs{c, info, uploadedFileStreamId, replacementFileStreamId} _args := &Z_FileWillBeUploadedArgs{c, info, uploadedFileStreamId, replacementFileStreamId}
_returns := &Z_FileWillBeUploadedReturns{A: _args.B} _returns := &Z_FileWillBeUploadedReturns{A: _args.B}
if g.implemented[FileWillBeUploadedId] {
if err := g.client.Call("Plugin.FileWillBeUploaded", _args, _returns); err != nil { if err := g.client.Call("Plugin.FileWillBeUploaded", _args, _returns); err != nil {
g.log.Error("RPC call FileWillBeUploaded to plugin failed.", mlog.Err(err)) g.log.Error("RPC call FileWillBeUploaded to plugin failed.", mlog.Err(err))
} }
}
// Ensure the io.Copy from the replacementFileConnection above completes.
<-replacementDone
return _returns.A, _returns.B return _returns.A, _returns.B
} }

Просмотреть файл

@@ -6,6 +6,9 @@ package web
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"reflect"
"runtime"
"strings"
"time" "time"
"github.com/NYTimes/gziphandler" "github.com/NYTimes/gziphandler"
@@ -16,10 +19,20 @@ import (
"github.com/mattermost/mattermost-server/utils" "github.com/mattermost/mattermost-server/utils"
) )
func GetHandlerName(h func(*Context, http.ResponseWriter, *http.Request)) string {
handlerName := runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name()
pos := strings.LastIndex(handlerName, ".")
if pos != -1 && len(handlerName) > pos {
handlerName = handlerName[pos+1:]
}
return handlerName
}
func (w *Web) NewHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { func (w *Web) NewHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
return &Handler{ return &Handler{
GetGlobalAppOptions: w.GetGlobalAppOptions, GetGlobalAppOptions: w.GetGlobalAppOptions,
HandleFunc: h, HandleFunc: h,
HandlerName: GetHandlerName(h),
RequireSession: false, RequireSession: false,
TrustRequester: false, TrustRequester: false,
RequireMfa: false, RequireMfa: false,
@@ -35,6 +48,7 @@ func (w *Web) NewStaticHandler(h func(*Context, http.ResponseWriter, *http.Reque
return &Handler{ return &Handler{
GetGlobalAppOptions: w.GetGlobalAppOptions, GetGlobalAppOptions: w.GetGlobalAppOptions,
HandleFunc: h, HandleFunc: h,
HandlerName: GetHandlerName(h),
RequireSession: false, RequireSession: false,
TrustRequester: false, TrustRequester: false,
RequireMfa: false, RequireMfa: false,
@@ -47,6 +61,7 @@ func (w *Web) NewStaticHandler(h func(*Context, http.ResponseWriter, *http.Reque
type Handler struct { type Handler struct {
GetGlobalAppOptions app.AppOptionCreator GetGlobalAppOptions app.AppOptionCreator
HandleFunc func(*Context, http.ResponseWriter, *http.Request) HandleFunc func(*Context, http.ResponseWriter, *http.Request)
HandlerName string
RequireSession bool RequireSession bool
TrustRequester bool TrustRequester bool
RequireMfa bool RequireMfa bool
@@ -192,6 +207,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != model.API_URL_SUFFIX+"/websocket" { if r.URL.Path != model.API_URL_SUFFIX+"/websocket" {
elapsed := float64(time.Since(now)) / float64(time.Second) elapsed := float64(time.Since(now)) / float64(time.Second)
c.App.Metrics.ObserveHttpRequestDuration(elapsed) c.App.Metrics.ObserveHttpRequestDuration(elapsed)
c.App.Metrics.ObserveApiEndpointDuration(h.HandlerName, elapsed)
} }
} }
} }
@@ -249,6 +265,7 @@ func (w *Web) ApiHandler(h func(*Context, http.ResponseWriter, *http.Request)) h
handler := &Handler{ handler := &Handler{
GetGlobalAppOptions: w.GetGlobalAppOptions, GetGlobalAppOptions: w.GetGlobalAppOptions,
HandleFunc: h, HandleFunc: h,
HandlerName: GetHandlerName(h),
RequireSession: false, RequireSession: false,
TrustRequester: false, TrustRequester: false,
RequireMfa: false, RequireMfa: false,
@@ -267,6 +284,7 @@ func (w *Web) ApiHandlerTrustRequester(h func(*Context, http.ResponseWriter, *ht
handler := &Handler{ handler := &Handler{
GetGlobalAppOptions: w.GetGlobalAppOptions, GetGlobalAppOptions: w.GetGlobalAppOptions,
HandleFunc: h, HandleFunc: h,
HandlerName: GetHandlerName(h),
RequireSession: false, RequireSession: false,
TrustRequester: true, TrustRequester: true,
RequireMfa: false, RequireMfa: false,
@@ -284,6 +302,7 @@ func (w *Web) ApiSessionRequired(h func(*Context, http.ResponseWriter, *http.Req
handler := &Handler{ handler := &Handler{
GetGlobalAppOptions: w.GetGlobalAppOptions, GetGlobalAppOptions: w.GetGlobalAppOptions,
HandleFunc: h, HandleFunc: h,
HandlerName: GetHandlerName(h),
RequireSession: true, RequireSession: true,
TrustRequester: false, TrustRequester: false,
RequireMfa: true, RequireMfa: true,
@@ -302,6 +321,7 @@ func (w *Web) apiHandlerTrustRequester(h func(*Context, http.ResponseWriter, *ht
handler := &Handler{ handler := &Handler{
GetGlobalAppOptions: w.GetGlobalAppOptions, GetGlobalAppOptions: w.GetGlobalAppOptions,
HandleFunc: h, HandleFunc: h,
HandlerName: GetHandlerName(h),
RequireSession: false, RequireSession: false,
TrustRequester: true, TrustRequester: true,
RequireMfa: false, RequireMfa: false,