mirror of
https://github.com/mattermost/mattermost.git
synced 2026-02-03 20:40:00 -05:00
Mono repo -> Master (#22553)
Combines the following repositories into one: https://github.com/mattermost/mattermost-server https://github.com/mattermost/mattermost-webapp https://github.com/mattermost/focalboard https://github.com/mattermost/mattermost-plugin-playbooks
This commit is contained in:
parent
b61c096497
commit
c943ed6859
13276 changed files with 1695615 additions and 223189 deletions
5
.github/codeql/codeql-config.yml
vendored
5
.github/codeql/codeql-config.yml
vendored
|
|
@ -7,8 +7,13 @@ query-filters:
|
|||
- recommendation
|
||||
- exclude:
|
||||
id: go/log-injection
|
||||
- exclude:
|
||||
id: js/insecure-randomness
|
||||
|
||||
paths-ignore:
|
||||
- templates
|
||||
- tests
|
||||
- 'api4/*_local.go'
|
||||
- webapp/channels/e2e
|
||||
- webapp/channels/tests
|
||||
- '**/*.test.*'
|
||||
196
.github/workflows/channels-ci.yml
vendored
Normal file
196
.github/workflows/channels-ci.yml
vendored
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
name: Web App CI
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- mono-repo*
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
jobs:
|
||||
check-lint:
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: webapp
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- name: ci/setup-node
|
||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||
id: setup_node
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: npm
|
||||
cache-dependency-path: 'webapp/package-lock.json'
|
||||
# - uses: actions/cache@2b8105bdae4b746666ee225970c4a281bbd9d51f # v3.2.4
|
||||
# id: npm-cache
|
||||
# with:
|
||||
# path: |
|
||||
# '**/node_modules'
|
||||
# 'e2e/playwright/node_modules'
|
||||
# 'e2e/cypress/node_modules'
|
||||
# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('e2e/cypress/package-lock.json') }}-${{ hashFiles('e2e/playwright/package-lock.json') }}
|
||||
- name: ci/get-node-modules
|
||||
# if: steps.npm-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
make node_modules
|
||||
# make channels/e2e/playwright/node_modules
|
||||
- name: ci/lint
|
||||
working-directory: webapp/channels
|
||||
run: |
|
||||
npm run check
|
||||
check-i18n:
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: webapp
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- name: ci/setup-node
|
||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||
id: setup_node
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: npm
|
||||
cache-dependency-path: 'webapp/package-lock.json'
|
||||
# - uses: actions/cache@2b8105bdae4b746666ee225970c4a281bbd9d51f # v3.2.4
|
||||
# id: npm-cache
|
||||
# with:
|
||||
# path: |
|
||||
# '**/node_modules'
|
||||
# 'e2e/playwright/node_modules'
|
||||
# 'e2e/cypress/node_modules'
|
||||
# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('e2e/cypress/package-lock.json') }}-${{ hashFiles('e2e/playwright/package-lock.json') }}
|
||||
- name: ci/get-node-modules
|
||||
# if: steps.npm-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
make node_modules
|
||||
# make channels/e2e/playwright/node_modules
|
||||
- name: ci/lint
|
||||
working-directory: webapp/channels
|
||||
run: |
|
||||
cp src/i18n/en.json /tmp/en.json
|
||||
mkdir -p /tmp/fake-mobile-dir/assets/base/i18n/
|
||||
echo '{}' > /tmp/fake-mobile-dir/assets/base/i18n/en.json
|
||||
npm run mmjstool -- i18n extract-webapp --webapp-dir ./src --mobile-dir /tmp/fake-mobile-dir
|
||||
diff /tmp/en.json src/i18n/en.json
|
||||
# Address weblate behavior which does not remove whole translation item when translation string is set to empty
|
||||
npm run mmjstool -- i18n clean-empty --webapp-dir ./src --mobile-dir /tmp/fake-mobile-dir --check
|
||||
npm run mmjstool -- i18n check-empty-src --webapp-dir ./src --mobile-dir /tmp/fake-mobile-dir
|
||||
rm -rf tmp
|
||||
check-type:
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: webapp
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- name: ci/setup-node
|
||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||
id: setup_node
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: npm
|
||||
cache-dependency-path: 'webapp/package-lock.json'
|
||||
# - uses: actions/cache@2b8105bdae4b746666ee225970c4a281bbd9d51f # v3.2.4
|
||||
# id: npm-cache
|
||||
# with:
|
||||
# path: |
|
||||
# '**/node_modules'
|
||||
# 'e2e/playwright/node_modules'
|
||||
# 'e2e/cypress/node_modules'
|
||||
# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('e2e/cypress/package-lock.json') }}-${{ hashFiles('e2e/playwright/package-lock.json') }}
|
||||
- name: ci/get-node-modules
|
||||
# if: steps.npm-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
make node_modules
|
||||
# make channels/e2e/playwright/node_modules
|
||||
- name: ci/lint
|
||||
working-directory: webapp/channels
|
||||
run: |
|
||||
npm run check-types
|
||||
tests:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [check-type, check-i18n, check-lint]
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: webapp
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- name: ci/setup-node
|
||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||
id: setup_node
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: npm
|
||||
cache-dependency-path: 'webapp/package-lock.json'
|
||||
# - uses: actions/cache@2b8105bdae4b746666ee225970c4a281bbd9d51f # v3.2.4
|
||||
# id: npm-cache
|
||||
# with:
|
||||
# path: |
|
||||
# '**/node_modules'
|
||||
# 'e2e/playwright/node_modules'
|
||||
# 'e2e/cypress/node_modules'
|
||||
# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('e2e/cypress/package-lock.json') }}-${{ hashFiles('e2e/playwright/package-lock.json') }}
|
||||
- name: ci/get-node-modules
|
||||
# if: steps.npm-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
make node_modules
|
||||
# make channels/e2e/playwright/node_modules
|
||||
- name: ci/detect-cpu-core
|
||||
id: cpu-cores
|
||||
uses: SimenB/github-actions-cpu-cores@c508d404ab007faae80a014072fd8c0e17792118 # v1.1.0
|
||||
- name: ci/test
|
||||
working-directory: webapp/channels
|
||||
run: |
|
||||
npm run test-ci -- --maxWorkers=${{ steps.cpu-cores.outputs.count }}
|
||||
- name: ci/publish-test-results
|
||||
uses: EnricoMi/publish-unit-test-result-action@a3caf02865c0604ad3dc1ecfcc5cdec9c41b7936 # v2.3.0
|
||||
if: always()
|
||||
with:
|
||||
junit_files: "webapp/channels/build/**/*.xml"
|
||||
comment_mode: always
|
||||
compare_to_earlier_commit: false
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: webapp
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- name: ci/setup-node
|
||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||
id: setup_node
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: npm
|
||||
cache-dependency-path: 'webapp/package-lock.json'
|
||||
# - uses: actions/cache@2b8105bdae4b746666ee225970c4a281bbd9d51f # v3.2.4
|
||||
# id: npm-cache
|
||||
# with:
|
||||
# path: |
|
||||
# '**/node_modules'
|
||||
# 'e2e/playwright/node_modules'
|
||||
# 'e2e/cypress/node_modules'
|
||||
# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('e2e/cypress/package-lock.json') }}-${{ hashFiles('e2e/playwright/package-lock.json') }}
|
||||
- name: ci/get-node-modules
|
||||
# if: steps.npm-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
make node_modules
|
||||
# make channels/e2e/playwright/node_modules
|
||||
- name: ci/build
|
||||
working-directory: webapp/channels
|
||||
run: |
|
||||
npm run build
|
||||
# run-performance-bechmarks:
|
||||
# uses: ./.github/workflows/performance-benchmarks.yml
|
||||
# needs: build
|
||||
103
.github/workflows/ci.yml
vendored
103
.github/workflows/ci.yml
vendored
|
|
@ -6,10 +6,14 @@ on:
|
|||
- master
|
||||
- cloud
|
||||
- release-*
|
||||
- mono-repo*
|
||||
jobs:
|
||||
check-mocks:
|
||||
name: Check mocks
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: server
|
||||
steps:
|
||||
- name: Checkout mattermost-server
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
|
@ -20,6 +24,9 @@ jobs:
|
|||
check-go-mod-tidy:
|
||||
name: Check go mod tidy
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: server
|
||||
steps:
|
||||
- name: Checkout mattermost-server
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
|
@ -30,6 +37,9 @@ jobs:
|
|||
check-gen-serialized:
|
||||
name: Check serialization methods for hot structs
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: server
|
||||
steps:
|
||||
- name: Checkout mattermost-server
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
|
@ -40,27 +50,24 @@ jobs:
|
|||
check-mattermost-vet:
|
||||
name: Check style
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
defaults:
|
||||
run:
|
||||
working-directory: server
|
||||
steps:
|
||||
- name: Checkout mattermost-server
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- name: Checkout focalboard
|
||||
run: |
|
||||
cd ..
|
||||
git clone --depth=1 --no-single-branch https://github.com/mattermost/focalboard.git
|
||||
cd focalboard
|
||||
git checkout $GITHUB_HEAD_REF || git checkout $GITHUB_BASE_REF || git checkout rolling-stable
|
||||
echo $(git rev-parse HEAD)
|
||||
cd ../mattermost-server
|
||||
make setup-go-work
|
||||
- name: Reset config
|
||||
run: make config-reset
|
||||
- name: Run plugin-checker
|
||||
run: make plugin-checker
|
||||
- name: Run mattermost-vet
|
||||
run: make vet BUILD_NUMBER='${GITHUB_HEAD_REF}' MM_NO_ENTERPRISE_LINT=true MM_VET_OPENSPEC_PATH='${PWD}/../mattermost-api-reference/v4/html/static/mattermost-openapi-v4.yaml'
|
||||
run: make vet BUILD_NUMBER='${GITHUB_HEAD_REF}' MM_NO_ENTERPRISE_LINT=true MM_VET_OPENSPEC_PATH='${PWD}/../../mattermost-api-reference/v4/html/static/mattermost-openapi-v4.yaml'
|
||||
check-migrations:
|
||||
name: Check migration files
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: server
|
||||
steps:
|
||||
- name: Checkout mattermost-server
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
|
@ -86,6 +93,9 @@ jobs:
|
|||
check-generate-work-templates:
|
||||
name: Generate work templates
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: server
|
||||
steps:
|
||||
- name: Checkout mattermost-server
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
|
@ -96,6 +106,9 @@ jobs:
|
|||
check-email-templates:
|
||||
name: Generate email templates
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: server
|
||||
steps:
|
||||
- name: Checkout mattermost-server
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
|
@ -108,6 +121,9 @@ jobs:
|
|||
check-store-layers:
|
||||
name: Check store layers
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: server
|
||||
steps:
|
||||
- name: Checkout mattermost-server
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
|
@ -118,6 +134,9 @@ jobs:
|
|||
check-app-layers:
|
||||
name: Check app layers
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: server
|
||||
steps:
|
||||
- name: Checkout mattermost-server
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
|
@ -149,51 +168,13 @@ jobs:
|
|||
build-mattermost-server:
|
||||
name: Build mattermost-server
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
defaults:
|
||||
run:
|
||||
working-directory: server
|
||||
needs: check-mattermost-vet
|
||||
steps:
|
||||
- name: Checkout mattermost-server
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- name: Checkout mattermost-webapp
|
||||
run: |
|
||||
cd ..
|
||||
git clone --depth=1 --no-single-branch https://github.com/mattermost/mattermost-webapp.git
|
||||
cd mattermost-webapp
|
||||
git checkout $GITHUB_HEAD_REF || git checkout master
|
||||
export WEBAPP_GIT_COMMIT=$(git rev-parse HEAD)
|
||||
echo "$WEBAPP_GIT_COMMIT"
|
||||
FILE_DIST=dist.tar.gz
|
||||
runtime="2 minute"
|
||||
endtime=$(date -ud "$runtime" +%s)
|
||||
while [[ $(date -u +%s) -le $endtime ]]; do
|
||||
if curl -s --max-time 30 -f -o $FILE_DIST https://pr-builds.mattermost.com/mattermost-webapp/commit/$WEBAPP_GIT_COMMIT/mattermost-webapp.tar.gz; then
|
||||
break
|
||||
fi
|
||||
echo "Waiting for webapp git commit $WEBAPP_GIT_COMMIT with sleep 5: `date +%H:%M:%S`"
|
||||
sleep 5
|
||||
done
|
||||
if [[ -f "$FILE_DIST" ]]; then
|
||||
echo "Precompiled version of web app found"
|
||||
mkdir dist && tar -xf $FILE_DIST -C dist --strip-components=1
|
||||
else
|
||||
echo "Building web app from source"
|
||||
make dist
|
||||
fi
|
||||
cd ../mattermost-server
|
||||
- name: Checkout and build focalboard
|
||||
run: |
|
||||
cd ..
|
||||
git clone --depth=1 --no-single-branch https://github.com/mattermost/focalboard.git
|
||||
cd focalboard
|
||||
git checkout $GITHUB_HEAD_REF || git checkout $GITHUB_BASE_REF || git checkout rolling-stable
|
||||
echo $(git rev-parse HEAD)
|
||||
make server-linux
|
||||
echo "Building Boards product for web app"
|
||||
# make prebuild build-product # TODO figure out how to get this to run without bypassing the Makefile
|
||||
make prebuild
|
||||
cd mattermost-plugin/webapp
|
||||
npm run build:product
|
||||
cd ../../../mattermost-server
|
||||
make setup-go-work
|
||||
- name: Build
|
||||
run: |
|
||||
make config-reset
|
||||
|
|
@ -203,22 +184,18 @@ jobs:
|
|||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
with:
|
||||
name: server-dist-artifact
|
||||
path: dist/
|
||||
path: server/dist/
|
||||
retention-days: 14
|
||||
- name: Persist build artifacts
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
with:
|
||||
name: server-build-artifact
|
||||
path: build/
|
||||
path: server/build/
|
||||
retention-days: 14
|
||||
upload-s3:
|
||||
name: Upload to S3 bucket
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- build-mattermost-server
|
||||
- test-mysql
|
||||
- test-postgres-binary
|
||||
- test-postgres-normal
|
||||
needs: build-mattermost-server
|
||||
env:
|
||||
REPO_NAME: ${{ github.event.repository.name }}
|
||||
steps:
|
||||
|
|
@ -226,7 +203,7 @@ jobs:
|
|||
uses: actions/download-artifact@e9ef242655d12993efdcda9058dee2db83a2cb9b # v3.0.2
|
||||
with:
|
||||
name: server-dist-artifact
|
||||
path: dist/
|
||||
path: server/dist/
|
||||
- name: Configure AWS
|
||||
uses: aws-actions/configure-aws-credentials@07c2f971bac433df982ccc261983ae443861db49 # v1-node16
|
||||
with:
|
||||
|
|
@ -242,8 +219,8 @@ jobs:
|
|||
run: echo "BRANCH_NAME=$(echo ${{ steps.branch.outputs.sanitized-branch-name }} | sed 's/^pull\//PR-/g')" >> $GITHUB_ENV
|
||||
- name: ci/artifact-upload
|
||||
run: |
|
||||
aws s3 cp dist/ s3://pr-builds.mattermost.com/$REPO_NAME/$BRANCH_NAME/ --acl public-read --cache-control "no-cache" --recursive
|
||||
aws s3 cp dist/ s3://pr-builds.mattermost.com/$REPO_NAME/commit/${{ github.sha }}/ --acl public-read --cache-control "no-cache" --recursive
|
||||
aws s3 cp server/dist/ s3://pr-builds.mattermost.com/$REPO_NAME/$BRANCH_NAME/ --acl public-read --cache-control "no-cache" --recursive
|
||||
aws s3 cp server/dist/ s3://pr-builds.mattermost.com/$REPO_NAME/commit/${{ github.sha }}/ --acl public-read --cache-control "no-cache" --recursive
|
||||
build-docker:
|
||||
name: Build docker image
|
||||
runs-on: ubuntu-22.04
|
||||
|
|
@ -253,7 +230,7 @@ jobs:
|
|||
uses: actions/download-artifact@e9ef242655d12993efdcda9058dee2db83a2cb9b # v3.0.2
|
||||
with:
|
||||
name: server-build-artifact
|
||||
path: build/
|
||||
path: server/build/
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@3da7dc6e2b31f99ef2cb9fb4c50fb0971e0d0139 # v2.1.0
|
||||
with:
|
||||
|
|
@ -266,7 +243,7 @@ jobs:
|
|||
DOCKER_CLI_EXPERIMENTAL: enabled
|
||||
run: |
|
||||
export TAG=$(echo "${{ github.event.pull_request.head.sha || github.sha }}" | cut -c1-7)
|
||||
cd build
|
||||
cd server/build
|
||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
export MM_PACKAGE=https://pr-builds.mattermost.com/mattermost-server/commit/${GITHUB_SHA}/mattermost-team-linux-amd64.tar.gz
|
||||
docker buildx build --push --build-arg MM_PACKAGE=$MM_PACKAGE -t mattermost/mm-te-test:${TAG} .
|
||||
|
|
|
|||
1
.github/workflows/codeql-analysis.yml
vendored
1
.github/workflows/codeql-analysis.yml
vendored
|
|
@ -36,6 +36,7 @@ jobs:
|
|||
|
||||
- name: Build
|
||||
run: |
|
||||
cd server
|
||||
make setup-go-work
|
||||
make build-linux-amd64
|
||||
|
||||
|
|
|
|||
58
.github/workflows/e2e-ci.yml
vendored
Normal file
58
.github/workflows/e2e-ci.yml
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
name: mattermost-e2e
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- mono-repo*
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
jobs:
|
||||
cypress-check:
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: e2e/cypress
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- name: ci/setup-node
|
||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||
id: setup_node
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: npm
|
||||
cache-dependency-path: 'e2e/cypress/package-lock.json'
|
||||
- name: ci/cypress/npm-install
|
||||
run: |
|
||||
npm ci
|
||||
- name: ci/cypress/npm-check
|
||||
run: |
|
||||
npm run check
|
||||
playwright-check:
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: e2e/playwright
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- name: ci/setup-node
|
||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||
id: setup_node
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: npm
|
||||
cache-dependency-path: 'e2e/playwright/package-lock.json'
|
||||
- name: ci/get-webapp-node-modules
|
||||
working-directory: webapp
|
||||
# requires build of client and types
|
||||
run: |
|
||||
make node_modules
|
||||
- name: ci/playwright/npm-install
|
||||
run: |
|
||||
npm ci
|
||||
- name: ci/playwright/npm-check
|
||||
run: |
|
||||
npm run check
|
||||
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- name: Run docker compose
|
||||
run: |
|
||||
cd build
|
||||
cd 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';
|
||||
|
|
@ -32,17 +32,19 @@ jobs:
|
|||
docker run --net ghactions_mm-test appropriate/curl:latest sh -c "until curl --max-time 5 --output - http://elasticsearch:9200; do echo waiting for elasticsearch; sleep 5; done;"
|
||||
- name: Run Tests
|
||||
run: |
|
||||
if [[ ${{ github.event_name }} == 'push' ]]; then
|
||||
if [[ ${{ github.ref_name }} == 'master' ]]; then
|
||||
export RACE_MODE="-race"
|
||||
fi
|
||||
mkdir -p client/plugins
|
||||
cd build
|
||||
cd server/build
|
||||
docker run --net ghactions_mm-test \
|
||||
--ulimit nofile=8096:8096 \
|
||||
--env-file=dotenv/test.env \
|
||||
--env MM_SQLSETTINGS_DRIVERNAME="${{ inputs.drivername }}" \
|
||||
--env MM_SQLSETTINGS_DATASOURCE="${{ inputs.datasource }}" \
|
||||
--env TEST_DATABASE_MYSQL_DSN="${{ inputs.datasource }}" \
|
||||
--env TEST_DATABASE_POSTGRESQL_DSN="${{ inputs.datasource }}" \
|
||||
-v ~/work/mattermost-server:/mattermost-server \
|
||||
-w /mattermost-server/mattermost-server \
|
||||
-w /mattermost-server/mattermost-server/server \
|
||||
$BUILD_IMAGE \
|
||||
make test-server$RACE_MODE BUILD_NUMBER=$GITHUB_HEAD_REF-$GITHUB_RUN_ID TESTFLAGS= TESTFLAGSEE=
|
||||
|
|
|
|||
24
.gitignore
vendored
24
.gitignore
vendored
|
|
@ -22,9 +22,29 @@ config/config.json
|
|||
config/logging.json
|
||||
/plugins
|
||||
|
||||
# disable folders generated by Cypress
|
||||
e2e/cypress/node_modules
|
||||
e2e/cypress/tests/downloads
|
||||
e2e/cypress/tests/screenshots
|
||||
e2e/cypress/tests/videos
|
||||
e2e/cypress/tests/integration/benchmark/__benchmarks__
|
||||
e2e/cypress/tests/integration/performance/logs
|
||||
e2e/cypress/tests/fixtures/ldap_tmp
|
||||
e2e/cypress/tests/fixtures/mmctl
|
||||
e2e/cypress/results
|
||||
e2e/cypress/.eslintcache
|
||||
|
||||
# disable files/folders generated by Playwright
|
||||
e2e/playwright/node_modules
|
||||
e2e/playwright/playwright-report
|
||||
e2e/playwright/storage_state
|
||||
e2e/playwright/test-results
|
||||
e2e/playwright/tests/**/*-darwin.png
|
||||
e2e/playwright/tests/**/*-window.png
|
||||
e2e/playwright/.eslintcache
|
||||
|
||||
# Enterprise & products imports files
|
||||
imports/imports.go
|
||||
imports/boards_imports.go
|
||||
|
||||
# go.work file
|
||||
go.work
|
||||
|
|
@ -106,7 +126,7 @@ api/data/*
|
|||
api4/data/*
|
||||
app/data/*
|
||||
|
||||
enterprise
|
||||
/enterprise
|
||||
|
||||
cover.out
|
||||
ecover.out
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ stages:
|
|||
|
||||
include:
|
||||
- project: mattermost/ci/mattermost-server
|
||||
ref: master
|
||||
ref: monorepo-testing
|
||||
file: private.yml
|
||||
|
||||
variables:
|
||||
|
|
|
|||
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
16.10.0
|
||||
829
Makefile
829
Makefile
|
|
@ -1,829 +0,0 @@
|
|||
.PHONY: build package run stop run-client run-server run-haserver stop-haserver stop-client stop-server restart restart-server restart-client restart-haserver start-docker clean-dist clean nuke check-style check-client-style check-server-style check-unit-tests test dist run-client-tests setup-run-client-tests cleanup-run-client-tests test-client build-linux build-osx build-windows package-prep package-linux package-osx package-windows internal-test-web-client vet run-server-for-web-client-tests diff-config prepackaged-plugins prepackaged-binaries test-server test-server-ee test-server-quick test-server-race new-migration migrations-extract
|
||||
|
||||
ROOT := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
|
||||
ifeq ($(OS),Windows_NT)
|
||||
PLATFORM := Windows
|
||||
else
|
||||
PLATFORM := $(shell uname)
|
||||
endif
|
||||
|
||||
# Set an environment variable on Linux used to resolve `docker.host.internal` inconsistencies with
|
||||
# docker. This can be reworked once https://github.com/docker/for-linux/issues/264 is resolved
|
||||
# satisfactorily.
|
||||
ifeq ($(PLATFORM),Linux)
|
||||
export IS_LINUX = -linux
|
||||
else
|
||||
export IS_LINUX =
|
||||
endif
|
||||
|
||||
# Detect M1/M2 Macs and set a flag.
|
||||
ifeq ($(shell uname)/$(shell uname -m),Darwin/arm64)
|
||||
M1_MAC = true
|
||||
endif
|
||||
|
||||
define LICENSE_HEADER
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
endef
|
||||
|
||||
IS_CI ?= false
|
||||
# Build Flags
|
||||
BUILD_NUMBER ?= $(BUILD_NUMBER:)
|
||||
BUILD_DATE = $(shell date -u)
|
||||
BUILD_HASH = $(shell git rev-parse HEAD)
|
||||
|
||||
# Go tags
|
||||
GOTAGS ?= $(GOTAGS:)
|
||||
|
||||
# If we don't set the build number it defaults to dev
|
||||
ifeq ($(BUILD_NUMBER),)
|
||||
BUILD_DATE := n/a
|
||||
BUILD_NUMBER := dev
|
||||
endif
|
||||
|
||||
ifeq ($(BUILD_NUMBER),dev)
|
||||
export MM_FEATUREFLAGS_GRAPHQL = true
|
||||
GOTAGS += "testlicensekey"
|
||||
endif
|
||||
|
||||
# Enterprise
|
||||
BUILD_ENTERPRISE_DIR ?= ../enterprise
|
||||
BUILD_ENTERPRISE ?= true
|
||||
BUILD_ENTERPRISE_READY = false
|
||||
BUILD_TYPE_NAME = team
|
||||
BUILD_HASH_ENTERPRISE = none
|
||||
ifneq ($(wildcard $(BUILD_ENTERPRISE_DIR)/.),)
|
||||
ifeq ($(BUILD_ENTERPRISE),true)
|
||||
BUILD_ENTERPRISE_READY = true
|
||||
BUILD_TYPE_NAME = enterprise
|
||||
BUILD_HASH_ENTERPRISE = $(shell cd $(BUILD_ENTERPRISE_DIR) && git rev-parse HEAD)
|
||||
else
|
||||
BUILD_ENTERPRISE_READY = false
|
||||
BUILD_TYPE_NAME = team
|
||||
endif
|
||||
else
|
||||
BUILD_ENTERPRISE_READY = false
|
||||
BUILD_TYPE_NAME = team
|
||||
endif
|
||||
|
||||
# Webapp
|
||||
BUILD_WEBAPP_DIR ?= ../mattermost-webapp
|
||||
BUILD_CLIENT = false
|
||||
BUILD_HASH_CLIENT = independent
|
||||
ifneq ($(wildcard $(BUILD_WEBAPP_DIR)/.),)
|
||||
ifeq ($(BUILD_CLIENT),true)
|
||||
BUILD_CLIENT = true
|
||||
BUILD_HASH_CLIENT = $(shell cd $(BUILD_WEBAPP_DIR) && git rev-parse HEAD)
|
||||
else
|
||||
BUILD_CLIENT = false
|
||||
endif
|
||||
else
|
||||
BUILD_CLIENT = false
|
||||
endif
|
||||
|
||||
# Boards
|
||||
BUILD_BOARDS_DIR ?= ../focalboard
|
||||
BUILD_BOARDS ?= true
|
||||
BUILD_HASH_BOARDS = none
|
||||
ifneq ($(wildcard $(BUILD_BOARDS_DIR)/.),)
|
||||
ifeq ($(BUILD_BOARDS),true)
|
||||
BUILD_BOARDS = true
|
||||
BUILD_HASH_BOARDS = $(shell cd $(BUILD_BOARDS_DIR) && git rev-parse HEAD)
|
||||
else
|
||||
BUILD_BOARDS = false
|
||||
endif
|
||||
else
|
||||
BUILD_BOARDS = false
|
||||
endif
|
||||
|
||||
# Playbooks
|
||||
BUILD_PLAYBOOKS_DIR ?= ../mattermost-plugin-playbooks
|
||||
BUILD_PLAYBOOKS ?= false
|
||||
BUILD_HASH_PLAYBOOKS = none
|
||||
|
||||
ifneq ($(wildcard $(BUILD_PLAYBOOKS_DIR)/.),)
|
||||
ifeq ($(BUILD_PLAYBOOKS),true)
|
||||
BUILD_PLAYBOOKS = true
|
||||
BUILD_HASH_PLAYBOOKS = $(shell cd $(BUILD_PLAYBOOKS_DIR) && git rev-parse HEAD)
|
||||
else
|
||||
BUILD_PLAYBOOKS = false
|
||||
endif
|
||||
else
|
||||
BUILD_PLAYBOOKS = false
|
||||
endif
|
||||
|
||||
# We need current user's UID for `run-haserver` so docker compose does not run server
|
||||
# as root and mess up file permissions for devs. When running like this HOME will be blank
|
||||
# and docker will add '/', so we need to set the go-build cache location or we'll get
|
||||
# permission errors on build as it tries to create a cache in filesystem root.
|
||||
export CURRENT_UID = $(shell id -u):$(shell id -g)
|
||||
ifeq ($(HOME),/)
|
||||
export XDG_CACHE_HOME = /tmp/go-cache/
|
||||
endif
|
||||
|
||||
# Go Flags
|
||||
GOFLAGS ?= $(GOFLAGS:)
|
||||
# We need to export GOBIN to allow it to be set
|
||||
# for processes spawned from the Makefile
|
||||
export GOBIN ?= $(PWD)/bin
|
||||
GO=go
|
||||
DELVE=dlv
|
||||
LDFLAGS += -X "github.com/mattermost/mattermost-server/v6/model.BuildNumber=$(BUILD_NUMBER)"
|
||||
LDFLAGS += -X "github.com/mattermost/mattermost-server/v6/model.BuildDate=$(BUILD_DATE)"
|
||||
LDFLAGS += -X "github.com/mattermost/mattermost-server/v6/model.BuildHash=$(BUILD_HASH)"
|
||||
LDFLAGS += -X "github.com/mattermost/mattermost-server/v6/model.BuildHashEnterprise=$(BUILD_HASH_ENTERPRISE)"
|
||||
LDFLAGS += -X "github.com/mattermost/mattermost-server/v6/model.BuildEnterpriseReady=$(BUILD_ENTERPRISE_READY)"
|
||||
LDFLAGS += -X "github.com/mattermost/mattermost-server/v6/model.BuildHashBoards=$(BUILD_HASH_BOARDS)"
|
||||
LDFLAGS += -X "github.com/mattermost/mattermost-server/v6/model.BuildBoards=$(BUILD_BOARDS)"
|
||||
LDFLAGS += -X "github.com/mattermost/mattermost-server/v6/model.BuildHashPlaybooks=$(BUILD_HASH_PLAYBOOKS)"
|
||||
|
||||
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
|
||||
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
|
||||
MINIMUM_SUPPORTED_GO_MAJOR_VERSION = 1
|
||||
MINIMUM_SUPPORTED_GO_MINOR_VERSION = 15
|
||||
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MINIMUM_SUPPORTED_GO_MAJOR_VERSION).$(MINIMUM_SUPPORTED_GO_MINOR_VERSION)
|
||||
|
||||
# GOOS/GOARCH of the build host, used to determine whether we're cross-compiling or not
|
||||
BUILDER_GOOS_GOARCH="$(shell $(GO) env GOOS)_$(shell $(GO) env GOARCH)"
|
||||
|
||||
PLATFORM_FILES="./cmd/mattermost"
|
||||
|
||||
# Output paths
|
||||
DIST_ROOT=dist
|
||||
DIST_PATH=$(DIST_ROOT)/mattermost
|
||||
DIST_PATH_LIN_AMD64=$(DIST_ROOT)/linux_amd64/mattermost
|
||||
DIST_PATH_LIN_ARM64=$(DIST_ROOT)/linux_arm64/mattermost
|
||||
DIST_PATH_OSX_AMD64=$(DIST_ROOT)/osx_amd64/mattermost
|
||||
DIST_PATH_OSX_ARM64=$(DIST_ROOT)/osx_arm64/mattermost
|
||||
DIST_PATH_WIN=$(DIST_ROOT)/windows/mattermost
|
||||
|
||||
# Tests
|
||||
TESTS=.
|
||||
|
||||
# Packages lists
|
||||
TE_PACKAGES=$(shell $(GO) list ./...)
|
||||
|
||||
TEMPLATES_DIR=templates
|
||||
|
||||
# Plugins Packages
|
||||
PLUGIN_PACKAGES ?= mattermost-plugin-antivirus-v0.1.2
|
||||
PLUGIN_PACKAGES += mattermost-plugin-autolink-v1.2.2
|
||||
PLUGIN_PACKAGES += mattermost-plugin-aws-SNS-v1.2.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-calls-v0.14.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-channel-export-v1.0.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-confluence-v1.3.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-custom-attributes-v1.3.1
|
||||
PLUGIN_PACKAGES += mattermost-plugin-github-v2.1.4
|
||||
PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.6.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-playbooks-v1.36.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-jenkins-v1.1.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-jira-v3.2.2
|
||||
PLUGIN_PACKAGES += mattermost-plugin-jitsi-v2.0.1
|
||||
PLUGIN_PACKAGES += mattermost-plugin-nps-v1.3.1
|
||||
PLUGIN_PACKAGES += mattermost-plugin-todo-v0.6.1
|
||||
PLUGIN_PACKAGES += mattermost-plugin-welcomebot-v1.2.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-zoom-v1.6.0
|
||||
PLUGIN_PACKAGES += focalboard-v7.9.1
|
||||
PLUGIN_PACKAGES += mattermost-plugin-apps-v1.2.0
|
||||
|
||||
# Prepares the enterprise build if exists. The IGNORE stuff is a hack to get the Makefile to execute the commands outside a target
|
||||
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
||||
IGNORE:=$(shell echo Enterprise build selected, preparing)
|
||||
IGNORE:=$(shell rm -f imports/imports.go)
|
||||
IGNORE:=$(shell cp $(BUILD_ENTERPRISE_DIR)/imports/imports.go imports/)
|
||||
IGNORE:=$(shell rm -f enterprise)
|
||||
else
|
||||
IGNORE:=$(shell rm -f imports/imports.go)
|
||||
endif
|
||||
|
||||
EE_PACKAGES=$(shell $(GO) list $(BUILD_ENTERPRISE_DIR)/...)
|
||||
|
||||
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
||||
ALL_PACKAGES=$(TE_PACKAGES) $(EE_PACKAGES)
|
||||
else
|
||||
ALL_PACKAGES=$(TE_PACKAGES)
|
||||
endif
|
||||
|
||||
# Prepare optional Boards build.
|
||||
BOARDS_PACKAGES=$(shell $(GO) list $(BUILD_BOARDS_DIR)/server/...)
|
||||
ifeq ($(BUILD_BOARDS),true)
|
||||
# We removed `ALL_PACKAGES += $(BOARDS_PACKAGES)` since board tests needs `-tag 'json1'` in the tests.
|
||||
# Adding that flag to server breaks the build with unsupported flag error.
|
||||
# PR: https://github.com/mattermost/mattermost-server/pull/20772
|
||||
# Ticket: https://mattermost.atlassian.net/browse/CLD-3800
|
||||
IGNORE:=$(shell echo Boards build selected, preparing)
|
||||
IGNORE:=$(shell rm -f imports/boards_imports.go)
|
||||
IGNORE:=$(shell cp $(BUILD_BOARDS_DIR)/mattermost-plugin/product/imports/boards_imports.go imports/)
|
||||
else
|
||||
IGNORE:=$(shell rm -f imports/boards_imports.go)
|
||||
endif
|
||||
|
||||
ifeq ($(BUILD_PLAYBOOKS),true)
|
||||
IGNORE:=$(shell cp $(BUILD_PLAYBOOKS_DIR)/product/imports/playbooks_imports.go imports/)
|
||||
else
|
||||
IGNORE:=$(shell rm -f imports/playbooks_imports.go)
|
||||
endif
|
||||
|
||||
all: run ## Alias for 'run'.
|
||||
|
||||
-include config.override.mk
|
||||
|
||||
# Make sure not to modify an overridden ENABLED_DOCKER_SERVICES variable
|
||||
DOCKER_SERVICES_OVERRIDE=false
|
||||
ifneq (,$(ENABLED_DOCKER_SERVICES))
|
||||
$(info ENABLED_DOCKER_SERVICES has been overridden)
|
||||
DOCKER_SERVICES_OVERRIDE=true
|
||||
endif
|
||||
|
||||
include config.mk
|
||||
include build/*.mk
|
||||
|
||||
LDFLAGS += -X "github.com/mattermost/mattermost-server/v6/model.MockCWS=$(MM_ENABLE_CWS_MOCK)"
|
||||
|
||||
RUN_IN_BACKGROUND ?=
|
||||
ifeq ($(RUN_SERVER_IN_BACKGROUND),true)
|
||||
RUN_IN_BACKGROUND := &
|
||||
endif
|
||||
|
||||
DOCKER_COMPOSE_OVERRIDE=
|
||||
ifneq ("$(wildcard ./docker-compose.override.yaml)","")
|
||||
DOCKER_COMPOSE_OVERRIDE=-f docker-compose.override.yaml
|
||||
endif
|
||||
|
||||
ifeq ($(M1_MAC),true)
|
||||
$(info M1 detected, applying elasticsearch override)
|
||||
DOCKER_COMPOSE_OVERRIDE := -f docker-compose.makefile.m1.yml $(DOCKER_COMPOSE_OVERRIDE)
|
||||
endif
|
||||
|
||||
ifneq ($(DOCKER_SERVICES_OVERRIDE),true)
|
||||
ifeq (,$(findstring minio,$(ENABLED_DOCKER_SERVICES)))
|
||||
TEMP_DOCKER_SERVICES:=$(TEMP_DOCKER_SERVICES) minio
|
||||
endif
|
||||
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
||||
ifeq (,$(findstring openldap,$(ENABLED_DOCKER_SERVICES)))
|
||||
TEMP_DOCKER_SERVICES:=$(TEMP_DOCKER_SERVICES) openldap
|
||||
endif
|
||||
ifeq (,$(findstring elasticsearch,$(ENABLED_DOCKER_SERVICES)))
|
||||
TEMP_DOCKER_SERVICES:=$(TEMP_DOCKER_SERVICES) elasticsearch
|
||||
endif
|
||||
endif
|
||||
ENABLED_DOCKER_SERVICES:=$(ENABLED_DOCKER_SERVICES) $(TEMP_DOCKER_SERVICES)
|
||||
endif
|
||||
|
||||
start-docker: ## Starts the docker containers for local development.
|
||||
ifneq ($(IS_CI),false)
|
||||
@echo CI Build: skipping docker start
|
||||
else ifeq ($(MM_NO_DOCKER),true)
|
||||
@echo No Docker Enabled: skipping docker start
|
||||
else
|
||||
@echo Starting docker containers
|
||||
|
||||
docker-compose rm start_dependencies
|
||||
$(GO) run ./build/docker-compose-generator/main.go $(ENABLED_DOCKER_SERVICES) | docker-compose -f docker-compose.makefile.yml -f /dev/stdin $(DOCKER_COMPOSE_OVERRIDE) run -T --rm start_dependencies
|
||||
ifneq (,$(findstring openldap,$(ENABLED_DOCKER_SERVICES)))
|
||||
cat tests/${LDAP_DATA}-data.ldif | docker-compose -f docker-compose.makefile.yml $(DOCKER_COMPOSE_OVERRIDE) exec -T openldap bash -c 'ldapadd -x -D "cn=admin,dc=mm,dc=test,dc=com" -w mostest || true';
|
||||
endif
|
||||
ifneq (,$(findstring mysql-read-replica,$(ENABLED_DOCKER_SERVICES)))
|
||||
./scripts/replica-mysql-config.sh
|
||||
endif
|
||||
endif
|
||||
|
||||
run-haserver:
|
||||
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
||||
@echo Starting mattermost in an HA topology '(3 node cluster)'
|
||||
|
||||
docker-compose -f docker-compose.yaml $(DOCKER_COMPOSE_OVERRIDE) up --remove-orphans haproxy
|
||||
endif
|
||||
|
||||
stop-haserver:
|
||||
@echo Stopping docker containers for HA topology
|
||||
docker-compose stop
|
||||
|
||||
stop-docker: ## Stops the docker containers for local development.
|
||||
ifeq ($(MM_NO_DOCKER),true)
|
||||
@echo No Docker Enabled: skipping docker stop
|
||||
else
|
||||
@echo Stopping docker containers
|
||||
|
||||
docker-compose stop
|
||||
endif
|
||||
|
||||
clean-docker: ## Deletes the docker containers for local development.
|
||||
ifeq ($(MM_NO_DOCKER),true)
|
||||
@echo No Docker Enabled: skipping docker clean
|
||||
else
|
||||
@echo Removing docker containers
|
||||
|
||||
docker-compose down -v
|
||||
docker-compose rm -v
|
||||
endif
|
||||
|
||||
plugin-checker:
|
||||
$(GO) run $(GOFLAGS) ./plugin/checker
|
||||
|
||||
prepackaged-plugins: ## Populate the prepackaged-plugins directory
|
||||
@echo Downloading prepackaged plugins
|
||||
mkdir -p prepackaged_plugins
|
||||
@cd prepackaged_plugins && for plugin_package in $(PLUGIN_PACKAGES) ; do \
|
||||
curl -f -O -L https://plugins-store.test.mattermost.com/release/$$plugin_package.tar.gz; \
|
||||
curl -f -O -L https://plugins-store.test.mattermost.com/release/$$plugin_package.tar.gz.sig; \
|
||||
done
|
||||
|
||||
prepackaged-binaries: ## Populate the prepackaged-binaries to the bin directory
|
||||
ifeq ($(shell test -f bin/mmctl && printf "yes"),yes)
|
||||
@echo "MMCTL already exists in bin/mmctl not downloading a new version."
|
||||
else
|
||||
@scripts/download_mmctl_release.sh
|
||||
endif
|
||||
|
||||
golangci-lint: ## Run golangci-lint on codebase
|
||||
@# Keep the version in sync with the command in .circleci/config.yml
|
||||
$(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.1
|
||||
|
||||
@echo Running golangci-lint
|
||||
$(GOBIN)/golangci-lint run ./...
|
||||
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
||||
ifneq ($(MM_NO_ENTERPRISE_LINT),true)
|
||||
$(GOBIN)/golangci-lint run ../enterprise/...
|
||||
endif
|
||||
endif
|
||||
ifeq ($(BUILD_BOARDS),true)
|
||||
ifneq ($(MM_NO_BOARDS_LINT),true)
|
||||
cd $(BUILD_BOARDS_DIR); make server-lint
|
||||
endif
|
||||
endif
|
||||
|
||||
app-layers: ## Extract interface from App struct
|
||||
$(GO) install github.com/reflog/struct2interface@v0.6.1
|
||||
$(GOBIN)/struct2interface -f "app" -o "app/app_iface.go" -p "app" -s "App" -i "AppIface" -t ./app/layer_generators/app_iface.go.tmpl
|
||||
$(GO) run ./app/layer_generators -in ./app/app_iface.go -out ./app/opentracing/opentracing_layer.go -template ./app/layer_generators/opentracing_layer.go.tmpl
|
||||
|
||||
i18n-extract: ## Extract strings for translation from the source code
|
||||
$(GO) install github.com/mattermost/mattermost-utilities/mmgotool@fdf2cd651b261bcd511a32da33dd76febedd44a8
|
||||
$(GOBIN)/mmgotool i18n extract --portal-dir=""
|
||||
|
||||
i18n-check: ## Exit on empty translation strings and translation source strings
|
||||
$(GO) install github.com/mattermost/mattermost-utilities/mmgotool@fdf2cd651b261bcd511a32da33dd76febedd44a8
|
||||
$(GOBIN)/mmgotool i18n clean-empty --portal-dir="" --check
|
||||
$(GOBIN)/mmgotool i18n check-empty-src --portal-dir=""
|
||||
|
||||
store-mocks: ## Creates mock files.
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.10.4
|
||||
$(GOBIN)/mockery --dir store --name ".*Store" --output store/storetest/mocks --note 'Regenerate this file using `make store-mocks`.'
|
||||
|
||||
telemetry-mocks: ## Creates mock files.
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.10.4
|
||||
$(GOBIN)/mockery --dir services/telemetry --all --output services/telemetry/mocks --note 'Regenerate this file using `make telemetry-mocks`.'
|
||||
|
||||
store-layers: ## Generate layers for the store
|
||||
$(GO) generate $(GOFLAGS) ./store
|
||||
|
||||
generate-worktemplates: ## Generate work templates
|
||||
$(GO) generate $(GOFLAGS) ./app/worktemplates
|
||||
|
||||
new-migration: ## Creates a new migration. Run with make new-migration name=<>
|
||||
$(GO) install github.com/mattermost/morph/cmd/morph@master
|
||||
@echo "Generating new migration for mysql"
|
||||
$(GOBIN)/morph generate $(name) --driver mysql --dir db/migrations --sequence
|
||||
|
||||
@echo "Generating new migration for postgres"
|
||||
$(GOBIN)/morph generate $(name) --driver postgres --dir db/migrations --sequence
|
||||
|
||||
filestore-mocks: ## Creates mock files.
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.10.4
|
||||
$(GOBIN)/mockery --dir shared/filestore --all --output shared/filestore/mocks --note 'Regenerate this file using `make filestore-mocks`.'
|
||||
|
||||
ldap-mocks: ## Creates mock files for ldap.
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.10.4
|
||||
$(GOBIN)/mockery --dir enterprise/ldap --all --output enterprise/ldap/mocks --note 'Regenerate this file using `make ldap-mocks`.'
|
||||
|
||||
plugin-mocks: ## Creates mock files for plugins.
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.10.4
|
||||
$(GOBIN)/mockery --dir plugin --name API --output plugin/plugintest --outpkg plugintest --case underscore --note 'Regenerate this file using `make plugin-mocks`.'
|
||||
$(GOBIN)/mockery --dir plugin --name Hooks --output plugin/plugintest --outpkg plugintest --case underscore --note 'Regenerate this file using `make plugin-mocks`.'
|
||||
$(GOBIN)/mockery --dir plugin --name Driver --output plugin/plugintest --outpkg plugintest --case underscore --note 'Regenerate this file using `make plugin-mocks`.'
|
||||
|
||||
einterfaces-mocks: ## Creates mock files for einterfaces.
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.10.4
|
||||
$(GOBIN)/mockery --dir einterfaces --all --output einterfaces/mocks --note 'Regenerate this file using `make einterfaces-mocks`.'
|
||||
|
||||
searchengine-mocks: ## Creates mock files for searchengines.
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.10.4
|
||||
$(GOBIN)/mockery --dir services/searchengine --all --output services/searchengine/mocks --note 'Regenerate this file using `make searchengine-mocks`.'
|
||||
|
||||
sharedchannel-mocks: ## Creates mock files for shared channels.
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.10.4
|
||||
$(GOBIN)/mockery --dir=./services/sharedchannel --name=ServerIface --output=./services/sharedchannel --inpackage --outpkg=sharedchannel --testonly --note 'Regenerate this file using `make sharedchannel-mocks`.'
|
||||
$(GOBIN)/mockery --dir=./services/sharedchannel --name=AppIface --output=./services/sharedchannel --inpackage --outpkg=sharedchannel --testonly --note 'Regenerate this file using `make sharedchannel-mocks`.'
|
||||
|
||||
misc-mocks: ## Creates mocks for misc interfaces.
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.10.4
|
||||
$(GOBIN)/mockery --dir utils --name LicenseValidatorIface --output utils/mocks --note 'Regenerate this file using `make misc-mocks`.'
|
||||
$(GOBIN)/mockery --dir app --name WorkTemplateExecutor --output app/mocks --note 'Regenerate this file using `make misc-mocks`.'
|
||||
|
||||
email-mocks: ## Creates mocks for misc interfaces.
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.10.4
|
||||
$(GOBIN)/mockery --dir app/email --name ServiceInterface --output app/email/mocks --note 'Regenerate this file using `make email-mocks`.'
|
||||
|
||||
platform-mocks: ## Creates mocks for platform interfaces.
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.14.0
|
||||
$(GOBIN)/mockery --dir app/platform --name SuiteIFace --output app/platform/mocks --note 'Regenerate this file using `make platform-mocks`.'
|
||||
|
||||
pluginapi: ## Generates api and hooks glue code for plugins
|
||||
$(GO) generate $(GOFLAGS) ./plugin
|
||||
|
||||
mocks: store-mocks telemetry-mocks filestore-mocks ldap-mocks plugin-mocks einterfaces-mocks searchengine-mocks sharedchannel-mocks misc-mocks email-mocks platform-mocks
|
||||
|
||||
layers: app-layers store-layers pluginapi
|
||||
|
||||
generated: mocks layers
|
||||
|
||||
check-prereqs: ## Checks prerequisite software status.
|
||||
./scripts/prereq-check.sh
|
||||
|
||||
check-prereqs-enterprise: setup-go-work ## Checks prerequisite software status for enterprise.
|
||||
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
||||
./scripts/prereq-check-enterprise.sh
|
||||
endif
|
||||
|
||||
setup-go-work: export BUILD_ENTERPRISE_READY := $(BUILD_ENTERPRISE_READY)
|
||||
setup-go-work: export BUILD_BOARDS := $(BUILD_BOARDS)
|
||||
setup-go-work: export BUILD_PLAYBOOKS := $(BUILD_PLAYBOOKS)
|
||||
setup-go-work: ## Sets up your go.work file
|
||||
./scripts/setup_go_work.sh $(IGNORE_GO_WORK_IF_EXISTS)
|
||||
|
||||
check-style: golangci-lint plugin-checker vet ## Runs style/lint checks
|
||||
|
||||
|
||||
do-cover-file: ## Creates the test coverage report file.
|
||||
@echo "mode: count" > cover.out
|
||||
|
||||
go-junit-report:
|
||||
$(GO) install github.com/jstemmer/go-junit-report@v1.0.0
|
||||
|
||||
test-compile: ## Compile tests.
|
||||
@echo COMPILE TESTS
|
||||
|
||||
for package in $(TE_PACKAGES) $(EE_PACKAGES); do \
|
||||
$(GO) test $(GOFLAGS) -c $$package; \
|
||||
done
|
||||
|
||||
gomodtidy:
|
||||
@cp go.mod go.mod.orig
|
||||
@cp go.sum go.sum.orig
|
||||
@if [ -f "imports/imports.go" ]; then \
|
||||
mv imports/imports.go imports/imports.go.orig; \
|
||||
fi;
|
||||
@if [ -f "imports/boards_imports.go" ]; then \
|
||||
mv imports/boards_imports.go imports/boards_imports.go.orig; \
|
||||
fi;
|
||||
$(GO) mod tidy
|
||||
@if [ "$$(diff go.mod go.mod.orig)" != "" -o "$$(diff go.sum go.sum.orig)" != "" ]; then \
|
||||
echo "go.mod/go.sum was modified. \ndiff- $$(diff go.mod go.mod.orig) \n$$(diff go.sum go.sum.orig) \nRun \"go mod tidy\"."; \
|
||||
rm go.*.orig; \
|
||||
exit 1; \
|
||||
fi;
|
||||
@if [ -f "imports/imports.go.orig" ]; then \
|
||||
mv imports/imports.go.orig imports/imports.go; \
|
||||
fi;
|
||||
@if [ -f "imports/boards_imports.go.orig" ]; then \
|
||||
mv imports/boards_imports.go.orig imports/boards_imports.go; \
|
||||
fi;
|
||||
@rm go.*.orig;
|
||||
|
||||
modules-tidy:
|
||||
@if [ -f "imports/imports.go" ]; then \
|
||||
mv imports/imports.go imports/imports.go.orig; \
|
||||
fi;
|
||||
@if [ -f "imports/boards_imports.go" ]; then \
|
||||
mv imports/boards_imports.go imports/boards_imports.go.orig; \
|
||||
fi;
|
||||
-$(GO) mod tidy
|
||||
@if [ -f "imports/imports.go.orig" ]; then \
|
||||
mv imports/imports.go.orig imports/imports.go; \
|
||||
fi;
|
||||
@if [ -f "imports/boards_imports.go.orig" ]; then \
|
||||
mv imports/boards_imports.go.orig imports/boards_imports.go; \
|
||||
fi;
|
||||
|
||||
test-server-pre: check-prereqs-enterprise start-docker go-junit-report do-cover-file ## Runs tests.
|
||||
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
||||
@echo Running all tests
|
||||
else
|
||||
@echo Running only TE tests
|
||||
endif
|
||||
|
||||
test-server-race: test-server-pre
|
||||
./scripts/test.sh "$(GO)" "-race $(GOFLAGS)" "$(ALL_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "90m" "atomic"
|
||||
ifneq ($(IS_CI),true)
|
||||
ifneq ($(MM_NO_DOCKER),true)
|
||||
ifneq ($(TEMP_DOCKER_SERVICES),)
|
||||
@echo Stopping temporary docker services
|
||||
docker-compose stop $(TEMP_DOCKER_SERVICES)
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
test-server: test-server-pre
|
||||
./scripts/test.sh "$(GO)" "$(GOFLAGS)" "$(ALL_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "45m" "count"
|
||||
ifneq ($(IS_CI),true)
|
||||
ifneq ($(MM_NO_DOCKER),true)
|
||||
ifneq ($(TEMP_DOCKER_SERVICES),)
|
||||
@echo Stopping temporary docker services
|
||||
docker-compose stop $(TEMP_DOCKER_SERVICES)
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
test-server-ee: check-prereqs-enterprise start-docker go-junit-report do-cover-file ## Runs EE tests.
|
||||
@echo Running only EE tests
|
||||
./scripts/test.sh "$(GO)" "$(GOFLAGS)" "$(EE_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "20m" "count"
|
||||
|
||||
test-server-quick: check-prereqs-enterprise ## Runs only quick tests.
|
||||
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
||||
@echo Running all tests
|
||||
$(GO) test $(GOFLAGS) -short $(ALL_PACKAGES)
|
||||
else
|
||||
@echo Running only TE tests
|
||||
$(GO) test $(GOFLAGS) -short $(TE_PACKAGES)
|
||||
endif
|
||||
|
||||
internal-test-web-client: ## Runs web client tests.
|
||||
$(GO) run $(GOFLAGS) $(PLATFORM_FILES) test web_client_tests
|
||||
|
||||
run-server-for-web-client-tests: ## Tests the server for web client.
|
||||
$(GO) run $(GOFLAGS) $(PLATFORM_FILES) test web_client_tests_server
|
||||
|
||||
test-client: ## Test client app.
|
||||
@echo Running client tests
|
||||
|
||||
cd $(BUILD_WEBAPP_DIR) && $(MAKE) test
|
||||
|
||||
test: test-server test-client ## Runs all checks and tests below (except race detection and postgres).
|
||||
|
||||
cover: ## Runs the golang coverage tool. You must run the unit tests first.
|
||||
@echo Opening coverage info in browser. If this failed run make test first
|
||||
|
||||
$(GO) tool cover -html=cover.out
|
||||
$(GO) tool cover -html=ecover.out
|
||||
|
||||
test-data: run-server inject-test-data ## start a local instance and add test data to it.
|
||||
|
||||
inject-test-data: # add test data to the local instance.
|
||||
@if ! ./scripts/wait-for-system-start.sh; then \
|
||||
make stop; \
|
||||
fi
|
||||
|
||||
@echo ServiceSettings.EnableLocalMode must be set to true.
|
||||
|
||||
bin/mmctl config set TeamSettings.MaxUsersPerTeam 100 --local
|
||||
bin/mmctl sampledata -u 60 --local
|
||||
|
||||
@echo You may need to restart the Mattermost server before using the following
|
||||
@echo ========================================================================
|
||||
@echo Login with a system admin account username=sysadmin password=Sys@dmin-sample1
|
||||
@echo Login with a regular account username=user-1 password=SampleUs@r-1
|
||||
@echo ========================================================================
|
||||
|
||||
validate-go-version: ## Validates the installed version of go against Mattermost's minimum requirement.
|
||||
@if [ $(GO_MAJOR_VERSION) -gt $(MINIMUM_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
||||
exit 0 ;\
|
||||
elif [ $(GO_MAJOR_VERSION) -lt $(MINIMUM_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
||||
exit 1; \
|
||||
elif [ $(GO_MINOR_VERSION) -lt $(MINIMUM_SUPPORTED_GO_MINOR_VERSION) ] ; then \
|
||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
build-templates: ## Compile all mjml email templates
|
||||
cd $(TEMPLATES_DIR) && $(MAKE) build
|
||||
|
||||
run-server: setup-go-work prepackaged-binaries validate-go-version start-docker ## Starts the server.
|
||||
@echo Running mattermost for development
|
||||
|
||||
mkdir -p $(BUILD_WEBAPP_DIR)/dist/files
|
||||
$(GO) run $(GOFLAGS) -tags $(GOTAGS) -ldflags '$(LDFLAGS)' $(PLATFORM_FILES) $(RUN_IN_BACKGROUND)
|
||||
|
||||
debug-server: start-docker ## Compile and start server using delve.
|
||||
mkdir -p $(BUILD_WEBAPP_DIR)/dist/files
|
||||
$(DELVE) debug $(PLATFORM_FILES) --build-flags="-ldflags '\
|
||||
-X github.com/mattermost/mattermost-server/v6/model.BuildNumber=$(BUILD_NUMBER)\
|
||||
-X \"github.com/mattermost/mattermost-server/v6/model.BuildDate=$(BUILD_DATE)\"\
|
||||
-X github.com/mattermost/mattermost-server/v6/model.BuildHash=$(BUILD_HASH)\
|
||||
-X github.com/mattermost/mattermost-server/v6/model.BuildHashEnterprise=$(BUILD_HASH_ENTERPRISE)\
|
||||
-X github.com/mattermost/mattermost-server/v6/model.BuildEnterpriseReady=$(BUILD_ENTERPRISE_READY)'"
|
||||
|
||||
debug-server-headless: start-docker ## Debug server from within an IDE like VSCode or IntelliJ.
|
||||
mkdir -p $(BUILD_WEBAPP_DIR)/dist/files
|
||||
$(DELVE) debug --headless --listen=:2345 --api-version=2 --accept-multiclient $(PLATFORM_FILES) --build-flags="-ldflags '\
|
||||
-X github.com/mattermost/mattermost-server/v6/model.BuildNumber=$(BUILD_NUMBER)\
|
||||
-X \"github.com/mattermost/mattermost-server/v6/model.BuildDate=$(BUILD_DATE)\"\
|
||||
-X github.com/mattermost/mattermost-server/v6/model.BuildHash=$(BUILD_HASH)\
|
||||
-X github.com/mattermost/mattermost-server/v6/model.BuildHashEnterprise=$(BUILD_HASH_ENTERPRISE)\
|
||||
-X github.com/mattermost/mattermost-server/v6/model.BuildEnterpriseReady=$(BUILD_ENTERPRISE_READY)'"
|
||||
|
||||
run-cli: start-docker ## Runs CLI.
|
||||
@echo Running mattermost for development
|
||||
@echo Example should be like 'make ARGS="-version" run-cli'
|
||||
|
||||
$(GO) run $(GOFLAGS) -tags $(GOTAGS) -ldflags '$(LDFLAGS)' $(PLATFORM_FILES) ${ARGS}
|
||||
|
||||
run-client: ## Runs the webapp.
|
||||
@echo Running mattermost client for development
|
||||
|
||||
ln -nfs $(BUILD_WEBAPP_DIR)/dist client
|
||||
cd $(BUILD_WEBAPP_DIR) && $(MAKE) run
|
||||
|
||||
run-client-fullmap: ## Legacy alias to run-client
|
||||
@echo Running mattermost client for development
|
||||
|
||||
cd $(BUILD_WEBAPP_DIR) && $(MAKE) run
|
||||
|
||||
run: check-prereqs run-server run-client ## Runs the server and webapp.
|
||||
|
||||
run-fullmap: run-server run-client ## Legacy alias to run
|
||||
|
||||
stop-server: ## Stops the server.
|
||||
@echo Stopping mattermost
|
||||
|
||||
ifeq ($(BUILDER_GOOS_GOARCH),"windows_amd64")
|
||||
wmic process where "Caption='go.exe' and CommandLine like '%go.exe run%'" call terminate
|
||||
wmic process where "Caption='mattermost.exe' and CommandLine like '%go-build%'" call terminate
|
||||
else
|
||||
@for PID in $$(ps -ef | grep "[g]o run" | grep "mattermost" | awk '{ print $$2 }'); do \
|
||||
echo stopping go $$PID; \
|
||||
kill $$PID; \
|
||||
done
|
||||
@for PID in $$(ps -ef | grep "[g]o-build" | grep "mattermost" | awk '{ print $$2 }'); do \
|
||||
echo stopping mattermost $$PID; \
|
||||
kill $$PID; \
|
||||
done
|
||||
endif
|
||||
|
||||
stop-client: ## Stops the webapp.
|
||||
@echo Stopping mattermost client
|
||||
|
||||
cd $(BUILD_WEBAPP_DIR) && $(MAKE) stop
|
||||
|
||||
stop: stop-server stop-client stop-docker ## Stops server, client and the docker compose.
|
||||
|
||||
restart: restart-server restart-client ## Restarts the server and webapp.
|
||||
|
||||
restart-server: | stop-server run-server ## Restarts the mattermost server to pick up development change.
|
||||
|
||||
restart-haserver:
|
||||
@echo Restarting mattermost in an HA topology
|
||||
|
||||
docker-compose restart follower2
|
||||
docker-compose restart follower
|
||||
docker-compose restart leader
|
||||
docker-compose restart haproxy
|
||||
|
||||
restart-client: | stop-client run-client ## Restarts the webapp.
|
||||
|
||||
run-job-server: ## Runs the background job server.
|
||||
@echo Running job server for development
|
||||
$(GO) run $(GOFLAGS) -tags $(GOTAGS) -ldflags '$(LDFLAGS)' $(PLATFORM_FILES) jobserver &
|
||||
|
||||
config-ldap: ## Configures LDAP.
|
||||
@echo Setting up configuration for local LDAP
|
||||
|
||||
@sed -i'' -e 's|"LdapServer": ".*"|"LdapServer": "localhost"|g' config/config.json
|
||||
@sed -i'' -e 's|"BaseDN": ".*"|"BaseDN": "dc=mm,dc=test,dc=com"|g' config/config.json
|
||||
@sed -i'' -e 's|"BindUsername": ".*"|"BindUsername": "cn=admin,dc=mm,dc=test,dc=com"|g' config/config.json
|
||||
@sed -i'' -e 's|"BindPassword": ".*"|"BindPassword": "mostest"|g' config/config.json
|
||||
@sed -i'' -e 's|"FirstNameAttribute": ".*"|"FirstNameAttribute": "cn"|g' config/config.json
|
||||
@sed -i'' -e 's|"LastNameAttribute": ".*"|"LastNameAttribute": "sn"|g' config/config.json
|
||||
@sed -i'' -e 's|"NicknameAttribute": ".*"|"NicknameAttribute": "cn"|g' config/config.json
|
||||
@sed -i'' -e 's|"EmailAttribute": ".*"|"EmailAttribute": "mail"|g' config/config.json
|
||||
@sed -i'' -e 's|"UsernameAttribute": ".*"|"UsernameAttribute": "uid"|g' config/config.json
|
||||
@sed -i'' -e 's|"IdAttribute": ".*"|"IdAttribute": "uid"|g' config/config.json
|
||||
@sed -i'' -e 's|"LoginIdAttribute": ".*"|"LoginIdAttribute": "uid"|g' config/config.json
|
||||
@sed -i'' -e 's|"GroupDisplayNameAttribute": ".*"|"GroupDisplayNameAttribute": "cn"|g' config/config.json
|
||||
@sed -i'' -e 's|"GroupIdAttribute": ".*"|"GroupIdAttribute": "entryUUID"|g' config/config.json
|
||||
|
||||
config-reset: ## Resets the config/config.json file to the default.
|
||||
@echo Resetting configuration to default
|
||||
rm -f config/config.json
|
||||
OUTPUT_CONFIG=$(PWD)/config/config.json $(GO) $(GOFLAGS) run ./scripts/config_generator
|
||||
|
||||
diff-config: ## Compares default configuration between two mattermost versions
|
||||
@./scripts/diff-config.sh
|
||||
|
||||
clean: stop-docker ## Clean up everything except persistent server data.
|
||||
@echo Cleaning
|
||||
|
||||
rm -Rf $(DIST_ROOT)
|
||||
$(GO) clean $(GOFLAGS) -i ./...
|
||||
|
||||
cd $(BUILD_WEBAPP_DIR) && $(MAKE) clean
|
||||
|
||||
find . -type d -name data | xargs rm -rf
|
||||
rm -rf logs
|
||||
|
||||
rm -f mattermost.log
|
||||
rm -f mattermost.log.jsonl
|
||||
rm -f npm-debug.log
|
||||
rm -f .prepare-go
|
||||
rm -f enterprise
|
||||
rm -f cover.out
|
||||
rm -f ecover.out
|
||||
rm -f *.out
|
||||
rm -f *.test
|
||||
rm -f imports/imports.go
|
||||
rm -f cmd/mattermost/cprofile*.out
|
||||
|
||||
nuke: clean clean-docker ## Clean plus removes persistent server data.
|
||||
@echo BOOM
|
||||
|
||||
rm -rf data
|
||||
rm -f go.work go.work.sum
|
||||
|
||||
setup-mac: ## Adds macOS hosts entries for Docker.
|
||||
echo $$(boot2docker ip 2> /dev/null) dockerhost | sudo tee -a /etc/hosts
|
||||
|
||||
update-dependencies: ## Uses go get -u to update all the dependencies while holding back any that require it.
|
||||
@echo Updating Dependencies
|
||||
|
||||
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
||||
@echo Enterprise repository detected, temporarily removing imports.go
|
||||
rm -f imports/imports.go
|
||||
endif
|
||||
|
||||
ifeq ($(BUILD_BOARDS),true)
|
||||
@echo Boards repository detected, temporarily removing boards_imports.go
|
||||
rm -f imports/boards_imports.go
|
||||
endif
|
||||
|
||||
# Update all dependencies (does not update across major versions)
|
||||
$(GO) get -u ./...
|
||||
|
||||
# Tidy up
|
||||
$(GO) mod tidy
|
||||
|
||||
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
||||
cp $(BUILD_ENTERPRISE_DIR)/imports/imports.go imports/
|
||||
endif
|
||||
|
||||
ifeq ($(BUILD_BOARDS),true)
|
||||
cp $(BUILD_BOARDS_DIR)/mattermost-plugin/product/imports/boards_imports.go imports/
|
||||
endif
|
||||
|
||||
vet: ## Run mattermost go vet specific checks
|
||||
$(GO) install github.com/mattermost/mattermost-govet/v2@new
|
||||
@VET_CMD="-license -structuredLogging -inconsistentReceiverName -inconsistentReceiverName.ignore=session_serial_gen.go,team_member_serial_gen.go,user_serial_gen.go -emptyStrCmp -tFatal -configtelemetry -errorAssertions"; \
|
||||
if ! [ -z "${MM_VET_OPENSPEC_PATH}" ] && [ -f "${MM_VET_OPENSPEC_PATH}" ]; then \
|
||||
VET_CMD="$$VET_CMD -openApiSync -openApiSync.spec=$$MM_VET_OPENSPEC_PATH"; \
|
||||
else \
|
||||
echo "MM_VET_OPENSPEC_PATH not set or spec yaml path in it is incorrect. Skipping API check"; \
|
||||
fi; \
|
||||
$(GO) vet -vettool=$(GOBIN)/mattermost-govet $$VET_CMD ./...
|
||||
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
||||
ifneq ($(MM_NO_ENTERPRISE_LINT),true)
|
||||
$(GO) vet -vettool=$(GOBIN)/mattermost-govet -enterpriseLicense -structuredLogging -tFatal ../enterprise/...
|
||||
endif
|
||||
endif
|
||||
|
||||
gen-serialized: export LICENSE_HEADER:=$(LICENSE_HEADER)
|
||||
gen-serialized: ## Generates serialization methods for hot structs
|
||||
# This tool only works at a file level, not at a package level.
|
||||
# There will be some warnings about "unresolved identifiers",
|
||||
# but that is because of the above problem. Since we are generating
|
||||
# methods for all the relevant files at a package level, all
|
||||
# identifiers will be resolved. An alternative to remove the warnings
|
||||
# would be to temporarily move all the structs to the same file,
|
||||
# but that involves a lot of manual work.
|
||||
$(GO) install github.com/tinylib/msgp@v1.1.6
|
||||
$(GOBIN)/msgp -file=./model/session.go -tests=false -o=./model/session_serial_gen.go
|
||||
@echo "$$LICENSE_HEADER" > tmp.go
|
||||
@cat ./model/session_serial_gen.go >> tmp.go
|
||||
@mv tmp.go ./model/session_serial_gen.go
|
||||
$(GOBIN)/msgp -file=./model/user.go -tests=false -o=./model/user_serial_gen.go
|
||||
@echo "$$LICENSE_HEADER" > tmp.go
|
||||
@cat ./model/user_serial_gen.go >> tmp.go
|
||||
@mv tmp.go ./model/user_serial_gen.go
|
||||
$(GOBIN)/msgp -file=./model/team_member.go -tests=false -o=./model/team_member_serial_gen.go
|
||||
@echo "$$LICENSE_HEADER" > tmp.go
|
||||
@cat ./model/team_member_serial_gen.go >> tmp.go
|
||||
@mv tmp.go ./model/team_member_serial_gen.go
|
||||
|
||||
todo: ## Display TODO and FIXME items in the source code.
|
||||
@! ag --ignore Makefile --ignore-dir runtime '(TODO|XXX|FIXME|"FIX ME")[: ]+'
|
||||
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
||||
@! ag --ignore Makefile --ignore-dir runtime '(TODO|XXX|FIXME|"FIX ME")[: ]+' enterprise/
|
||||
endif
|
||||
|
||||
## Help documentation à la https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
|
||||
help:
|
||||
@grep -E '^[0-9a-zA-Z_-]+:.*?## .*$$' ./Makefile | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
@echo
|
||||
@echo You can modify the default settings for this Makefile creating a file config.mk based on the default-config.mk
|
||||
@echo
|
||||
|
||||
migrations-extract:
|
||||
@echo Listing migration files
|
||||
@echo "# Autogenerated file to synchronize migrations sequence in the PR workflow, please do not edit.\n#" > db/migrations/migrations.list
|
||||
find db/migrations -maxdepth 2 -mindepth 2 | sort >> db/migrations/migrations.list
|
||||
435
api4/api.go
435
api4/api.go
|
|
@ -1,435 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
graphql "github.com/graph-gophers/graphql-go"
|
||||
_ "github.com/mattermost/go-i18n/i18n"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/web"
|
||||
)
|
||||
|
||||
type Routes struct {
|
||||
Root *mux.Router // ''
|
||||
APIRoot *mux.Router // 'api/v4'
|
||||
APIRoot5 *mux.Router // 'api/v5'
|
||||
|
||||
Users *mux.Router // 'api/v4/users'
|
||||
User *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}'
|
||||
UserByUsername *mux.Router // 'api/v4/users/username/{username:[A-Za-z0-9\\_\\-\\.]+}'
|
||||
UserByEmail *mux.Router // 'api/v4/users/email/{email:.+}'
|
||||
|
||||
Bots *mux.Router // 'api/v4/bots'
|
||||
Bot *mux.Router // 'api/v4/bots/{bot_user_id:[A-Za-z0-9]+}'
|
||||
|
||||
Teams *mux.Router // 'api/v4/teams'
|
||||
TeamsForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams'
|
||||
Team *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}'
|
||||
TeamForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}'
|
||||
UserThreads *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/threads'
|
||||
UserThread *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/threads/{thread_id:[A-Za-z0-9]+}'
|
||||
TeamByName *mux.Router // 'api/v4/teams/name/{team_name:[A-Za-z0-9_-]+}'
|
||||
TeamMembers *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/members'
|
||||
TeamMember *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/members/{user_id:[A-Za-z0-9]+}'
|
||||
TeamMembersForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/members'
|
||||
|
||||
Channels *mux.Router // 'api/v4/channels'
|
||||
Channel *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}'
|
||||
ChannelForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/channels/{channel_id:[A-Za-z0-9]+}'
|
||||
ChannelByName *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/channels/name/{channel_name:[A-Za-z0-9_-]+}'
|
||||
ChannelByNameForTeamName *mux.Router // 'api/v4/teams/name/{team_name:[A-Za-z0-9_-]+}/channels/name/{channel_name:[A-Za-z0-9_-]+}'
|
||||
ChannelsForTeam *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/channels'
|
||||
ChannelMembers *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/members'
|
||||
ChannelMember *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/members/{user_id:[A-Za-z0-9]+}'
|
||||
ChannelMembersForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/channels/members'
|
||||
ChannelModerations *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/moderations'
|
||||
ChannelCategories *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/channels/categories'
|
||||
|
||||
Posts *mux.Router // 'api/v4/posts'
|
||||
Post *mux.Router // 'api/v4/posts/{post_id:[A-Za-z0-9]+}'
|
||||
PostsForChannel *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/posts'
|
||||
PostsForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/posts'
|
||||
PostForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/posts/{post_id:[A-Za-z0-9]+}'
|
||||
|
||||
Files *mux.Router // 'api/v4/files'
|
||||
File *mux.Router // 'api/v4/files/{file_id:[A-Za-z0-9]+}'
|
||||
|
||||
Uploads *mux.Router // 'api/v4/uploads'
|
||||
Upload *mux.Router // 'api/v4/uploads/{upload_id:[A-Za-z0-9]+}'
|
||||
|
||||
Plugins *mux.Router // 'api/v4/plugins'
|
||||
Plugin *mux.Router // 'api/v4/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}'
|
||||
|
||||
PublicFile *mux.Router // '/files/{file_id:[A-Za-z0-9]+}/public'
|
||||
|
||||
Commands *mux.Router // 'api/v4/commands'
|
||||
Command *mux.Router // 'api/v4/commands/{command_id:[A-Za-z0-9]+}'
|
||||
|
||||
Hooks *mux.Router // 'api/v4/hooks'
|
||||
IncomingHooks *mux.Router // 'api/v4/hooks/incoming'
|
||||
IncomingHook *mux.Router // 'api/v4/hooks/incoming/{hook_id:[A-Za-z0-9]+}'
|
||||
OutgoingHooks *mux.Router // 'api/v4/hooks/outgoing'
|
||||
OutgoingHook *mux.Router // 'api/v4/hooks/outgoing/{hook_id:[A-Za-z0-9]+}'
|
||||
|
||||
OAuth *mux.Router // 'api/v4/oauth'
|
||||
OAuthApps *mux.Router // 'api/v4/oauth/apps'
|
||||
OAuthApp *mux.Router // 'api/v4/oauth/apps/{app_id:[A-Za-z0-9]+}'
|
||||
|
||||
OpenGraph *mux.Router // 'api/v4/opengraph'
|
||||
|
||||
SAML *mux.Router // 'api/v4/saml'
|
||||
Compliance *mux.Router // 'api/v4/compliance'
|
||||
Cluster *mux.Router // 'api/v4/cluster'
|
||||
|
||||
Image *mux.Router // 'api/v4/image'
|
||||
|
||||
LDAP *mux.Router // 'api/v4/ldap'
|
||||
|
||||
Elasticsearch *mux.Router // 'api/v4/elasticsearch'
|
||||
|
||||
Bleve *mux.Router // 'api/v4/bleve'
|
||||
|
||||
DataRetention *mux.Router // 'api/v4/data_retention'
|
||||
|
||||
Brand *mux.Router // 'api/v4/brand'
|
||||
|
||||
System *mux.Router // 'api/v4/system'
|
||||
|
||||
Jobs *mux.Router // 'api/v4/jobs'
|
||||
|
||||
Preferences *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/preferences'
|
||||
|
||||
License *mux.Router // 'api/v4/license'
|
||||
|
||||
Public *mux.Router // 'api/v4/public'
|
||||
|
||||
Reactions *mux.Router // 'api/v4/reactions'
|
||||
|
||||
Roles *mux.Router // 'api/v4/roles'
|
||||
Schemes *mux.Router // 'api/v4/schemes'
|
||||
|
||||
Emojis *mux.Router // 'api/v4/emoji'
|
||||
Emoji *mux.Router // 'api/v4/emoji/{emoji_id:[A-Za-z0-9]+}'
|
||||
EmojiByName *mux.Router // 'api/v4/emoji/name/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}'
|
||||
|
||||
ReactionByNameForPostForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/posts/{post_id:[A-Za-z0-9]+}/reactions/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}'
|
||||
|
||||
TermsOfService *mux.Router // 'api/v4/terms_of_service'
|
||||
Groups *mux.Router // 'api/v4/groups'
|
||||
|
||||
Cloud *mux.Router // 'api/v4/cloud'
|
||||
|
||||
Imports *mux.Router // 'api/v4/imports'
|
||||
|
||||
Exports *mux.Router // 'api/v4/exports'
|
||||
Export *mux.Router // 'api/v4/exports/{export_name:.+\\.zip}'
|
||||
|
||||
RemoteCluster *mux.Router // 'api/v4/remotecluster'
|
||||
SharedChannels *mux.Router // 'api/v4/sharedchannels'
|
||||
|
||||
Permissions *mux.Router // 'api/v4/permissions'
|
||||
|
||||
InsightsForTeam *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/top'
|
||||
InsightsForUser *mux.Router // 'api/v4/users/me/top'
|
||||
|
||||
Usage *mux.Router // 'api/v4/usage'
|
||||
|
||||
WorkTemplates *mux.Router // 'api/v4/worktemplates'
|
||||
|
||||
HostedCustomer *mux.Router // 'api/v4/hosted_customer'
|
||||
|
||||
Drafts *mux.Router // 'api/v4/drafts'
|
||||
}
|
||||
|
||||
type API struct {
|
||||
srv *app.Server
|
||||
schema *graphql.Schema
|
||||
BaseRoutes *Routes
|
||||
}
|
||||
|
||||
func Init(srv *app.Server) (*API, error) {
|
||||
api := &API{
|
||||
srv: srv,
|
||||
BaseRoutes: &Routes{},
|
||||
}
|
||||
|
||||
api.BaseRoutes.Root = srv.Router
|
||||
api.BaseRoutes.APIRoot = srv.Router.PathPrefix(model.APIURLSuffix).Subrouter()
|
||||
api.BaseRoutes.APIRoot5 = srv.Router.PathPrefix(model.APIURLSuffixV5).Subrouter()
|
||||
|
||||
api.BaseRoutes.Users = api.BaseRoutes.APIRoot.PathPrefix("/users").Subrouter()
|
||||
api.BaseRoutes.User = api.BaseRoutes.APIRoot.PathPrefix("/users/{user_id:[A-Za-z0-9]+}").Subrouter()
|
||||
api.BaseRoutes.UserByUsername = api.BaseRoutes.Users.PathPrefix("/username/{username:[A-Za-z0-9\\_\\-\\.]+}").Subrouter()
|
||||
api.BaseRoutes.UserByEmail = api.BaseRoutes.Users.PathPrefix("/email/{email:.+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.Bots = api.BaseRoutes.APIRoot.PathPrefix("/bots").Subrouter()
|
||||
api.BaseRoutes.Bot = api.BaseRoutes.APIRoot.PathPrefix("/bots/{bot_user_id:[A-Za-z0-9]+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.Teams = api.BaseRoutes.APIRoot.PathPrefix("/teams").Subrouter()
|
||||
api.BaseRoutes.TeamsForUser = api.BaseRoutes.User.PathPrefix("/teams").Subrouter()
|
||||
api.BaseRoutes.Team = api.BaseRoutes.Teams.PathPrefix("/{team_id:[A-Za-z0-9]+}").Subrouter()
|
||||
api.BaseRoutes.TeamForUser = api.BaseRoutes.TeamsForUser.PathPrefix("/{team_id:[A-Za-z0-9]+}").Subrouter()
|
||||
api.BaseRoutes.UserThreads = api.BaseRoutes.TeamForUser.PathPrefix("/threads").Subrouter()
|
||||
api.BaseRoutes.UserThread = api.BaseRoutes.TeamForUser.PathPrefix("/threads/{thread_id:[A-Za-z0-9]+}").Subrouter()
|
||||
api.BaseRoutes.TeamByName = api.BaseRoutes.Teams.PathPrefix("/name/{team_name:[A-Za-z0-9_-]+}").Subrouter()
|
||||
api.BaseRoutes.TeamMembers = api.BaseRoutes.Team.PathPrefix("/members").Subrouter()
|
||||
api.BaseRoutes.TeamMember = api.BaseRoutes.TeamMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
|
||||
api.BaseRoutes.TeamMembersForUser = api.BaseRoutes.User.PathPrefix("/teams/members").Subrouter()
|
||||
|
||||
api.BaseRoutes.Channels = api.BaseRoutes.APIRoot.PathPrefix("/channels").Subrouter()
|
||||
api.BaseRoutes.Channel = api.BaseRoutes.Channels.PathPrefix("/{channel_id:[A-Za-z0-9]+}").Subrouter()
|
||||
api.BaseRoutes.ChannelForUser = api.BaseRoutes.User.PathPrefix("/channels/{channel_id:[A-Za-z0-9]+}").Subrouter()
|
||||
api.BaseRoutes.ChannelByName = api.BaseRoutes.Team.PathPrefix("/channels/name/{channel_name:[A-Za-z0-9_-]+}").Subrouter()
|
||||
api.BaseRoutes.ChannelByNameForTeamName = api.BaseRoutes.TeamByName.PathPrefix("/channels/name/{channel_name:[A-Za-z0-9_-]+}").Subrouter()
|
||||
api.BaseRoutes.ChannelsForTeam = api.BaseRoutes.Team.PathPrefix("/channels").Subrouter()
|
||||
api.BaseRoutes.ChannelMembers = api.BaseRoutes.Channel.PathPrefix("/members").Subrouter()
|
||||
api.BaseRoutes.ChannelMember = api.BaseRoutes.ChannelMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
|
||||
api.BaseRoutes.ChannelMembersForUser = api.BaseRoutes.User.PathPrefix("/teams/{team_id:[A-Za-z0-9]+}/channels/members").Subrouter()
|
||||
api.BaseRoutes.ChannelModerations = api.BaseRoutes.Channel.PathPrefix("/moderations").Subrouter()
|
||||
api.BaseRoutes.ChannelCategories = api.BaseRoutes.User.PathPrefix("/teams/{team_id:[A-Za-z0-9]+}/channels/categories").Subrouter()
|
||||
|
||||
api.BaseRoutes.Posts = api.BaseRoutes.APIRoot.PathPrefix("/posts").Subrouter()
|
||||
api.BaseRoutes.Post = api.BaseRoutes.Posts.PathPrefix("/{post_id:[A-Za-z0-9]+}").Subrouter()
|
||||
api.BaseRoutes.PostsForChannel = api.BaseRoutes.Channel.PathPrefix("/posts").Subrouter()
|
||||
api.BaseRoutes.PostsForUser = api.BaseRoutes.User.PathPrefix("/posts").Subrouter()
|
||||
api.BaseRoutes.PostForUser = api.BaseRoutes.PostsForUser.PathPrefix("/{post_id:[A-Za-z0-9]+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.Files = api.BaseRoutes.APIRoot.PathPrefix("/files").Subrouter()
|
||||
api.BaseRoutes.File = api.BaseRoutes.Files.PathPrefix("/{file_id:[A-Za-z0-9]+}").Subrouter()
|
||||
api.BaseRoutes.PublicFile = api.BaseRoutes.Root.PathPrefix("/files/{file_id:[A-Za-z0-9]+}/public").Subrouter()
|
||||
|
||||
api.BaseRoutes.Uploads = api.BaseRoutes.APIRoot.PathPrefix("/uploads").Subrouter()
|
||||
api.BaseRoutes.Upload = api.BaseRoutes.Uploads.PathPrefix("/{upload_id:[A-Za-z0-9]+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.Plugins = api.BaseRoutes.APIRoot.PathPrefix("/plugins").Subrouter()
|
||||
api.BaseRoutes.Plugin = api.BaseRoutes.Plugins.PathPrefix("/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.Commands = api.BaseRoutes.APIRoot.PathPrefix("/commands").Subrouter()
|
||||
api.BaseRoutes.Command = api.BaseRoutes.Commands.PathPrefix("/{command_id:[A-Za-z0-9]+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.Hooks = api.BaseRoutes.APIRoot.PathPrefix("/hooks").Subrouter()
|
||||
api.BaseRoutes.IncomingHooks = api.BaseRoutes.Hooks.PathPrefix("/incoming").Subrouter()
|
||||
api.BaseRoutes.IncomingHook = api.BaseRoutes.IncomingHooks.PathPrefix("/{hook_id:[A-Za-z0-9]+}").Subrouter()
|
||||
api.BaseRoutes.OutgoingHooks = api.BaseRoutes.Hooks.PathPrefix("/outgoing").Subrouter()
|
||||
api.BaseRoutes.OutgoingHook = api.BaseRoutes.OutgoingHooks.PathPrefix("/{hook_id:[A-Za-z0-9]+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.SAML = api.BaseRoutes.APIRoot.PathPrefix("/saml").Subrouter()
|
||||
|
||||
api.BaseRoutes.OAuth = api.BaseRoutes.APIRoot.PathPrefix("/oauth").Subrouter()
|
||||
api.BaseRoutes.OAuthApps = api.BaseRoutes.OAuth.PathPrefix("/apps").Subrouter()
|
||||
api.BaseRoutes.OAuthApp = api.BaseRoutes.OAuthApps.PathPrefix("/{app_id:[A-Za-z0-9]+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.Compliance = api.BaseRoutes.APIRoot.PathPrefix("/compliance").Subrouter()
|
||||
api.BaseRoutes.Cluster = api.BaseRoutes.APIRoot.PathPrefix("/cluster").Subrouter()
|
||||
api.BaseRoutes.LDAP = api.BaseRoutes.APIRoot.PathPrefix("/ldap").Subrouter()
|
||||
api.BaseRoutes.Brand = api.BaseRoutes.APIRoot.PathPrefix("/brand").Subrouter()
|
||||
api.BaseRoutes.System = api.BaseRoutes.APIRoot.PathPrefix("/system").Subrouter()
|
||||
api.BaseRoutes.Preferences = api.BaseRoutes.User.PathPrefix("/preferences").Subrouter()
|
||||
api.BaseRoutes.License = api.BaseRoutes.APIRoot.PathPrefix("/license").Subrouter()
|
||||
api.BaseRoutes.Public = api.BaseRoutes.APIRoot.PathPrefix("/public").Subrouter()
|
||||
api.BaseRoutes.Reactions = api.BaseRoutes.APIRoot.PathPrefix("/reactions").Subrouter()
|
||||
api.BaseRoutes.Jobs = api.BaseRoutes.APIRoot.PathPrefix("/jobs").Subrouter()
|
||||
api.BaseRoutes.Elasticsearch = api.BaseRoutes.APIRoot.PathPrefix("/elasticsearch").Subrouter()
|
||||
api.BaseRoutes.Bleve = api.BaseRoutes.APIRoot.PathPrefix("/bleve").Subrouter()
|
||||
api.BaseRoutes.DataRetention = api.BaseRoutes.APIRoot.PathPrefix("/data_retention").Subrouter()
|
||||
|
||||
api.BaseRoutes.Emojis = api.BaseRoutes.APIRoot.PathPrefix("/emoji").Subrouter()
|
||||
api.BaseRoutes.Emoji = api.BaseRoutes.APIRoot.PathPrefix("/emoji/{emoji_id:[A-Za-z0-9]+}").Subrouter()
|
||||
api.BaseRoutes.EmojiByName = api.BaseRoutes.Emojis.PathPrefix("/name/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.ReactionByNameForPostForUser = api.BaseRoutes.PostForUser.PathPrefix("/reactions/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.OpenGraph = api.BaseRoutes.APIRoot.PathPrefix("/opengraph").Subrouter()
|
||||
|
||||
api.BaseRoutes.Roles = api.BaseRoutes.APIRoot.PathPrefix("/roles").Subrouter()
|
||||
api.BaseRoutes.Schemes = api.BaseRoutes.APIRoot.PathPrefix("/schemes").Subrouter()
|
||||
|
||||
api.BaseRoutes.Image = api.BaseRoutes.APIRoot.PathPrefix("/image").Subrouter()
|
||||
|
||||
api.BaseRoutes.TermsOfService = api.BaseRoutes.APIRoot.PathPrefix("/terms_of_service").Subrouter()
|
||||
api.BaseRoutes.Groups = api.BaseRoutes.APIRoot.PathPrefix("/groups").Subrouter()
|
||||
|
||||
api.BaseRoutes.Cloud = api.BaseRoutes.APIRoot.PathPrefix("/cloud").Subrouter()
|
||||
|
||||
api.BaseRoutes.Imports = api.BaseRoutes.APIRoot.PathPrefix("/imports").Subrouter()
|
||||
api.BaseRoutes.Exports = api.BaseRoutes.APIRoot.PathPrefix("/exports").Subrouter()
|
||||
api.BaseRoutes.Export = api.BaseRoutes.Exports.PathPrefix("/{export_name:.+\\.zip}").Subrouter()
|
||||
|
||||
api.BaseRoutes.RemoteCluster = api.BaseRoutes.APIRoot.PathPrefix("/remotecluster").Subrouter()
|
||||
api.BaseRoutes.SharedChannels = api.BaseRoutes.APIRoot.PathPrefix("/sharedchannels").Subrouter()
|
||||
|
||||
api.BaseRoutes.Permissions = api.BaseRoutes.APIRoot.PathPrefix("/permissions").Subrouter()
|
||||
|
||||
api.BaseRoutes.InsightsForTeam = api.BaseRoutes.Team.PathPrefix("/top").Subrouter()
|
||||
api.BaseRoutes.InsightsForUser = api.BaseRoutes.Users.PathPrefix("/me/top").Subrouter()
|
||||
|
||||
api.BaseRoutes.Usage = api.BaseRoutes.APIRoot.PathPrefix("/usage").Subrouter()
|
||||
|
||||
api.BaseRoutes.WorkTemplates = api.BaseRoutes.APIRoot.PathPrefix("/worktemplates").Subrouter()
|
||||
|
||||
api.BaseRoutes.HostedCustomer = api.BaseRoutes.APIRoot.PathPrefix("/hosted_customer").Subrouter()
|
||||
|
||||
api.BaseRoutes.Drafts = api.BaseRoutes.APIRoot.PathPrefix("/drafts").Subrouter()
|
||||
|
||||
api.InitUser()
|
||||
api.InitBot()
|
||||
api.InitTeam()
|
||||
api.InitChannel()
|
||||
api.InitPost()
|
||||
api.InitFile()
|
||||
api.InitUpload()
|
||||
api.InitSystem()
|
||||
api.InitLicense()
|
||||
api.InitConfig()
|
||||
api.InitWebhook()
|
||||
api.InitPreference()
|
||||
api.InitSaml()
|
||||
api.InitCompliance()
|
||||
api.InitCluster()
|
||||
api.InitLdap()
|
||||
api.InitElasticsearch()
|
||||
api.InitBleve()
|
||||
api.InitDataRetention()
|
||||
api.InitBrand()
|
||||
api.InitJob()
|
||||
api.InitCommand()
|
||||
api.InitStatus()
|
||||
api.InitWebSocket()
|
||||
api.InitEmoji()
|
||||
api.InitOAuth()
|
||||
api.InitReaction()
|
||||
api.InitOpenGraph()
|
||||
api.InitPlugin()
|
||||
api.InitRole()
|
||||
api.InitScheme()
|
||||
api.InitImage()
|
||||
api.InitTermsOfService()
|
||||
api.InitGroup()
|
||||
api.InitAction()
|
||||
api.InitCloud()
|
||||
api.InitImport()
|
||||
api.InitRemoteCluster()
|
||||
api.InitSharedChannels()
|
||||
api.InitPermissions()
|
||||
api.InitExport()
|
||||
api.InitInsights()
|
||||
api.InitUsage()
|
||||
api.InitWorkTemplate()
|
||||
api.InitHostedCustomer()
|
||||
api.InitDrafts()
|
||||
if err := api.InitGraphQL(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
srv.Router.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))
|
||||
|
||||
InitLocal(srv)
|
||||
|
||||
return api, nil
|
||||
}
|
||||
|
||||
func InitLocal(srv *app.Server) *API {
|
||||
api := &API{
|
||||
srv: srv,
|
||||
BaseRoutes: &Routes{},
|
||||
}
|
||||
|
||||
api.BaseRoutes.Root = srv.LocalRouter
|
||||
api.BaseRoutes.APIRoot = srv.LocalRouter.PathPrefix(model.APIURLSuffix).Subrouter()
|
||||
|
||||
api.BaseRoutes.Users = api.BaseRoutes.APIRoot.PathPrefix("/users").Subrouter()
|
||||
api.BaseRoutes.User = api.BaseRoutes.Users.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
|
||||
api.BaseRoutes.UserByUsername = api.BaseRoutes.Users.PathPrefix("/username/{username:[A-Za-z0-9\\_\\-\\.]+}").Subrouter()
|
||||
api.BaseRoutes.UserByEmail = api.BaseRoutes.Users.PathPrefix("/email/{email:.+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.Bots = api.BaseRoutes.APIRoot.PathPrefix("/bots").Subrouter()
|
||||
api.BaseRoutes.Bot = api.BaseRoutes.APIRoot.PathPrefix("/bots/{bot_user_id:[A-Za-z0-9]+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.Teams = api.BaseRoutes.APIRoot.PathPrefix("/teams").Subrouter()
|
||||
api.BaseRoutes.Team = api.BaseRoutes.Teams.PathPrefix("/{team_id:[A-Za-z0-9]+}").Subrouter()
|
||||
api.BaseRoutes.TeamByName = api.BaseRoutes.Teams.PathPrefix("/name/{team_name:[A-Za-z0-9_-]+}").Subrouter()
|
||||
api.BaseRoutes.TeamMembers = api.BaseRoutes.Team.PathPrefix("/members").Subrouter()
|
||||
api.BaseRoutes.TeamMember = api.BaseRoutes.TeamMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.Channels = api.BaseRoutes.APIRoot.PathPrefix("/channels").Subrouter()
|
||||
api.BaseRoutes.Channel = api.BaseRoutes.Channels.PathPrefix("/{channel_id:[A-Za-z0-9]+}").Subrouter()
|
||||
api.BaseRoutes.ChannelByName = api.BaseRoutes.Team.PathPrefix("/channels/name/{channel_name:[A-Za-z0-9_-]+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.ChannelByNameForTeamName = api.BaseRoutes.TeamByName.PathPrefix("/channels/name/{channel_name:[A-Za-z0-9_-]+}").Subrouter()
|
||||
api.BaseRoutes.ChannelsForTeam = api.BaseRoutes.Team.PathPrefix("/channels").Subrouter()
|
||||
api.BaseRoutes.ChannelMembers = api.BaseRoutes.Channel.PathPrefix("/members").Subrouter()
|
||||
api.BaseRoutes.ChannelMember = api.BaseRoutes.ChannelMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
|
||||
api.BaseRoutes.ChannelMembersForUser = api.BaseRoutes.User.PathPrefix("/teams/{team_id:[A-Za-z0-9]+}/channels/members").Subrouter()
|
||||
|
||||
api.BaseRoutes.Plugins = api.BaseRoutes.APIRoot.PathPrefix("/plugins").Subrouter()
|
||||
api.BaseRoutes.Plugin = api.BaseRoutes.Plugins.PathPrefix("/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.Commands = api.BaseRoutes.APIRoot.PathPrefix("/commands").Subrouter()
|
||||
api.BaseRoutes.Command = api.BaseRoutes.Commands.PathPrefix("/{command_id:[A-Za-z0-9]+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.Hooks = api.BaseRoutes.APIRoot.PathPrefix("/hooks").Subrouter()
|
||||
api.BaseRoutes.IncomingHooks = api.BaseRoutes.Hooks.PathPrefix("/incoming").Subrouter()
|
||||
api.BaseRoutes.IncomingHook = api.BaseRoutes.IncomingHooks.PathPrefix("/{hook_id:[A-Za-z0-9]+}").Subrouter()
|
||||
api.BaseRoutes.OutgoingHooks = api.BaseRoutes.Hooks.PathPrefix("/outgoing").Subrouter()
|
||||
api.BaseRoutes.OutgoingHook = api.BaseRoutes.OutgoingHooks.PathPrefix("/{hook_id:[A-Za-z0-9]+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.License = api.BaseRoutes.APIRoot.PathPrefix("/license").Subrouter()
|
||||
|
||||
api.BaseRoutes.Groups = api.BaseRoutes.APIRoot.PathPrefix("/groups").Subrouter()
|
||||
|
||||
api.BaseRoutes.LDAP = api.BaseRoutes.APIRoot.PathPrefix("/ldap").Subrouter()
|
||||
api.BaseRoutes.System = api.BaseRoutes.APIRoot.PathPrefix("/system").Subrouter()
|
||||
api.BaseRoutes.Posts = api.BaseRoutes.APIRoot.PathPrefix("/posts").Subrouter()
|
||||
api.BaseRoutes.Post = api.BaseRoutes.Posts.PathPrefix("/{post_id:[A-Za-z0-9]+}").Subrouter()
|
||||
api.BaseRoutes.PostsForChannel = api.BaseRoutes.Channel.PathPrefix("/posts").Subrouter()
|
||||
|
||||
api.BaseRoutes.Roles = api.BaseRoutes.APIRoot.PathPrefix("/roles").Subrouter()
|
||||
|
||||
api.BaseRoutes.Uploads = api.BaseRoutes.APIRoot.PathPrefix("/uploads").Subrouter()
|
||||
api.BaseRoutes.Upload = api.BaseRoutes.Uploads.PathPrefix("/{upload_id:[A-Za-z0-9]+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.Imports = api.BaseRoutes.APIRoot.PathPrefix("/imports").Subrouter()
|
||||
api.BaseRoutes.Exports = api.BaseRoutes.APIRoot.PathPrefix("/exports").Subrouter()
|
||||
api.BaseRoutes.Export = api.BaseRoutes.Exports.PathPrefix("/{export_name:.+\\.zip}").Subrouter()
|
||||
|
||||
api.BaseRoutes.Jobs = api.BaseRoutes.APIRoot.PathPrefix("/jobs").Subrouter()
|
||||
|
||||
api.BaseRoutes.SAML = api.BaseRoutes.APIRoot.PathPrefix("/saml").Subrouter()
|
||||
|
||||
api.InitUserLocal()
|
||||
api.InitTeamLocal()
|
||||
api.InitChannelLocal()
|
||||
api.InitConfigLocal()
|
||||
api.InitWebhookLocal()
|
||||
api.InitPluginLocal()
|
||||
api.InitCommandLocal()
|
||||
api.InitLicenseLocal()
|
||||
api.InitBotLocal()
|
||||
api.InitGroupLocal()
|
||||
api.InitLdapLocal()
|
||||
api.InitSystemLocal()
|
||||
api.InitPostLocal()
|
||||
api.InitRoleLocal()
|
||||
api.InitUploadLocal()
|
||||
api.InitImportLocal()
|
||||
api.InitExportLocal()
|
||||
api.InitJobLocal()
|
||||
api.InitSamlLocal()
|
||||
|
||||
srv.LocalRouter.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
func (api *API) Handle404(w http.ResponseWriter, r *http.Request) {
|
||||
app := app.New(app.ServerConnector(api.srv.Channels()))
|
||||
web.Handle404(app, w, r)
|
||||
}
|
||||
|
||||
var ReturnStatusOK = web.ReturnStatusOK
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
func (api *API) InitBleve() {
|
||||
api.BaseRoutes.Bleve.Handle("/purge_indexes", api.APISessionRequired(purgeBleveIndexes)).Methods("POST")
|
||||
}
|
||||
|
||||
func purgeBleveIndexes(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
auditRec := c.MakeAuditRecord("purgeBleveIndexes", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionPurgeBleveIndexes) {
|
||||
c.SetPermissionError(model.PermissionPurgeBleveIndexes)
|
||||
return
|
||||
}
|
||||
|
||||
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
|
||||
c.Err = model.NewAppError("purgeBleveIndexes", "api.restricted_system_admin", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.PurgeBleveIndexes(); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBlevePurgeIndexes(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("as system user", func(t *testing.T) {
|
||||
resp, err := th.Client.PurgeBleveIndexes()
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("as system user with write experimental permission", func(t *testing.T) {
|
||||
th.AddPermissionToRole(model.PermissionPurgeBleveIndexes.Id, model.SystemUserRoleId)
|
||||
defer th.RemovePermissionFromRole(model.PermissionSysconsoleWriteExperimental.Id, model.SystemUserRoleId)
|
||||
resp, err := th.Client.PurgeBleveIndexes()
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("as system admin", func(t *testing.T) {
|
||||
resp, err := th.SystemAdminClient.PurgeBleveIndexes()
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("as restricted system admin", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true })
|
||||
|
||||
resp, err := th.SystemAdminClient.PurgeBleveIndexes()
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
}
|
||||
320
api4/bot.go
320
api4/bot.go
|
|
@ -1,320 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitBot() {
|
||||
api.BaseRoutes.Bots.Handle("", api.APISessionRequired(createBot)).Methods("POST")
|
||||
api.BaseRoutes.Bot.Handle("", api.APISessionRequired(patchBot)).Methods("PUT")
|
||||
api.BaseRoutes.Bot.Handle("", api.APISessionRequired(getBot)).Methods("GET")
|
||||
api.BaseRoutes.Bots.Handle("", api.APISessionRequired(getBots)).Methods("GET")
|
||||
api.BaseRoutes.Bot.Handle("/disable", api.APISessionRequired(disableBot)).Methods("POST")
|
||||
api.BaseRoutes.Bot.Handle("/enable", api.APISessionRequired(enableBot)).Methods("POST")
|
||||
api.BaseRoutes.Bot.Handle("/convert_to_user", api.APISessionRequired(convertBotToUser)).Methods("POST")
|
||||
api.BaseRoutes.Bot.Handle("/assign/{user_id:[A-Za-z0-9]+}", api.APISessionRequired(assignBot)).Methods("POST")
|
||||
}
|
||||
|
||||
func createBot(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var botPatch *model.BotPatch
|
||||
err := json.NewDecoder(r.Body).Decode(&botPatch)
|
||||
if err != nil {
|
||||
c.SetInvalidParamWithErr("bot", err)
|
||||
return
|
||||
}
|
||||
|
||||
bot := &model.Bot{
|
||||
OwnerId: c.AppContext.Session().UserId,
|
||||
}
|
||||
bot.Patch(botPatch)
|
||||
|
||||
auditRec := c.MakeAuditRecord("createBot", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameterAuditable(auditRec, "bot", bot)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateBot) {
|
||||
c.SetPermissionError(model.PermissionCreateBot)
|
||||
return
|
||||
}
|
||||
|
||||
if user, err := c.App.GetUser(c.AppContext.Session().UserId); err == nil {
|
||||
if user.IsBot {
|
||||
c.SetPermissionError(model.PermissionCreateBot)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !*c.App.Config().ServiceSettings.EnableBotAccountCreation {
|
||||
c.Err = model.NewAppError("createBot", "api.bot.create_disabled", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
createdBot, appErr := c.App.CreateBot(c.AppContext, bot)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventObjectType("bot")
|
||||
auditRec.AddEventResultState(createdBot) // overwrite meta
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(createdBot); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func patchBot(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireBotUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
botUserId := c.Params.BotUserId
|
||||
|
||||
var botPatch *model.BotPatch
|
||||
err := json.NewDecoder(r.Body).Decode(&botPatch)
|
||||
if err != nil {
|
||||
c.SetInvalidParamWithErr("bot", err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("patchBot", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "id", botUserId)
|
||||
audit.AddEventParameterAuditable(auditRec, "bot", botPatch)
|
||||
|
||||
if err := c.App.SessionHasPermissionToManageBot(*c.AppContext.Session(), botUserId); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
updatedBot, appErr := c.App.PatchBot(botUserId, botPatch)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(updatedBot)
|
||||
auditRec.AddEventObjectType("bot")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(updatedBot); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getBot(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireBotUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
botUserId := c.Params.BotUserId
|
||||
|
||||
includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted"))
|
||||
|
||||
bot, appErr := c.App.GetBot(botUserId, includeDeleted)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadOthersBots) {
|
||||
// Allow access to any bot.
|
||||
} else if bot.OwnerId == c.AppContext.Session().UserId {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadBots) {
|
||||
// Pretend like the bot doesn't exist at all to avoid revealing that the
|
||||
// user is a bot. It's kind of silly in this case, sine we created the bot,
|
||||
// but we don't have read bot permissions.
|
||||
c.Err = model.MakeBotNotFoundError(botUserId)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Pretend like the bot doesn't exist at all, to avoid revealing that the
|
||||
// user is a bot.
|
||||
c.Err = model.MakeBotNotFoundError(botUserId)
|
||||
return
|
||||
}
|
||||
|
||||
if c.HandleEtag(bot.Etag(), "Get Bot", w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(bot); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getBots(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted"))
|
||||
onlyOrphaned, _ := strconv.ParseBool(r.URL.Query().Get("only_orphaned"))
|
||||
|
||||
var OwnerId string
|
||||
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadOthersBots) {
|
||||
// Get bots created by any user.
|
||||
OwnerId = ""
|
||||
} else if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadBots) {
|
||||
// Only get bots created by this user.
|
||||
OwnerId = c.AppContext.Session().UserId
|
||||
} else {
|
||||
c.SetPermissionError(model.PermissionReadBots)
|
||||
return
|
||||
}
|
||||
|
||||
bots, appErr := c.App.GetBots(&model.BotGetOptions{
|
||||
Page: c.Params.Page,
|
||||
PerPage: c.Params.PerPage,
|
||||
OwnerId: OwnerId,
|
||||
IncludeDeleted: includeDeleted,
|
||||
OnlyOrphaned: onlyOrphaned,
|
||||
})
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if c.HandleEtag(bots.Etag(), "Get Bots", w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(bots); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func disableBot(c *Context, w http.ResponseWriter, _ *http.Request) {
|
||||
updateBotActive(c, w, false)
|
||||
}
|
||||
|
||||
func enableBot(c *Context, w http.ResponseWriter, _ *http.Request) {
|
||||
updateBotActive(c, w, true)
|
||||
}
|
||||
|
||||
func updateBotActive(c *Context, w http.ResponseWriter, active bool) {
|
||||
c.RequireBotUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
botUserId := c.Params.BotUserId
|
||||
|
||||
auditRec := c.MakeAuditRecord("updateBotActive", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "id", botUserId)
|
||||
audit.AddEventParameter(auditRec, "enable", active)
|
||||
|
||||
if err := c.App.SessionHasPermissionToManageBot(*c.AppContext.Session(), botUserId); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
bot, err := c.App.UpdateBotActive(c.AppContext, botUserId, active)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(bot)
|
||||
auditRec.AddEventObjectType("bot")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(bot); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func assignBot(c *Context, w http.ResponseWriter, _ *http.Request) {
|
||||
c.RequireUserId()
|
||||
c.RequireBotUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
botUserId := c.Params.BotUserId
|
||||
userId := c.Params.UserId
|
||||
|
||||
auditRec := c.MakeAuditRecord("assignBot", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "id", botUserId)
|
||||
audit.AddEventParameter(auditRec, "user_id", userId)
|
||||
|
||||
if err := c.App.SessionHasPermissionToManageBot(*c.AppContext.Session(), botUserId); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if user, err := c.App.GetUser(userId); err == nil {
|
||||
if user.IsBot {
|
||||
c.SetPermissionError(model.PermissionAssignBot)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
bot, err := c.App.UpdateBotOwner(botUserId, userId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(bot)
|
||||
auditRec.AddEventObjectType("bot")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(bot); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func convertBotToUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireBotUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
bot, err := c.App.GetBot(c.Params.BotUserId, false)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
var userPatch model.UserPatch
|
||||
jsonErr := json.NewDecoder(r.Body).Decode(&userPatch)
|
||||
if jsonErr != nil || userPatch.Password == nil || *userPatch.Password == "" {
|
||||
c.SetInvalidParamWithErr("userPatch", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
systemAdmin, _ := strconv.ParseBool(r.URL.Query().Get("set_system_admin"))
|
||||
|
||||
auditRec := c.MakeAuditRecord("convertBotToUser", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameterAuditable(auditRec, "bot", bot)
|
||||
audit.AddEventParameterAuditable(auditRec, "user_patch", &userPatch)
|
||||
audit.AddEventParameter(auditRec, "set_system_admin", systemAdmin)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := c.App.ConvertBotToUser(c.AppContext, bot, &userPatch, systemAdmin)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(user)
|
||||
auditRec.AddEventObjectType("user")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(user); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
2137
api4/channel.go
2137
api4/channel.go
File diff suppressed because it is too large
Load diff
|
|
@ -1,333 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func getCategoriesForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId().RequireTeamId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
|
||||
categories, appErr := c.App.GetSidebarCategoriesForTeamForUser(c.AppContext, c.Params.UserId, c.Params.TeamId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
categoriesJSON, err := json.Marshal(categories)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getCategoriesForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(categoriesJSON)
|
||||
}
|
||||
|
||||
func createCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId().RequireTeamId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("createCategoryForTeamForUser", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
var categoryCreateRequest model.SidebarCategoryWithChannels
|
||||
err := json.NewDecoder(r.Body).Decode(&categoryCreateRequest)
|
||||
if err != nil || c.Params.UserId != categoryCreateRequest.UserId || c.Params.TeamId != categoryCreateRequest.TeamId {
|
||||
c.SetInvalidParamWithErr("category", err)
|
||||
return
|
||||
}
|
||||
|
||||
if appErr := validateSidebarCategory(c, c.Params.TeamId, c.Params.UserId, &categoryCreateRequest); appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
category, appErr := c.App.CreateSidebarCategory(c.AppContext, c.Params.UserId, c.Params.TeamId, &categoryCreateRequest)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
categoryJSON, err := json.Marshal(category)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("createCategoryForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
w.Write(categoryJSON)
|
||||
}
|
||||
|
||||
func getCategoryOrderForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId().RequireTeamId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
|
||||
order, appErr := c.App.GetSidebarCategoryOrder(c.AppContext, c.Params.UserId, c.Params.TeamId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
err := json.NewEncoder(w).Encode(order)
|
||||
if err != nil {
|
||||
c.Logger.Warn("Error writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func updateCategoryOrderForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId().RequireTeamId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("updateCategoryOrderForTeamForUser", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
categoryOrder := model.ArrayFromJSON(r.Body)
|
||||
|
||||
for _, categoryId := range categoryOrder {
|
||||
if !c.App.SessionHasPermissionToCategory(c.AppContext, *c.AppContext.Session(), c.Params.UserId, c.Params.TeamId, categoryId) {
|
||||
c.SetInvalidParam("category")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err := c.App.UpdateSidebarCategoryOrder(c.AppContext, c.Params.UserId, c.Params.TeamId, categoryOrder)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
w.Write([]byte(model.ArrayToJSON(categoryOrder)))
|
||||
}
|
||||
|
||||
func getCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId().RequireTeamId().RequireCategoryId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToCategory(c.AppContext, *c.AppContext.Session(), c.Params.UserId, c.Params.TeamId, c.Params.CategoryId) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
|
||||
categories, appErr := c.App.GetSidebarCategory(c.AppContext, c.Params.CategoryId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
categoriesJSON, err := json.Marshal(categories)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getCategoryForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(categoriesJSON)
|
||||
}
|
||||
|
||||
func updateCategoriesForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId().RequireTeamId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("updateCategoriesForTeamForUser", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
var categoriesUpdateRequest []*model.SidebarCategoryWithChannels
|
||||
err := json.NewDecoder(r.Body).Decode(&categoriesUpdateRequest)
|
||||
if err != nil {
|
||||
c.SetInvalidParamWithErr("category", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, category := range categoriesUpdateRequest {
|
||||
if !c.App.SessionHasPermissionToCategory(c.AppContext, *c.AppContext.Session(), c.Params.UserId, c.Params.TeamId, category.Id) {
|
||||
c.SetInvalidParam("category")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if appErr := validateSidebarCategories(c, c.Params.TeamId, c.Params.UserId, categoriesUpdateRequest); appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
categories, appErr := c.App.UpdateSidebarCategories(c.AppContext, c.Params.UserId, c.Params.TeamId, categoriesUpdateRequest)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
categoriesJSON, err := json.Marshal(categories)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("updateCategoriesForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
w.Write(categoriesJSON)
|
||||
}
|
||||
|
||||
func validateSidebarCategory(c *Context, teamId, userId string, category *model.SidebarCategoryWithChannels) *model.AppError {
|
||||
channels, appErr := c.App.GetChannelsForTeamForUser(c.AppContext, teamId, userId, &model.ChannelSearchOpts{
|
||||
IncludeDeleted: true,
|
||||
LastDeleteAt: 0,
|
||||
})
|
||||
if appErr != nil {
|
||||
return model.NewAppError("validateSidebarCategory", "api.invalid_channel", nil, "", http.StatusBadRequest).Wrap(appErr)
|
||||
}
|
||||
|
||||
category.Channels = validateSidebarCategoryChannels(c, userId, category.Channels, channels)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSidebarCategories(c *Context, teamId, userId string, categories []*model.SidebarCategoryWithChannels) *model.AppError {
|
||||
channels, err := c.App.GetChannelsForTeamForUser(c.AppContext, teamId, userId, &model.ChannelSearchOpts{
|
||||
IncludeDeleted: true,
|
||||
LastDeleteAt: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return model.NewAppError("validateSidebarCategory", "api.invalid_channel", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
for _, category := range categories {
|
||||
category.Channels = validateSidebarCategoryChannels(c, userId, category.Channels, channels)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSidebarCategoryChannels(c *Context, userId string, channelIds []string, channels model.ChannelList) []string {
|
||||
var filtered []string
|
||||
|
||||
for _, channelId := range channelIds {
|
||||
found := false
|
||||
for _, channel := range channels {
|
||||
if channel.Id == channelId {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
filtered = append(filtered, channelId)
|
||||
} else {
|
||||
c.Logger.Info("Stopping user from adding channel to their sidebar when they are not a member", mlog.String("user_id", userId), mlog.String("channel_id", channelId))
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
func updateCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId().RequireTeamId().RequireCategoryId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToCategory(c.AppContext, *c.AppContext.Session(), c.Params.UserId, c.Params.TeamId, c.Params.CategoryId) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("updateCategoryForTeamForUser", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
var categoryUpdateRequest model.SidebarCategoryWithChannels
|
||||
err := json.NewDecoder(r.Body).Decode(&categoryUpdateRequest)
|
||||
if err != nil || categoryUpdateRequest.TeamId != c.Params.TeamId || categoryUpdateRequest.UserId != c.Params.UserId {
|
||||
c.SetInvalidParamWithErr("category", err)
|
||||
return
|
||||
}
|
||||
|
||||
if appErr := validateSidebarCategory(c, c.Params.TeamId, c.Params.UserId, &categoryUpdateRequest); appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
categoryUpdateRequest.Id = c.Params.CategoryId
|
||||
|
||||
categories, appErr := c.App.UpdateSidebarCategories(c.AppContext, c.Params.UserId, c.Params.TeamId, []*model.SidebarCategoryWithChannels{&categoryUpdateRequest})
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
categoryJSON, err := json.Marshal(categories[0])
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("updateCategoryForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
w.Write(categoryJSON)
|
||||
}
|
||||
|
||||
func deleteCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId().RequireTeamId().RequireCategoryId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToCategory(c.AppContext, *c.AppContext.Session(), c.Params.UserId, c.Params.TeamId, c.Params.CategoryId) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("deleteCategoryForTeamForUser", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
appErr := c.App.DeleteSidebarCategory(c.AppContext, c.Params.UserId, c.Params.TeamId, c.Params.CategoryId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
4624
api4/channel_test.go
4624
api4/channel_test.go
File diff suppressed because it is too large
Load diff
804
api4/cloud.go
804
api4/cloud.go
|
|
@ -1,804 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/web"
|
||||
)
|
||||
|
||||
func (api *API) InitCloud() {
|
||||
// GET /api/v4/cloud/products
|
||||
api.BaseRoutes.Cloud.Handle("/products", api.APISessionRequired(getCloudProducts)).Methods("GET")
|
||||
// GET /api/v4/cloud/limits
|
||||
api.BaseRoutes.Cloud.Handle("/limits", api.APISessionRequired(getCloudLimits)).Methods("GET")
|
||||
|
||||
api.BaseRoutes.Cloud.Handle("/products/selfhosted", api.APISessionRequired(getSelfHostedProducts)).Methods("GET")
|
||||
|
||||
// POST /api/v4/cloud/payment
|
||||
// POST /api/v4/cloud/payment/confirm
|
||||
api.BaseRoutes.Cloud.Handle("/payment", api.APISessionRequired(createCustomerPayment)).Methods("POST")
|
||||
api.BaseRoutes.Cloud.Handle("/payment/confirm", api.APISessionRequired(confirmCustomerPayment)).Methods("POST")
|
||||
|
||||
// GET /api/v4/cloud/customer
|
||||
// PUT /api/v4/cloud/customer
|
||||
// PUT /api/v4/cloud/customer/address
|
||||
api.BaseRoutes.Cloud.Handle("/customer", api.APISessionRequired(getCloudCustomer)).Methods("GET")
|
||||
api.BaseRoutes.Cloud.Handle("/customer", api.APISessionRequired(updateCloudCustomer)).Methods("PUT")
|
||||
api.BaseRoutes.Cloud.Handle("/customer/address", api.APISessionRequired(updateCloudCustomerAddress)).Methods("PUT")
|
||||
|
||||
// GET /api/v4/cloud/subscription
|
||||
api.BaseRoutes.Cloud.Handle("/subscription", api.APISessionRequired(getSubscription)).Methods("GET")
|
||||
api.BaseRoutes.Cloud.Handle("/subscription/invoices", api.APISessionRequired(getInvoicesForSubscription)).Methods("GET")
|
||||
api.BaseRoutes.Cloud.Handle("/subscription/invoices/{invoice_id:[_A-Za-z0-9]+}/pdf", api.APISessionRequired(getSubscriptionInvoicePDF)).Methods("GET")
|
||||
api.BaseRoutes.Cloud.Handle("/subscription/self-serve-status", api.APISessionRequired(getLicenseSelfServeStatus)).Methods("GET")
|
||||
api.BaseRoutes.Cloud.Handle("/subscription", api.APISessionRequired(changeSubscription)).Methods("PUT")
|
||||
|
||||
// GET /api/v4/cloud/request-trial
|
||||
api.BaseRoutes.Cloud.Handle("/request-trial", api.APISessionRequired(requestCloudTrial)).Methods("PUT")
|
||||
|
||||
// GET /api/v4/cloud/validate-business-email
|
||||
api.BaseRoutes.Cloud.Handle("/validate-business-email", api.APISessionRequired(validateBusinessEmail)).Methods("POST")
|
||||
api.BaseRoutes.Cloud.Handle("/validate-workspace-business-email", api.APISessionRequired(validateWorkspaceBusinessEmail)).Methods("POST")
|
||||
|
||||
// POST /api/v4/cloud/webhook
|
||||
api.BaseRoutes.Cloud.Handle("/webhook", api.CloudAPIKeyRequired(handleCWSWebhook)).Methods("POST")
|
||||
|
||||
// GET /api/v4/cloud/cws-health-check
|
||||
api.BaseRoutes.Cloud.Handle("/check-cws-connection", api.APIHandler(handleCheckCWSConnection)).Methods("GET")
|
||||
|
||||
api.BaseRoutes.Cloud.Handle("/delete-workspace", api.APISessionRequired(selfServeDeleteWorkspace)).Methods(http.MethodDelete)
|
||||
}
|
||||
|
||||
func getSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.Channels().License().IsCloud() {
|
||||
c.Err = model.NewAppError("Api4.getSubscription", "api.cloud.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
subscription, err := c.App.Cloud().GetSubscription(c.AppContext.Session().UserId)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getSubscription", "api.cloud.request_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// if it is an end user, return basic subscription data without sensitive information
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
|
||||
subscription = &model.Subscription{
|
||||
ID: subscription.ID,
|
||||
ProductID: subscription.ProductID,
|
||||
IsFreeTrial: subscription.IsFreeTrial,
|
||||
TrialEndAt: subscription.TrialEndAt,
|
||||
CustomerID: "",
|
||||
AddOns: []string{},
|
||||
StartAt: 0,
|
||||
EndAt: 0,
|
||||
CreateAt: 0,
|
||||
Seats: 0,
|
||||
Status: "",
|
||||
DNS: "",
|
||||
LastInvoice: &model.Invoice{},
|
||||
DelinquentSince: subscription.DelinquentSince,
|
||||
}
|
||||
}
|
||||
|
||||
json, err := json.Marshal(subscription)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getSubscription", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(json)
|
||||
}
|
||||
|
||||
func changeSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
userId := c.AppContext.Session().UserId
|
||||
|
||||
if !c.App.Channels().License().IsCloud() {
|
||||
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.license_error", nil, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
|
||||
return
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
var subscriptionChange *model.SubscriptionChange
|
||||
if err = json.Unmarshal(bodyBytes, &subscriptionChange); err != nil {
|
||||
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
currentSubscription, appErr := c.App.Cloud().GetSubscription(userId)
|
||||
if appErr != nil {
|
||||
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
||||
return
|
||||
}
|
||||
|
||||
changedSub, err := c.App.Cloud().ChangeSubscription(userId, currentSubscription.ID, subscriptionChange)
|
||||
if err != nil {
|
||||
appErr := model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
if err.Error() == "compliance-failed" {
|
||||
c.Logger.Error("Compliance check failed", mlog.Err(err))
|
||||
appErr.StatusCode = http.StatusUnprocessableEntity
|
||||
}
|
||||
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if subscriptionChange.Feedback != nil {
|
||||
c.App.Srv().GetTelemetryService().SendTelemetry("downgrade_feedback", subscriptionChange.Feedback.ToMap())
|
||||
}
|
||||
|
||||
json, err := json.Marshal(changedSub)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
product, err := c.App.Cloud().GetCloudProduct(c.AppContext.Session().UserId, subscriptionChange.ProductID)
|
||||
if err != nil || product == nil {
|
||||
c.Logger.Error("Error finding the new cloud product", mlog.Err(err))
|
||||
}
|
||||
|
||||
if product.SKU == string(model.SkuCloudStarter) {
|
||||
w.Write(json)
|
||||
return
|
||||
}
|
||||
|
||||
isYearly := product.IsYearly()
|
||||
|
||||
// Log failures for purchase confirmation email, but don't show an error to the user so as not to confuse them
|
||||
// At this point, the upgrade is complete.
|
||||
if appErr := c.App.SendUpgradeConfirmationEmail(isYearly); appErr != nil {
|
||||
c.Logger.Error("Error sending purchase confirmation email", mlog.Err(appErr))
|
||||
}
|
||||
|
||||
w.Write(json)
|
||||
}
|
||||
|
||||
func requestCloudTrial(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.Channels().License().IsCloud() {
|
||||
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
|
||||
return
|
||||
}
|
||||
|
||||
// check if the email needs to be set
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
// this value will not be empty when both emails (user admin and CWS customer) are not business email and
|
||||
// a new business email was provided via the request business email modal
|
||||
var startTrialRequest *model.StartCloudTrialRequest
|
||||
if err = json.Unmarshal(bodyBytes, &startTrialRequest); err != nil {
|
||||
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
changedSub, err := c.App.Cloud().RequestCloudTrial(c.AppContext.Session().UserId, startTrialRequest.SubscriptionID, startTrialRequest.Email)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
json, err := json.Marshal(changedSub)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer c.App.Srv().Cloud.InvalidateCaches()
|
||||
|
||||
w.Write(json)
|
||||
}
|
||||
|
||||
func validateBusinessEmail(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.Channels().License().IsCloud() {
|
||||
c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
|
||||
return
|
||||
}
|
||||
|
||||
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
|
||||
if appErr != nil {
|
||||
c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.request_error", nil, "", http.StatusForbidden).Wrap(appErr)
|
||||
return
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
var emailToValidate *model.ValidateBusinessEmailRequest
|
||||
err = json.Unmarshal(bodyBytes, &emailToValidate)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = c.App.Cloud().ValidateBusinessEmail(user.Id, emailToValidate.Email)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.request_error", nil, "", http.StatusForbidden).Wrap(err)
|
||||
emailResp := model.ValidateBusinessEmailResponse{IsValid: false}
|
||||
if err := json.NewEncoder(w).Encode(emailResp); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
emailResp := model.ValidateBusinessEmailResponse{IsValid: true}
|
||||
if err := json.NewEncoder(w).Encode(emailResp); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func validateWorkspaceBusinessEmail(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.Channels().License().IsCloud() {
|
||||
c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
|
||||
return
|
||||
}
|
||||
|
||||
user, userErr := c.App.GetUser(c.AppContext.Session().UserId)
|
||||
if userErr != nil {
|
||||
c.Err = userErr
|
||||
return
|
||||
}
|
||||
|
||||
// get the cloud customer email to validate if is a valid business email
|
||||
cloudCustomer, err := c.App.Cloud().GetCloudCustomer(user.Id)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.request_error", nil, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
emailErr := c.App.Cloud().ValidateBusinessEmail(user.Id, cloudCustomer.Email)
|
||||
|
||||
// if the current workspace email is not a valid business email
|
||||
if emailErr != nil {
|
||||
// grab the current admin email and validate it
|
||||
errValidatingAdminEmail := c.App.Cloud().ValidateBusinessEmail(user.Id, user.Email)
|
||||
if errValidatingAdminEmail != nil {
|
||||
c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.request_error", nil, errValidatingAdminEmail.Error(), http.StatusForbidden)
|
||||
emailResp := model.ValidateBusinessEmailResponse{IsValid: false}
|
||||
if err := json.NewEncoder(w).Encode(emailResp); err != nil {
|
||||
mlog.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// if any of the emails is valid, return ok
|
||||
emailResp := model.ValidateBusinessEmailResponse{IsValid: true}
|
||||
if err := json.NewEncoder(w).Encode(emailResp); err != nil {
|
||||
mlog.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getSelfHostedProducts(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
products, err := c.App.Cloud().GetSelfHostedProducts(c.AppContext.Session().UserId)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getSelfHostedProducts", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
byteProductsData, err := json.Marshal(products)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getSelfHostedProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
|
||||
sanitizedProducts := []model.UserFacingProduct{}
|
||||
err = json.Unmarshal(byteProductsData, &sanitizedProducts)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getSelfHostedProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
byteSanitizedProductsData, err := json.Marshal(sanitizedProducts)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getSelfHostedProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(byteSanitizedProductsData)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(byteProductsData)
|
||||
}
|
||||
|
||||
func getCloudProducts(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.Channels().License().IsCloud() {
|
||||
c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
includeLegacyProducts := r.URL.Query().Get("include_legacy") == "true"
|
||||
|
||||
products, err := c.App.Cloud().GetCloudProducts(c.AppContext.Session().UserId, includeLegacyProducts)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
byteProductsData, err := json.Marshal(products)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
|
||||
sanitizedProducts := []model.UserFacingProduct{}
|
||||
err = json.Unmarshal(byteProductsData, &sanitizedProducts)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
byteSanitizedProductsData, err := json.Marshal(sanitizedProducts)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(byteSanitizedProductsData)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(byteProductsData)
|
||||
}
|
||||
|
||||
func getCloudLimits(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.Channels().License().IsCloud() {
|
||||
c.Err = model.NewAppError("Api4.getCloudLimits", "api.cloud.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
limits, err := c.App.Cloud().GetCloudLimits(c.AppContext.Session().UserId)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getCloudLimits", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
json, err := json.Marshal(limits)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getCloudLimits", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(json)
|
||||
}
|
||||
|
||||
func getCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.Channels().License().IsCloud() {
|
||||
c.Err = model.NewAppError("Api4.getCloudCustomer", "api.cloud.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadBilling)
|
||||
return
|
||||
}
|
||||
|
||||
customer, err := c.App.Cloud().GetCloudCustomer(c.AppContext.Session().UserId)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getCloudCustomer", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
json, err := json.Marshal(customer)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getCloudCustomer", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(json)
|
||||
}
|
||||
|
||||
// getLicenseSelfServeStatus makes check for the license in the CWS self-serve portal and establishes if the license is renewable, expandable etc.
|
||||
func getLicenseSelfServeStatus(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) {
|
||||
c.SetPermissionError(model.PermissionManageLicenseInformation)
|
||||
return
|
||||
}
|
||||
|
||||
_, token, err := c.App.Srv().GenerateLicenseRenewalLink()
|
||||
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
status, cloudErr := c.App.Cloud().GetLicenseSelfServeStatus(c.AppContext.Session().UserId, token)
|
||||
if cloudErr != nil {
|
||||
c.Err = model.NewAppError("Api4.getLicenseSelfServeStatus", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(cloudErr)
|
||||
return
|
||||
}
|
||||
|
||||
json, jsonErr := json.Marshal(status)
|
||||
if jsonErr != nil {
|
||||
c.Err = model.NewAppError("Api4.getLicenseSelfServeStatus", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(json)
|
||||
}
|
||||
|
||||
func updateCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.Channels().License().IsCloud() {
|
||||
c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
|
||||
return
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
var customerInfo *model.CloudCustomerInfo
|
||||
if err = json.Unmarshal(bodyBytes, &customerInfo); err != nil {
|
||||
c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
customer, appErr := c.App.Cloud().UpdateCloudCustomer(c.AppContext.Session().UserId, customerInfo)
|
||||
if appErr != nil {
|
||||
c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
||||
return
|
||||
}
|
||||
|
||||
json, err := json.Marshal(customer)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(json)
|
||||
}
|
||||
|
||||
func updateCloudCustomerAddress(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.Channels().License().IsCloud() {
|
||||
c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
|
||||
return
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
var address *model.Address
|
||||
if err = json.Unmarshal(bodyBytes, &address); err != nil {
|
||||
c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
customer, appErr := c.App.Cloud().UpdateCloudCustomerAddress(c.AppContext.Session().UserId, address)
|
||||
if appErr != nil {
|
||||
c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
||||
return
|
||||
}
|
||||
|
||||
json, err := json.Marshal(customer)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(json)
|
||||
}
|
||||
|
||||
func createCustomerPayment(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.Channels().License().IsCloud() {
|
||||
c.Err = model.NewAppError("Api4.createCustomerPayment", "api.cloud.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("createCustomerPayment", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
intent, err := c.App.Cloud().CreateCustomerPayment(c.AppContext.Session().UserId)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.createCustomerPayment", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
json, err := json.Marshal(intent)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.createCustomerPayment", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
w.Write(json)
|
||||
}
|
||||
|
||||
func confirmCustomerPayment(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.Channels().License().IsCloud() {
|
||||
c.Err = model.NewAppError("Api4.confirmCustomerPayment", "api.cloud.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("confirmCustomerPayment", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.confirmCustomerPayment", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
var confirmRequest *model.ConfirmPaymentMethodRequest
|
||||
if err = json.Unmarshal(bodyBytes, &confirmRequest); err != nil {
|
||||
c.Err = model.NewAppError("Api4.confirmCustomerPayment", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = c.App.Cloud().ConfirmCustomerPayment(c.AppContext.Session().UserId, confirmRequest)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.createCustomerPayment", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func getInvoicesForSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.Channels().License().IsCloud() {
|
||||
c.Err = model.NewAppError("Api4.getInvoicesForSubscription", "api.cloud.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadBilling)
|
||||
return
|
||||
}
|
||||
|
||||
invoices, appErr := c.App.Cloud().GetInvoicesForSubscription(c.AppContext.Session().UserId)
|
||||
if appErr != nil {
|
||||
c.Err = model.NewAppError("Api4.getInvoicesForSubscription", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
||||
return
|
||||
}
|
||||
|
||||
json, err := json.Marshal(invoices)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getInvoicesForSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(json)
|
||||
}
|
||||
|
||||
func getSubscriptionInvoicePDF(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.Channels().License().IsCloud() {
|
||||
c.Err = model.NewAppError("Api4.getSubscriptionInvoicePDF", "api.cloud.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireInvoiceId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadBilling)
|
||||
return
|
||||
}
|
||||
|
||||
pdfData, filename, appErr := c.App.Cloud().GetInvoicePDF(c.AppContext.Session().UserId, c.Params.InvoiceId)
|
||||
if appErr != nil {
|
||||
c.Err = model.NewAppError("Api4.getSubscriptionInvoicePDF", "api.cloud.request_error", nil, appErr.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
web.WriteFileResponse(
|
||||
filename,
|
||||
"application/pdf",
|
||||
int64(binary.Size(pdfData)),
|
||||
time.Now(),
|
||||
*c.App.Config().ServiceSettings.WebserverMode,
|
||||
bytes.NewReader(pdfData),
|
||||
false,
|
||||
w,
|
||||
r,
|
||||
)
|
||||
}
|
||||
|
||||
func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.Channels().License().IsCloud() {
|
||||
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
var event *model.CWSWebhookPayload
|
||||
if err = json.Unmarshal(bodyBytes, &event); err != nil {
|
||||
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
switch event.Event {
|
||||
case model.EventTypeFailedPayment:
|
||||
if nErr := c.App.SendPaymentFailedEmail(event.FailedPayment); nErr != nil {
|
||||
c.Err = nErr
|
||||
return
|
||||
}
|
||||
case model.EventTypeFailedPaymentNoCard:
|
||||
if nErr := c.App.SendNoCardPaymentFailedEmail(); nErr != nil {
|
||||
c.Err = nErr
|
||||
return
|
||||
}
|
||||
case model.EventTypeSendUpgradeConfirmationEmail:
|
||||
|
||||
// isYearly determines whether to send the yearly or monthly Upgrade email
|
||||
isYearly := false
|
||||
if event.Subscription != nil && event.CloudWorkspaceOwner != nil {
|
||||
user, appErr := c.App.GetUserByUsername(event.CloudWorkspaceOwner.UserName)
|
||||
if appErr != nil {
|
||||
c.Err = model.NewAppError("Api4.handleCWSWebhook", appErr.Id, nil, appErr.Error(), appErr.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the current cloud product to determine whether it's a monthly or yearly product
|
||||
product, err := c.App.Cloud().GetCloudProduct(user.Id, event.Subscription.ProductID)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.request_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
isYearly = product.IsYearly()
|
||||
}
|
||||
|
||||
if nErr := c.App.SendUpgradeConfirmationEmail(isYearly); nErr != nil {
|
||||
c.Err = nErr
|
||||
return
|
||||
}
|
||||
case model.EventTypeSendAdminWelcomeEmail:
|
||||
user, appErr := c.App.GetUserByUsername(event.CloudWorkspaceOwner.UserName)
|
||||
if appErr != nil {
|
||||
c.Err = model.NewAppError("Api4.handleCWSWebhook", appErr.Id, nil, appErr.Error(), appErr.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
teams, appErr := c.App.GetAllTeams()
|
||||
if appErr != nil {
|
||||
c.Err = model.NewAppError("Api4.handleCWSWebhook", appErr.Id, nil, appErr.Error(), appErr.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
team := teams[0]
|
||||
|
||||
subscription, err := c.App.Cloud().GetSubscription(user.Id)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.request_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.Srv().EmailService.SendCloudWelcomeEmail(user.Email, user.Locale, team.InviteId, subscription.GetWorkSpaceNameFromDNS(), subscription.DNS, *c.App.Config().ServiceSettings.SiteURL); err != nil {
|
||||
c.Err = model.NewAppError("SendCloudWelcomeEmail", "api.user.send_cloud_welcome_email.error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
case model.EventTypeTriggerDelinquencyEmail:
|
||||
var emailToTrigger model.DelinquencyEmail
|
||||
if event.DelinquencyEmail != nil {
|
||||
emailToTrigger = model.DelinquencyEmail(event.DelinquencyEmail.EmailToTrigger)
|
||||
} else {
|
||||
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.delinquency_email.missing_email_to_trigger", nil, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if nErr := c.App.SendDelinquencyEmail(emailToTrigger); nErr != nil {
|
||||
c.Err = nErr
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.cws_webhook_event_missing_error", nil, "", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func handleCheckCWSConnection(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
cloud := c.App.Cloud()
|
||||
if cloud == nil {
|
||||
c.Err = model.NewAppError("Api4.handleCWSHealthCheck", "api.server.cws.needs_enterprise_edition", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := cloud.CheckCWSConnection(c.AppContext.Session().UserId); err != nil {
|
||||
c.Err = model.NewAppError("Api4.handleCWSHealthCheck", "api.server.cws.health_check.app_error", nil, "CWS Server is not available.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func selfServeDeleteWorkspace(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.selfServeDeleteWorkspace", "api.cloud.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
var deleteRequest *model.WorkspaceDeletionRequest
|
||||
if err = json.Unmarshal(bodyBytes, &deleteRequest); err != nil {
|
||||
c.Err = model.NewAppError("Api4.selfServeDeleteWorkspace", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.Cloud().SelfServeDeleteWorkspace(c.AppContext.Session().UserId, deleteRequest); err != nil {
|
||||
c.Err = model.NewAppError("Api4.selfServeDeleteWorkspace", "api.server.cws.delete_workspace.app_error", nil, "CWS Server failed to delete workspace.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.App.Srv().GetTelemetryService().SendTelemetry("delete_workspace_feedback", deleteRequest.Feedback.ToMap())
|
||||
|
||||
ReturnStatusOK(w)
|
||||
|
||||
}
|
||||
|
|
@ -1,809 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/einterfaces/mocks"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
func Test_getCloudLimits(t *testing.T) {
|
||||
t.Run("no license returns not implemented", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.Srv().RemoveLicense()
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
limits, r, err := th.Client.GetProductLimits()
|
||||
require.Error(t, err)
|
||||
require.Nil(t, limits)
|
||||
require.Equal(t, http.StatusForbidden, r.StatusCode, "Expected 403 forbidden")
|
||||
})
|
||||
|
||||
t.Run("non cloud license returns not implemented", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense())
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
limits, r, err := th.Client.GetProductLimits()
|
||||
require.Error(t, err)
|
||||
require.Nil(t, limits)
|
||||
require.Equal(t, http.StatusForbidden, r.StatusCode, "Expected 403 forbidden")
|
||||
})
|
||||
|
||||
t.Run("error fetching limits returns internal server error", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
cloud := &mocks.CloudInterface{}
|
||||
cloud.Mock.On("GetCloudLimits", mock.Anything).Return(nil, errors.New("Unable to get limits"))
|
||||
|
||||
cloudImpl := th.App.Srv().Cloud
|
||||
defer func() {
|
||||
th.App.Srv().Cloud = cloudImpl
|
||||
}()
|
||||
th.App.Srv().Cloud = cloud
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
limits, r, err := th.Client.GetProductLimits()
|
||||
require.Error(t, err)
|
||||
require.Nil(t, limits)
|
||||
require.Equal(t, http.StatusInternalServerError, r.StatusCode, "Expected 500 Internal Server Error")
|
||||
})
|
||||
|
||||
t.Run("unauthenticated users can not access", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Logout()
|
||||
|
||||
limits, r, err := th.Client.GetProductLimits()
|
||||
require.Error(t, err)
|
||||
require.Nil(t, limits)
|
||||
require.Equal(t, http.StatusUnauthorized, r.StatusCode, "Expected 401 Unauthorized")
|
||||
})
|
||||
|
||||
t.Run("good request with cloud server", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
cloud := &mocks.CloudInterface{}
|
||||
ten := 10
|
||||
mockLimits := &model.ProductLimits{
|
||||
Messages: &model.MessagesLimits{
|
||||
History: &ten,
|
||||
},
|
||||
}
|
||||
cloud.Mock.On("GetCloudLimits", mock.Anything).Return(mockLimits, nil)
|
||||
|
||||
cloudImpl := th.App.Srv().Cloud
|
||||
defer func() {
|
||||
th.App.Srv().Cloud = cloudImpl
|
||||
}()
|
||||
th.App.Srv().Cloud = cloud
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
limits, r, err := th.Client.GetProductLimits()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, r.StatusCode, "Expected 200 OK")
|
||||
require.Equal(t, mockLimits, limits)
|
||||
require.Equal(t, *mockLimits.Messages.History, *limits.Messages.History)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_GetSubscription(t *testing.T) {
|
||||
deliquencySince := int64(2000000000)
|
||||
|
||||
subscription := &model.Subscription{
|
||||
ID: "MySubscriptionID",
|
||||
CustomerID: "MyCustomer",
|
||||
ProductID: "SomeProductId",
|
||||
AddOns: []string{},
|
||||
StartAt: 1000000000,
|
||||
EndAt: 2000000000,
|
||||
CreateAt: 1000000000,
|
||||
Seats: 10,
|
||||
IsFreeTrial: "true",
|
||||
DNS: "some.dns.server",
|
||||
TrialEndAt: 2000000000,
|
||||
LastInvoice: &model.Invoice{},
|
||||
DelinquentSince: &deliquencySince,
|
||||
}
|
||||
|
||||
userFacingSubscription := &model.Subscription{
|
||||
ID: "MySubscriptionID",
|
||||
CustomerID: "",
|
||||
ProductID: "SomeProductId",
|
||||
AddOns: []string{},
|
||||
StartAt: 0,
|
||||
EndAt: 0,
|
||||
CreateAt: 0,
|
||||
Seats: 0,
|
||||
IsFreeTrial: "true",
|
||||
DNS: "",
|
||||
TrialEndAt: 2000000000,
|
||||
LastInvoice: &model.Invoice{},
|
||||
DelinquentSince: &deliquencySince,
|
||||
}
|
||||
|
||||
t.Run("NON Admin users receive the user facing subscription", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
cloud := mocks.CloudInterface{}
|
||||
|
||||
cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil)
|
||||
|
||||
cloudImpl := th.App.Srv().Cloud
|
||||
defer func() {
|
||||
th.App.Srv().Cloud = cloudImpl
|
||||
}()
|
||||
th.App.Srv().Cloud = &cloud
|
||||
|
||||
subscriptionReturned, r, err := th.Client.GetSubscription()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, subscriptionReturned, userFacingSubscription)
|
||||
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
|
||||
})
|
||||
|
||||
t.Run("Admin users receive the full subscription information", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
cloud := mocks.CloudInterface{}
|
||||
|
||||
cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil)
|
||||
|
||||
cloudImpl := th.App.Srv().Cloud
|
||||
defer func() {
|
||||
th.App.Srv().Cloud = cloudImpl
|
||||
}()
|
||||
th.App.Srv().Cloud = &cloud
|
||||
|
||||
subscriptionReturned, r, err := th.SystemAdminClient.GetSubscription()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, subscriptionReturned, subscription)
|
||||
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_requestTrial(t *testing.T) {
|
||||
subscription := &model.Subscription{
|
||||
ID: "MySubscriptionID",
|
||||
CustomerID: "MyCustomer",
|
||||
ProductID: "SomeProductId",
|
||||
AddOns: []string{},
|
||||
StartAt: 1000000000,
|
||||
EndAt: 2000000000,
|
||||
CreateAt: 1000000000,
|
||||
Seats: 10,
|
||||
DNS: "some.dns.server",
|
||||
}
|
||||
|
||||
newValidBusinessEmail := model.StartCloudTrialRequest{Email: ""}
|
||||
|
||||
t.Run("NON Admin users are UNABLE to request the trial", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
cloud := mocks.CloudInterface{}
|
||||
|
||||
cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil)
|
||||
cloud.Mock.On("RequestCloudTrial", mock.Anything, mock.Anything, "").Return(subscription, nil)
|
||||
cloud.Mock.On("InvalidateCaches").Return(nil)
|
||||
|
||||
cloudImpl := th.App.Srv().Cloud
|
||||
defer func() {
|
||||
th.App.Srv().Cloud = cloudImpl
|
||||
}()
|
||||
th.App.Srv().Cloud = &cloud
|
||||
|
||||
subscriptionChanged, r, err := th.Client.RequestCloudTrial(&newValidBusinessEmail)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, subscriptionChanged)
|
||||
require.Equal(t, http.StatusForbidden, r.StatusCode, "403 Forbidden")
|
||||
})
|
||||
|
||||
t.Run("ADMIN user are ABLE to request the trial", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
cloud := mocks.CloudInterface{}
|
||||
|
||||
cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil)
|
||||
cloud.Mock.On("RequestCloudTrial", mock.Anything, mock.Anything, "").Return(subscription, nil)
|
||||
cloud.Mock.On("InvalidateCaches").Return(nil)
|
||||
|
||||
cloudImpl := th.App.Srv().Cloud
|
||||
defer func() {
|
||||
th.App.Srv().Cloud = cloudImpl
|
||||
}()
|
||||
th.App.Srv().Cloud = &cloud
|
||||
|
||||
subscriptionChanged, r, err := th.SystemAdminClient.RequestCloudTrial(&newValidBusinessEmail)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, subscriptionChanged, subscription)
|
||||
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
|
||||
})
|
||||
|
||||
t.Run("ADMIN user are ABLE to request the trial with valid business email", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
// patch the customer with the additional contact updated with the valid business email
|
||||
newValidBusinessEmail.Email = *model.NewString("valid.email@mattermost.com")
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
cloud := mocks.CloudInterface{}
|
||||
|
||||
cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil)
|
||||
cloud.Mock.On("RequestCloudTrial", mock.Anything, mock.Anything, "valid.email@mattermost.com").Return(subscription, nil)
|
||||
cloud.Mock.On("InvalidateCaches").Return(nil)
|
||||
|
||||
cloudImpl := th.App.Srv().Cloud
|
||||
defer func() {
|
||||
th.App.Srv().Cloud = cloudImpl
|
||||
}()
|
||||
th.App.Srv().Cloud = &cloud
|
||||
|
||||
subscriptionChanged, r, err := th.SystemAdminClient.RequestCloudTrial(&newValidBusinessEmail)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, subscriptionChanged, subscription)
|
||||
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
|
||||
})
|
||||
|
||||
t.Run("Empty body returns bad request", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
r, err := th.SystemAdminClient.DoAPIPutBytes("/cloud/request-trial", nil)
|
||||
require.Error(t, err)
|
||||
closeBody(r)
|
||||
require.Equal(t, http.StatusBadRequest, r.StatusCode, "Status Bad Request")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_validateBusinessEmail(t *testing.T) {
|
||||
t.Run("Returns forbidden for non admin executors", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
invalidEmail := model.ValidateBusinessEmailRequest{Email: "invalid@gmail.com"}
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
cloud := mocks.CloudInterface{}
|
||||
|
||||
cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, invalidEmail.Email).Return(errors.New("invalid email"))
|
||||
|
||||
cloudImpl := th.App.Srv().Cloud
|
||||
defer func() {
|
||||
th.App.Srv().Cloud = cloudImpl
|
||||
}()
|
||||
th.App.Srv().Cloud = &cloud
|
||||
|
||||
res, err := th.Client.ValidateBusinessEmail(&invalidEmail)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusForbidden, res.StatusCode, "403")
|
||||
})
|
||||
|
||||
t.Run("Returns forbidden for invalid business email", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
validBusinessEmail := model.ValidateBusinessEmailRequest{Email: "invalid@slacker.com"}
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
cloud := mocks.CloudInterface{}
|
||||
|
||||
cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, validBusinessEmail.Email).Return(errors.New("invalid email"))
|
||||
|
||||
cloudImpl := th.App.Srv().Cloud
|
||||
defer func() {
|
||||
th.App.Srv().Cloud = cloudImpl
|
||||
}()
|
||||
th.App.Srv().Cloud = &cloud
|
||||
|
||||
res, err := th.SystemAdminClient.ValidateBusinessEmail(&validBusinessEmail)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusForbidden, res.StatusCode, "403")
|
||||
})
|
||||
|
||||
t.Run("Validate business email for admin", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
validBusinessEmail := model.ValidateBusinessEmailRequest{Email: "valid@mattermost.com"}
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
cloud := mocks.CloudInterface{}
|
||||
|
||||
cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, validBusinessEmail.Email).Return(nil)
|
||||
|
||||
cloudImpl := th.App.Srv().Cloud
|
||||
defer func() {
|
||||
th.App.Srv().Cloud = cloudImpl
|
||||
}()
|
||||
th.App.Srv().Cloud = &cloud
|
||||
|
||||
res, err := th.SystemAdminClient.ValidateBusinessEmail(&validBusinessEmail)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, "200")
|
||||
})
|
||||
|
||||
t.Run("Empty body returns bad request", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
r, err := th.SystemAdminClient.DoAPIPostBytes("/cloud/validate-business-email", nil)
|
||||
require.Error(t, err)
|
||||
closeBody(r)
|
||||
require.Equal(t, http.StatusBadRequest, r.StatusCode, "Status Bad Request")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_validateWorkspaceBusinessEmail(t *testing.T) {
|
||||
t.Run("validate the Cloud Customer has used a valid email to create the workspace", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
cloud := mocks.CloudInterface{}
|
||||
|
||||
cloudCustomerInfo := model.CloudCustomerInfo{
|
||||
Email: "valid@mattermost.com",
|
||||
}
|
||||
|
||||
cloudCustomer := &model.CloudCustomer{
|
||||
CloudCustomerInfo: cloudCustomerInfo,
|
||||
}
|
||||
|
||||
cloud.Mock.On("GetCloudCustomer", th.SystemAdminUser.Id).Return(cloudCustomer, nil)
|
||||
cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, cloudCustomerInfo.Email).Return(nil)
|
||||
|
||||
cloudImpl := th.App.Srv().Cloud
|
||||
defer func() {
|
||||
th.App.Srv().Cloud = cloudImpl
|
||||
}()
|
||||
th.App.Srv().Cloud = &cloud
|
||||
|
||||
_, err := th.SystemAdminClient.ValidateWorkspaceBusinessEmail()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("validate the Cloud Customer has used a invalid email to create the workspace and must validate admin email", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
cloud := mocks.CloudInterface{}
|
||||
|
||||
cloudCustomerInfo := model.CloudCustomerInfo{
|
||||
Email: "invalid@gmail.com",
|
||||
}
|
||||
|
||||
cloudCustomer := &model.CloudCustomer{
|
||||
CloudCustomerInfo: cloudCustomerInfo,
|
||||
}
|
||||
|
||||
cloud.Mock.On("GetCloudCustomer", th.SystemAdminUser.Id).Return(cloudCustomer, nil)
|
||||
|
||||
// first call to validate the cloud customer email
|
||||
cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, cloudCustomerInfo.Email).Return(errors.New("invalid email"))
|
||||
|
||||
// second call to validate the user admin email
|
||||
cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, th.SystemAdminUser.Email).Return(nil)
|
||||
|
||||
cloudImpl := th.App.Srv().Cloud
|
||||
defer func() {
|
||||
th.App.Srv().Cloud = cloudImpl
|
||||
}()
|
||||
th.App.Srv().Cloud = &cloud
|
||||
|
||||
_, err := th.SystemAdminClient.ValidateWorkspaceBusinessEmail()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Error while grabbing the cloud customer returns bad request", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
cloud := mocks.CloudInterface{}
|
||||
|
||||
cloudCustomerInfo := model.CloudCustomerInfo{
|
||||
Email: "badrequest@gmail.com",
|
||||
}
|
||||
|
||||
// return an error while getting the cloud customer so we validate the forbidden error return
|
||||
cloud.Mock.On("GetCloudCustomer", th.SystemAdminUser.Id).Return(nil, errors.New("error while gettings the cloud customer"))
|
||||
|
||||
// required cloud mocks so the request doesn't fail
|
||||
cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, cloudCustomerInfo.Email).Return(errors.New("invalid email"))
|
||||
cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, th.SystemAdminUser.Email).Return(nil)
|
||||
|
||||
cloudImpl := th.App.Srv().Cloud
|
||||
defer func() {
|
||||
th.App.Srv().Cloud = cloudImpl
|
||||
}()
|
||||
th.App.Srv().Cloud = &cloud
|
||||
|
||||
r, err := th.SystemAdminClient.DoAPIPostBytes("/cloud/validate-workspace-business-email", nil)
|
||||
require.Error(t, err)
|
||||
closeBody(r)
|
||||
require.Equal(t, http.StatusBadRequest, r.StatusCode, "Status Bad Request")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetCloudProducts(t *testing.T) {
|
||||
cloudProducts := []*model.Product{
|
||||
{
|
||||
ID: "prod_test1",
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
PricePerSeat: 10,
|
||||
SKU: "sku",
|
||||
PriceID: "price_id",
|
||||
Family: "family",
|
||||
RecurringInterval: "monthly",
|
||||
BillingScheme: "billing_scheme",
|
||||
CrossSellsTo: "",
|
||||
},
|
||||
{
|
||||
ID: "prod_test2",
|
||||
Name: "name2",
|
||||
Description: "description2",
|
||||
PricePerSeat: 100,
|
||||
SKU: "sku2",
|
||||
PriceID: "price_id2",
|
||||
Family: "family2",
|
||||
RecurringInterval: "monthly",
|
||||
BillingScheme: "billing_scheme2",
|
||||
CrossSellsTo: "prod_test3",
|
||||
},
|
||||
{
|
||||
ID: "prod_test3",
|
||||
Name: "name3",
|
||||
Description: "description3",
|
||||
PricePerSeat: 1000,
|
||||
SKU: "sku3",
|
||||
PriceID: "price_id3",
|
||||
Family: "family3",
|
||||
RecurringInterval: "yearly",
|
||||
BillingScheme: "billing_scheme3",
|
||||
CrossSellsTo: "prod_test2",
|
||||
},
|
||||
}
|
||||
|
||||
sanitizedProducts := []*model.Product{
|
||||
{
|
||||
ID: "prod_test1",
|
||||
Name: "name",
|
||||
PricePerSeat: 10,
|
||||
SKU: "sku",
|
||||
RecurringInterval: "monthly",
|
||||
CrossSellsTo: "",
|
||||
},
|
||||
{
|
||||
ID: "prod_test2",
|
||||
Name: "name2",
|
||||
PricePerSeat: 100,
|
||||
SKU: "sku2",
|
||||
RecurringInterval: "monthly",
|
||||
CrossSellsTo: "prod_test3",
|
||||
},
|
||||
{
|
||||
ID: "prod_test3",
|
||||
Name: "name3",
|
||||
PricePerSeat: 1000,
|
||||
SKU: "sku3",
|
||||
RecurringInterval: "yearly",
|
||||
CrossSellsTo: "prod_test2",
|
||||
},
|
||||
}
|
||||
t.Run("get products for admins", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(th.SystemAdminUser.Email, th.SystemAdminUser.Password)
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
cloud := mocks.CloudInterface{}
|
||||
cloud.Mock.On("GetCloudProducts", mock.Anything, mock.Anything).Return(cloudProducts, nil)
|
||||
cloudImpl := th.App.Srv().Cloud
|
||||
defer func() {
|
||||
th.App.Srv().Cloud = cloudImpl
|
||||
}()
|
||||
th.App.Srv().Cloud = &cloud
|
||||
|
||||
returnedProducts, r, err := th.Client.GetCloudProducts()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
|
||||
require.Equal(t, returnedProducts, cloudProducts)
|
||||
})
|
||||
|
||||
t.Run("get products for non admins", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
cloud := mocks.CloudInterface{}
|
||||
|
||||
cloud.Mock.On("GetCloudProducts", mock.Anything, mock.Anything).Return(cloudProducts, nil)
|
||||
|
||||
cloudImpl := th.App.Srv().Cloud
|
||||
defer func() {
|
||||
th.App.Srv().Cloud = cloudImpl
|
||||
}()
|
||||
th.App.Srv().Cloud = &cloud
|
||||
|
||||
returnedProducts, r, err := th.Client.GetCloudProducts()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
|
||||
require.Equal(t, returnedProducts, sanitizedProducts)
|
||||
|
||||
// make a more explicit check
|
||||
require.Equal(t, returnedProducts[0].ID, "prod_test1")
|
||||
require.Equal(t, returnedProducts[0].Name, "name")
|
||||
require.Equal(t, returnedProducts[0].SKU, "sku")
|
||||
require.Equal(t, returnedProducts[0].PricePerSeat, float64(10))
|
||||
require.Equal(t, returnedProducts[0].Description, "")
|
||||
require.Equal(t, returnedProducts[0].PriceID, "")
|
||||
require.Equal(t, returnedProducts[0].Family, model.SubscriptionFamily(""))
|
||||
require.Equal(t, returnedProducts[0].RecurringInterval, model.RecurringInterval("monthly"))
|
||||
require.Equal(t, returnedProducts[0].BillingScheme, model.BillingScheme(""))
|
||||
require.Equal(t, returnedProducts[0].CrossSellsTo, "")
|
||||
|
||||
require.Equal(t, returnedProducts[1].ID, "prod_test2")
|
||||
require.Equal(t, returnedProducts[1].Name, "name2")
|
||||
require.Equal(t, returnedProducts[1].SKU, "sku2")
|
||||
require.Equal(t, returnedProducts[1].PricePerSeat, float64(100))
|
||||
require.Equal(t, returnedProducts[1].Description, "")
|
||||
require.Equal(t, returnedProducts[1].PriceID, "")
|
||||
require.Equal(t, returnedProducts[1].Family, model.SubscriptionFamily(""))
|
||||
require.Equal(t, returnedProducts[1].RecurringInterval, model.RecurringInterval("monthly"))
|
||||
require.Equal(t, returnedProducts[1].BillingScheme, model.BillingScheme(""))
|
||||
require.Equal(t, returnedProducts[1].CrossSellsTo, "prod_test3")
|
||||
|
||||
require.Equal(t, returnedProducts[2].ID, "prod_test3")
|
||||
require.Equal(t, returnedProducts[2].Name, "name3")
|
||||
require.Equal(t, returnedProducts[2].SKU, "sku3")
|
||||
require.Equal(t, returnedProducts[2].PricePerSeat, float64(1000))
|
||||
require.Equal(t, returnedProducts[2].Description, "")
|
||||
require.Equal(t, returnedProducts[2].PriceID, "")
|
||||
require.Equal(t, returnedProducts[2].Family, model.SubscriptionFamily(""))
|
||||
require.Equal(t, returnedProducts[2].RecurringInterval, model.RecurringInterval("yearly"))
|
||||
require.Equal(t, returnedProducts[2].BillingScheme, model.BillingScheme(""))
|
||||
require.Equal(t, returnedProducts[2].CrossSellsTo, "prod_test2")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_GetExpandStatsForSubscription(t *testing.T) {
|
||||
status := &model.SubscriptionLicenseSelfServeStatusResponse{
|
||||
IsExpandable: true,
|
||||
}
|
||||
|
||||
licenseId := "licenseID"
|
||||
|
||||
t.Run("NON Admin users are UNABLE to request expand stats for the subscription", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
cloud := mocks.CloudInterface{}
|
||||
|
||||
cloud.Mock.On("GetLicenseSelfServeStatus", mock.Anything).Return(status, nil)
|
||||
|
||||
cloudImpl := th.App.Srv().Cloud
|
||||
defer func() {
|
||||
th.App.Srv().Cloud = cloudImpl
|
||||
}()
|
||||
th.App.Srv().Cloud = &cloud
|
||||
|
||||
checksMade, r, err := th.Client.GetSubscriptionStatus(licenseId)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, checksMade)
|
||||
require.Equal(t, http.StatusForbidden, r.StatusCode, "403 Forbidden")
|
||||
})
|
||||
|
||||
t.Run("Admin users are UNABLE to request licenses is expendable due missing the id", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(th.SystemAdminUser.Email, th.SystemAdminUser.Password)
|
||||
|
||||
cloud := mocks.CloudInterface{}
|
||||
|
||||
cloud.Mock.On("GetLicenseSelfServeStatus", mock.Anything).Return(status, nil)
|
||||
|
||||
cloudImpl := th.App.Srv().Cloud
|
||||
defer func() {
|
||||
th.App.Srv().Cloud = cloudImpl
|
||||
}()
|
||||
th.App.Srv().Cloud = &cloud
|
||||
|
||||
checks, r, err := th.Client.GetSubscriptionStatus("")
|
||||
require.Error(t, err)
|
||||
require.Nil(t, checks)
|
||||
require.Equal(t, http.StatusBadRequest, r.StatusCode, "400 Bad Request")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetSelfHostedProducts(t *testing.T) {
|
||||
products := []*model.Product{
|
||||
{
|
||||
ID: "prod_test",
|
||||
Name: "Self-Hosted Professional",
|
||||
Description: "Ideal for small companies and departments with data security requirements",
|
||||
PricePerSeat: 10,
|
||||
SKU: "professional",
|
||||
PriceID: "price_1JPXbNI67GP2qpb4VuFdFbwQ",
|
||||
Family: "on-prem",
|
||||
RecurringInterval: model.RecurringIntervalYearly,
|
||||
},
|
||||
{
|
||||
ID: "prod_test2",
|
||||
Name: "Self-Hosted Enterprise",
|
||||
Description: "Built to scale for high-trust organizations and companies in regulated industries.",
|
||||
PricePerSeat: 30,
|
||||
SKU: "enterprise",
|
||||
PriceID: "price_1JPXaVI67GP2qpb4l40bXyRu",
|
||||
Family: "on-prem",
|
||||
RecurringInterval: model.RecurringIntervalYearly,
|
||||
},
|
||||
}
|
||||
|
||||
sanitizedProducts := []*model.Product{
|
||||
{
|
||||
ID: "prod_test",
|
||||
Name: "Self-Hosted Professional",
|
||||
PricePerSeat: 10,
|
||||
SKU: "professional",
|
||||
RecurringInterval: model.RecurringIntervalYearly,
|
||||
},
|
||||
{
|
||||
ID: "prod_test2",
|
||||
Name: "Self-Hosted Enterprise",
|
||||
PricePerSeat: 30,
|
||||
SKU: "enterprise",
|
||||
RecurringInterval: model.RecurringIntervalYearly,
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("get products for admins", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(th.SystemAdminUser.Email, th.SystemAdminUser.Password)
|
||||
|
||||
cloud := mocks.CloudInterface{}
|
||||
cloud.Mock.On("GetSelfHostedProducts", mock.Anything, mock.Anything).Return(products, nil)
|
||||
cloudImpl := th.App.Srv().Cloud
|
||||
defer func() {
|
||||
th.App.Srv().Cloud = cloudImpl
|
||||
}()
|
||||
th.App.Srv().Cloud = &cloud
|
||||
|
||||
returnedProducts, r, err := th.Client.GetSelfHostedProducts()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
|
||||
require.Equal(t, returnedProducts, products)
|
||||
})
|
||||
|
||||
t.Run("get products for non admins", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
cloud := mocks.CloudInterface{}
|
||||
|
||||
cloud.Mock.On("GetSelfHostedProducts", mock.Anything, mock.Anything).Return(products, nil)
|
||||
|
||||
cloudImpl := th.App.Srv().Cloud
|
||||
defer func() {
|
||||
th.App.Srv().Cloud = cloudImpl
|
||||
}()
|
||||
th.App.Srv().Cloud = &cloud
|
||||
|
||||
returnedProducts, r, err := th.Client.GetSelfHostedProducts()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
|
||||
require.Equal(t, returnedProducts, sanitizedProducts)
|
||||
|
||||
// make a more explicit check
|
||||
require.Equal(t, returnedProducts[0].ID, "prod_test")
|
||||
require.Equal(t, returnedProducts[0].Name, "Self-Hosted Professional")
|
||||
require.Equal(t, returnedProducts[0].SKU, "professional")
|
||||
require.Equal(t, returnedProducts[0].PricePerSeat, float64(10))
|
||||
require.Equal(t, returnedProducts[0].Description, "")
|
||||
require.Equal(t, returnedProducts[0].PriceID, "")
|
||||
require.Equal(t, returnedProducts[0].Family, model.SubscriptionFamily(""))
|
||||
require.Equal(t, returnedProducts[0].RecurringInterval, model.RecurringInterval("year"))
|
||||
require.Equal(t, returnedProducts[0].BillingScheme, model.BillingScheme(""))
|
||||
require.Equal(t, returnedProducts[0].CrossSellsTo, "")
|
||||
|
||||
require.Equal(t, returnedProducts[1].ID, "prod_test2")
|
||||
require.Equal(t, returnedProducts[1].Name, "Self-Hosted Enterprise")
|
||||
require.Equal(t, returnedProducts[1].SKU, "enterprise")
|
||||
require.Equal(t, returnedProducts[1].PricePerSeat, float64(30))
|
||||
require.Equal(t, returnedProducts[1].Description, "")
|
||||
require.Equal(t, returnedProducts[1].PriceID, "")
|
||||
require.Equal(t, returnedProducts[1].Family, model.SubscriptionFamily(""))
|
||||
require.Equal(t, returnedProducts[1].RecurringInterval, model.RecurringInterval("year"))
|
||||
require.Equal(t, returnedProducts[1].BillingScheme, model.BillingScheme(""))
|
||||
require.Equal(t, returnedProducts[1].CrossSellsTo, "")
|
||||
})
|
||||
}
|
||||
488
api4/command.go
488
api4/command.go
|
|
@ -1,488 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitCommand() {
|
||||
api.BaseRoutes.Commands.Handle("", api.APISessionRequired(createCommand)).Methods("POST")
|
||||
api.BaseRoutes.Commands.Handle("", api.APISessionRequired(listCommands)).Methods("GET")
|
||||
api.BaseRoutes.Commands.Handle("/execute", api.APISessionRequired(executeCommand)).Methods("POST")
|
||||
|
||||
api.BaseRoutes.Command.Handle("", api.APISessionRequired(getCommand)).Methods("GET")
|
||||
api.BaseRoutes.Command.Handle("", api.APISessionRequired(updateCommand)).Methods("PUT")
|
||||
api.BaseRoutes.Command.Handle("/move", api.APISessionRequired(moveCommand)).Methods("PUT")
|
||||
api.BaseRoutes.Command.Handle("", api.APISessionRequired(deleteCommand)).Methods("DELETE")
|
||||
|
||||
api.BaseRoutes.Team.Handle("/commands/autocomplete", api.APISessionRequired(listAutocompleteCommands)).Methods("GET")
|
||||
api.BaseRoutes.Team.Handle("/commands/autocomplete_suggestions", api.APISessionRequired(listCommandAutocompleteSuggestions)).Methods("GET")
|
||||
api.BaseRoutes.Command.Handle("/regen_token", api.APISessionRequired(regenCommandToken)).Methods("PUT")
|
||||
}
|
||||
|
||||
func createCommand(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var cmd model.Command
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&cmd); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("command", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("createCommand", audit.Fail)
|
||||
audit.AddEventParameterAuditable(auditRec, "command", &cmd)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) {
|
||||
c.SetPermissionError(model.PermissionManageSlashCommands)
|
||||
return
|
||||
}
|
||||
|
||||
cmd.CreatorId = c.AppContext.Session().UserId
|
||||
|
||||
rcmd, err := c.App.CreateCommand(&cmd)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
c.LogAudit("success")
|
||||
auditRec.AddEventResultState(rcmd)
|
||||
auditRec.AddEventObjectType("command")
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(rcmd); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func updateCommand(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireCommandId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var cmd model.Command
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&cmd); jsonErr != nil || cmd.Id != c.Params.CommandId {
|
||||
c.SetInvalidParamWithErr("command", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("updateCommand", audit.Fail)
|
||||
audit.AddEventParameterAuditable(auditRec, "command", &cmd)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
oldCmd, err := c.App.GetCommand(c.Params.CommandId)
|
||||
if err != nil {
|
||||
audit.AddEventParameter(auditRec, "command_id", c.Params.CommandId)
|
||||
c.SetCommandNotFoundError()
|
||||
return
|
||||
}
|
||||
auditRec.AddEventPriorState(oldCmd)
|
||||
|
||||
if cmd.TeamId != oldCmd.TeamId {
|
||||
c.Err = model.NewAppError("updateCommand", "api.command.team_mismatch.app_error", nil, "user_id="+c.AppContext.Session().UserId, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), oldCmd.TeamId, model.PermissionManageSlashCommands) {
|
||||
c.LogAudit("fail - inappropriate permissions")
|
||||
// here we return Not_found instead of a permissions error so we don't leak the existence of
|
||||
// a command to someone without permissions for the team it belongs to.
|
||||
c.SetCommandNotFoundError()
|
||||
return
|
||||
}
|
||||
|
||||
if c.AppContext.Session().UserId != oldCmd.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), oldCmd.TeamId, model.PermissionManageOthersSlashCommands) {
|
||||
c.LogAudit("fail - inappropriate permissions")
|
||||
c.SetPermissionError(model.PermissionManageOthersSlashCommands)
|
||||
return
|
||||
}
|
||||
|
||||
rcmd, err := c.App.UpdateCommand(oldCmd, &cmd)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventResultState(rcmd)
|
||||
auditRec.AddEventObjectType("command")
|
||||
auditRec.Success()
|
||||
c.LogAudit("success")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(rcmd); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func moveCommand(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireCommandId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var cmr model.CommandMoveRequest
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&cmr); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("team_id", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("moveCommand", audit.Fail)
|
||||
audit.AddEventParameter(auditRec, "command_move_request", cmr.TeamId)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
newTeam, appErr := c.App.GetTeam(cmr.TeamId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
audit.AddEventParameterAuditable(auditRec, "team", newTeam)
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), newTeam.Id, model.PermissionManageSlashCommands) {
|
||||
c.LogAudit("fail - inappropriate permissions")
|
||||
c.SetPermissionError(model.PermissionManageSlashCommands)
|
||||
return
|
||||
}
|
||||
|
||||
cmd, appErr := c.App.GetCommand(c.Params.CommandId)
|
||||
if appErr != nil {
|
||||
c.SetCommandNotFoundError()
|
||||
return
|
||||
}
|
||||
auditRec.AddEventPriorState(cmd)
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) {
|
||||
c.LogAudit("fail - inappropriate permissions")
|
||||
// here we return Not_found instead of a permissions error so we don't leak the existence of
|
||||
// a command to someone without permissions for the team it belongs to.
|
||||
c.SetCommandNotFoundError()
|
||||
return
|
||||
}
|
||||
|
||||
if appErr = c.App.MoveCommand(newTeam, cmd); appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventResultState(cmd)
|
||||
auditRec.AddEventObjectType("command")
|
||||
auditRec.Success()
|
||||
c.LogAudit("success")
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func deleteCommand(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireCommandId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("deleteCommand", audit.Fail)
|
||||
audit.AddEventParameter(auditRec, "command_id", c.Params.CommandId)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
cmd, err := c.App.GetCommand(c.Params.CommandId)
|
||||
if err != nil {
|
||||
c.SetCommandNotFoundError()
|
||||
return
|
||||
}
|
||||
auditRec.AddEventPriorState(cmd)
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) {
|
||||
c.LogAudit("fail - inappropriate permissions")
|
||||
// here we return Not_found instead of a permissions error so we don't leak the existence of
|
||||
// a command to someone without permissions for the team it belongs to.
|
||||
c.SetCommandNotFoundError()
|
||||
return
|
||||
}
|
||||
|
||||
if c.AppContext.Session().UserId != cmd.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageOthersSlashCommands) {
|
||||
c.LogAudit("fail - inappropriate permissions")
|
||||
c.SetPermissionError(model.PermissionManageOthersSlashCommands)
|
||||
return
|
||||
}
|
||||
|
||||
err = c.App.DeleteCommand(cmd.Id)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventObjectType("command")
|
||||
auditRec.Success()
|
||||
c.LogAudit("success")
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func listCommands(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
customOnly, _ := strconv.ParseBool(r.URL.Query().Get("custom_only"))
|
||||
|
||||
teamId := r.URL.Query().Get("team_id")
|
||||
if teamId == "" {
|
||||
c.SetInvalidParam("team_id")
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionViewTeam) {
|
||||
c.SetPermissionError(model.PermissionViewTeam)
|
||||
return
|
||||
}
|
||||
|
||||
var commands []*model.Command
|
||||
var err *model.AppError
|
||||
if customOnly {
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageSlashCommands) {
|
||||
c.SetPermissionError(model.PermissionManageSlashCommands)
|
||||
return
|
||||
}
|
||||
commands, err = c.App.ListTeamCommands(teamId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
} else {
|
||||
//User with no permission should see only system commands
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageSlashCommands) {
|
||||
commands, err = c.App.ListAutocompleteCommands(teamId, c.AppContext.T)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
} else {
|
||||
commands, err = c.App.ListAllCommands(teamId, c.AppContext.T)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(commands); err != nil {
|
||||
c.Logger.Warn("Error writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getCommand(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireCommandId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cmd, err := c.App.GetCommand(c.Params.CommandId)
|
||||
if err != nil {
|
||||
c.SetCommandNotFoundError()
|
||||
return
|
||||
}
|
||||
|
||||
// check for permissions to view this command; must have perms to view team and
|
||||
// PERMISSION_MANAGE_SLASH_COMMANDS for the team the command belongs to.
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionViewTeam) {
|
||||
// here we return Not_found instead of a permissions error so we don't leak the existence of
|
||||
// a command to someone without permissions for the team it belongs to.
|
||||
c.SetCommandNotFoundError()
|
||||
return
|
||||
}
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) {
|
||||
// again, return not_found to ensure id existence does not leak.
|
||||
c.SetCommandNotFoundError()
|
||||
return
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(cmd); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var commandArgs model.CommandArgs
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&commandArgs); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("command_args", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
if len(commandArgs.Command) <= 1 || strings.Index(commandArgs.Command, "/") != 0 || !model.IsValidId(commandArgs.ChannelId) {
|
||||
c.Err = model.NewAppError("executeCommand", "api.command.execute_command.start.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("executeCommand", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameterAuditable(auditRec, "command_args", &commandArgs)
|
||||
|
||||
// checks that user is a member of the specified channel, and that they have permission to use slash commands in it
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), commandArgs.ChannelId, model.PermissionUseSlashCommands) {
|
||||
c.SetPermissionError(model.PermissionUseSlashCommands)
|
||||
return
|
||||
}
|
||||
|
||||
channel, err := c.App.GetChannel(c.AppContext, commandArgs.ChannelId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if channel.Type != model.ChannelTypeDirect && channel.Type != model.ChannelTypeGroup {
|
||||
// if this isn't a DM or GM, the team id is implicitly taken from the channel so that slash commands created on
|
||||
// some other team can't be run against this one
|
||||
commandArgs.TeamId = channel.TeamId
|
||||
} else {
|
||||
// if the slash command was used in a DM or GM, ensure that the user is a member of the specified team, so that
|
||||
// they can't just execute slash commands against arbitrary teams
|
||||
if c.AppContext.Session().GetTeamByTeamId(commandArgs.TeamId) == nil {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionUseSlashCommands) {
|
||||
c.SetPermissionError(model.PermissionUseSlashCommands)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commandArgs.UserId = c.AppContext.Session().UserId
|
||||
commandArgs.T = c.AppContext.T
|
||||
commandArgs.SiteURL = c.GetSiteURLHeader()
|
||||
commandArgs.Session = *c.AppContext.Session()
|
||||
|
||||
response, err := c.App.ExecuteCommand(c.AppContext, &commandArgs)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func listAutocompleteCommands(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireTeamId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
|
||||
c.SetPermissionError(model.PermissionViewTeam)
|
||||
return
|
||||
}
|
||||
|
||||
commands, err := c.App.ListAutocompleteCommands(c.Params.TeamId, c.AppContext.T)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(commands); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func listCommandAutocompleteSuggestions(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireTeamId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
|
||||
c.SetPermissionError(model.PermissionViewTeam)
|
||||
return
|
||||
}
|
||||
|
||||
roleId := model.SystemUserRoleId
|
||||
if c.IsSystemAdmin() {
|
||||
roleId = model.SystemAdminRoleId
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
userInput := query.Get("user_input")
|
||||
if userInput == "" {
|
||||
c.SetInvalidParam("userInput")
|
||||
return
|
||||
}
|
||||
userInput = strings.TrimPrefix(userInput, "/")
|
||||
|
||||
commands, appErr := c.App.ListAutocompleteCommands(c.Params.TeamId, c.AppContext.T)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
commandArgs := &model.CommandArgs{
|
||||
ChannelId: query.Get("channel_id"),
|
||||
TeamId: c.Params.TeamId,
|
||||
RootId: query.Get("root_id"),
|
||||
UserId: c.AppContext.Session().UserId,
|
||||
T: c.AppContext.T,
|
||||
Session: *c.AppContext.Session(),
|
||||
SiteURL: c.GetSiteURLHeader(),
|
||||
Command: userInput,
|
||||
}
|
||||
|
||||
suggestions := c.App.GetSuggestions(c.AppContext, commandArgs, commands, roleId)
|
||||
|
||||
js, err := json.Marshal(suggestions)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("listCommandAutocompleteSuggestions", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func regenCommandToken(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireCommandId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("regenCommandToken", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
cmd, err := c.App.GetCommand(c.Params.CommandId)
|
||||
if err != nil {
|
||||
audit.AddEventParameter(auditRec, "command_id", c.Params.CommandId)
|
||||
c.SetCommandNotFoundError()
|
||||
return
|
||||
}
|
||||
auditRec.AddEventPriorState(cmd)
|
||||
audit.AddEventParameter(auditRec, "command_id", c.Params.CommandId)
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) {
|
||||
c.LogAudit("fail - inappropriate permissions")
|
||||
// here we return Not_found instead of a permissions error so we don't leak the existence of
|
||||
// a command to someone without permissions for the team it belongs to.
|
||||
c.SetCommandNotFoundError()
|
||||
return
|
||||
}
|
||||
|
||||
if c.AppContext.Session().UserId != cmd.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageOthersSlashCommands) {
|
||||
c.LogAudit("fail - inappropriate permissions")
|
||||
c.SetPermissionError(model.PermissionManageOthersSlashCommands)
|
||||
return
|
||||
}
|
||||
|
||||
rcmd, err := c.App.RegenCommandToken(cmd)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
auditRec.AddEventResultState(rcmd)
|
||||
auditRec.Success()
|
||||
c.LogAudit("success")
|
||||
|
||||
resp := make(map[string]string)
|
||||
resp["token"] = rcmd.Token
|
||||
|
||||
w.Write([]byte(model.MapToJSON(resp)))
|
||||
}
|
||||
1067
api4/command_test.go
1067
api4/command_test.go
File diff suppressed because it is too large
Load diff
|
|
@ -1,162 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/avct/uasurfer"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitCompliance() {
|
||||
api.BaseRoutes.Compliance.Handle("/reports", api.APISessionRequired(createComplianceReport)).Methods("POST")
|
||||
api.BaseRoutes.Compliance.Handle("/reports", api.APISessionRequired(getComplianceReports)).Methods("GET")
|
||||
api.BaseRoutes.Compliance.Handle("/reports/{report_id:[A-Za-z0-9]+}", api.APISessionRequired(getComplianceReport)).Methods("GET")
|
||||
api.BaseRoutes.Compliance.Handle("/reports/{report_id:[A-Za-z0-9]+}/download", api.APISessionRequiredTrustRequester(downloadComplianceReport)).Methods("GET")
|
||||
}
|
||||
|
||||
func createComplianceReport(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var job model.Compliance
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&job); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("compliance", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("createComplianceReport", audit.Fail)
|
||||
audit.AddEventParameterAuditable(auditRec, "compliance", &job)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateComplianceExportJob) {
|
||||
c.SetPermissionError(model.PermissionCreateComplianceExportJob)
|
||||
return
|
||||
}
|
||||
|
||||
job.UserId = c.AppContext.Session().UserId
|
||||
|
||||
rjob, err := c.App.SaveComplianceReport(&job)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(rjob)
|
||||
auditRec.AddEventObjectType("compliance")
|
||||
auditRec.AddMeta("compliance_id", rjob.Id)
|
||||
auditRec.AddMeta("compliance_desc", rjob.Desc)
|
||||
c.LogAudit("")
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(rjob); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getComplianceReports(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadComplianceExportJob) {
|
||||
c.SetPermissionError(model.PermissionReadComplianceExportJob)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("getComplianceReports", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
crs, err := c.App.GetComplianceReports(c.Params.Page, c.Params.PerPage)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
if err := json.NewEncoder(w).Encode(crs); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getComplianceReport(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireReportId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("getComplianceReport", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadComplianceExportJob) {
|
||||
c.SetPermissionError(model.PermissionReadComplianceExportJob)
|
||||
return
|
||||
}
|
||||
|
||||
audit.AddEventParameter(auditRec, "report_id", c.Params.ReportId)
|
||||
job, err := c.App.GetComplianceReport(c.Params.ReportId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddMeta("compliance_id", job.Id)
|
||||
auditRec.AddMeta("compliance_desc", job.Desc)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(job); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func downloadComplianceReport(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireReportId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("downloadComplianceReport", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "compliance_id", c.Params.ReportId)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionDownloadComplianceExportResult) {
|
||||
c.SetPermissionError(model.PermissionDownloadComplianceExportResult)
|
||||
return
|
||||
}
|
||||
|
||||
job, err := c.App.GetComplianceReport(c.Params.ReportId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
auditRec.AddEventResultState(job)
|
||||
auditRec.AddEventObjectType("compliance")
|
||||
|
||||
reportBytes, err := c.App.GetComplianceFile(job)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
auditRec.AddMeta("length", len(reportBytes))
|
||||
|
||||
c.LogAudit("downloaded " + job.Desc)
|
||||
|
||||
w.Header().Set("Cache-Control", "max-age=2592000, private")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(reportBytes)))
|
||||
w.Header().Del("Content-Type") // Content-Type will be set automatically by the http writer
|
||||
|
||||
// attach extra headers to trigger a download on IE, Edge, and Safari
|
||||
ua := uasurfer.Parse(r.UserAgent())
|
||||
|
||||
w.Header().Set("Content-Disposition", "attachment;filename=\""+job.JobName()+".zip\"")
|
||||
|
||||
if ua.Browser.Name == uasurfer.BrowserIE || ua.Browser.Name == uasurfer.BrowserSafari {
|
||||
// trim off anything before the final / so we just get the file's name
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
w.Write(reportBytes)
|
||||
}
|
||||
443
api4/config.go
443
api4/config.go
|
|
@ -1,443 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/config"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/i18n"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/utils"
|
||||
)
|
||||
|
||||
var writeFilter func(c *Context, structField reflect.StructField) bool
|
||||
var readFilter func(c *Context, structField reflect.StructField) bool
|
||||
var permissionMap map[string]*model.Permission
|
||||
|
||||
type filterType string
|
||||
|
||||
const (
|
||||
FilterTypeWrite filterType = "write"
|
||||
FilterTypeRead filterType = "read"
|
||||
)
|
||||
|
||||
func (api *API) InitConfig() {
|
||||
api.BaseRoutes.APIRoot.Handle("/config", api.APISessionRequired(getConfig)).Methods("GET")
|
||||
api.BaseRoutes.APIRoot.Handle("/config", api.APISessionRequired(updateConfig)).Methods("PUT")
|
||||
api.BaseRoutes.APIRoot.Handle("/config/patch", api.APISessionRequired(patchConfig)).Methods("PUT")
|
||||
api.BaseRoutes.APIRoot.Handle("/config/reload", api.APISessionRequired(configReload)).Methods("POST")
|
||||
api.BaseRoutes.APIRoot.Handle("/config/client", api.APIHandler(getClientConfig)).Methods("GET")
|
||||
api.BaseRoutes.APIRoot.Handle("/config/environment", api.APISessionRequired(getEnvironmentConfig)).Methods("GET")
|
||||
}
|
||||
|
||||
func init() {
|
||||
writeFilter = makeFilterConfigByPermission(FilterTypeWrite)
|
||||
readFilter = makeFilterConfigByPermission(FilterTypeRead)
|
||||
permissionMap = map[string]*model.Permission{}
|
||||
for _, p := range model.AllPermissions {
|
||||
permissionMap[p.Id] = p
|
||||
}
|
||||
}
|
||||
|
||||
func getConfig(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleReadPermissions) {
|
||||
c.SetPermissionError(model.SysconsoleReadPermissions...)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("getConfig", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
cfg, err := config.Merge(&model.Config{}, c.App.GetSanitizedConfig(), &utils.MergeConfig{
|
||||
StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
|
||||
return readFilter(c, structField)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getConfig", "api.config.get_config.restricted_merge.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
if c.App.Channels().License().IsCloud() {
|
||||
js, jsonErr := cfg.ToJSONFiltered(model.ConfigAccessTagType, model.ConfigAccessTagCloudRestrictable)
|
||||
if jsonErr != nil {
|
||||
c.Err = model.NewAppError("getConfig", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Write(js)
|
||||
return
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(cfg); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func configReload(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
auditRec := c.MakeAuditRecord("configReload", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReloadConfig) {
|
||||
c.SetPermissionError(model.PermissionReloadConfig)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.AppContext.Session().IsUnrestricted() && *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
|
||||
c.Err = model.NewAppError("configReload", "api.restricted_system_admin", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.ReloadConfig(); err != nil {
|
||||
c.Err = model.NewAppError("configReload", "api.config.reload_config.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func updateConfig(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var cfg *model.Config
|
||||
err := json.NewDecoder(r.Body).Decode(&cfg)
|
||||
if err != nil || cfg == nil {
|
||||
c.SetInvalidParamWithErr("config", err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("updateConfig", audit.Fail)
|
||||
|
||||
// audit.AddEventParameter(auditRec, "config", cfg) // TODO We can do this but do we want to?
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
cfg.SetDefaults()
|
||||
|
||||
if !c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleWritePermissions) {
|
||||
c.SetPermissionError(model.SysconsoleWritePermissions...)
|
||||
return
|
||||
}
|
||||
|
||||
appCfg := c.App.Config()
|
||||
if *appCfg.ServiceSettings.SiteURL != "" && *cfg.ServiceSettings.SiteURL == "" {
|
||||
c.Err = model.NewAppError("updateConfig", "api.config.update_config.clear_siteurl.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err = config.Merge(appCfg, cfg, &utils.MergeConfig{
|
||||
StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
|
||||
return writeFilter(c, structField)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("updateConfig", "api.config.update_config.restricted_merge.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Do not allow plugin uploads to be toggled through the API
|
||||
*cfg.PluginSettings.EnableUploads = *appCfg.PluginSettings.EnableUploads
|
||||
|
||||
// Do not allow certificates to be changed through the API
|
||||
// This shallow-copies the slice header. So be careful if there are concurrent
|
||||
// modifications to the slice.
|
||||
cfg.PluginSettings.SignaturePublicKeyFiles = appCfg.PluginSettings.SignaturePublicKeyFiles
|
||||
|
||||
// Do not allow marketplace URL to be toggled through the API if EnableUploads are disabled.
|
||||
if cfg.PluginSettings.EnableUploads != nil && !*appCfg.PluginSettings.EnableUploads {
|
||||
*cfg.PluginSettings.MarketplaceURL = *appCfg.PluginSettings.MarketplaceURL
|
||||
}
|
||||
|
||||
if cfg.PluginSettings.PluginStates[model.PluginIdFocalboard].Enable && cfg.FeatureFlags.BoardsProduct {
|
||||
c.Err = model.NewAppError("EnablePlugin", "app.plugin.product_mode.app_error", map[string]any{"Name": model.PluginIdFocalboard}, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// There are some settings that cannot be changed in a cloud env
|
||||
if c.App.Channels().License().IsCloud() {
|
||||
// Both of them cannot be nil since cfg.SetDefaults is called earlier for cfg,
|
||||
// and appCfg is the existing earlier config and if it's nil, server sets a default value.
|
||||
if *appCfg.ComplianceSettings.Directory != *cfg.ComplianceSettings.Directory {
|
||||
c.Err = model.NewAppError("updateConfig", "api.config.update_config.not_allowed_security.app_error", map[string]any{"Name": "ComplianceSettings.Directory"}, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.App.HandleMessageExportConfig(cfg, appCfg)
|
||||
|
||||
if appErr := cfg.IsValid(); appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
oldCfg, newCfg, appErr := c.App.SaveConfig(cfg, true)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
// If the config for default server locale has changed, reinitialize the server's translations.
|
||||
if oldCfg.LocalizationSettings.DefaultServerLocale != newCfg.LocalizationSettings.DefaultServerLocale {
|
||||
s := newCfg.LocalizationSettings
|
||||
if err = i18n.InitTranslations(*s.DefaultServerLocale, *s.DefaultClientLocale); err != nil {
|
||||
c.Err = model.NewAppError("updateConfig", "api.config.update_config.translations.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
diffs, err := config.Diff(oldCfg, newCfg)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("updateConfig", "api.config.update_config.diff.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
auditRec.AddEventPriorState(&diffs)
|
||||
|
||||
newCfg.Sanitize()
|
||||
|
||||
cfg, err = config.Merge(&model.Config{}, newCfg, &utils.MergeConfig{
|
||||
StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
|
||||
return readFilter(c, structField)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("updateConfig", "api.config.update_config.restricted_merge.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
//auditRec.AddEventResultState(cfg) // TODO we can do this too but do we want to? the config object is huge
|
||||
auditRec.AddEventObjectType("config")
|
||||
auditRec.Success()
|
||||
c.LogAudit("updateConfig")
|
||||
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
if c.App.Channels().License().IsCloud() {
|
||||
js, err := cfg.ToJSONFiltered(model.ConfigAccessTagType, model.ConfigAccessTagCloudRestrictable)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("updateConfig", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
w.Write(js)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(cfg); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getClientConfig(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
format := r.URL.Query().Get("format")
|
||||
|
||||
if format == "" {
|
||||
c.Err = model.NewAppError("getClientConfig", "api.config.client.old_format.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
if format != "old" {
|
||||
c.SetInvalidParam("format")
|
||||
return
|
||||
}
|
||||
|
||||
var config map[string]string
|
||||
if c.AppContext.Session().UserId == "" {
|
||||
config = c.App.Srv().Platform().LimitedClientConfigWithComputed()
|
||||
} else {
|
||||
config = c.App.Srv().Platform().ClientConfigWithComputed()
|
||||
}
|
||||
|
||||
w.Write([]byte(model.MapToJSON(config)))
|
||||
}
|
||||
|
||||
func getEnvironmentConfig(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// Only return the environment variables for the subsections which the client is
|
||||
// allowed to see
|
||||
envConfig := c.App.GetEnvironmentConfig(func(structField reflect.StructField) bool {
|
||||
return readFilter(c, structField)
|
||||
})
|
||||
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Write([]byte(model.StringInterfaceToJSON(envConfig)))
|
||||
}
|
||||
|
||||
func patchConfig(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var cfg *model.Config
|
||||
err := json.NewDecoder(r.Body).Decode(&cfg)
|
||||
if err != nil || cfg == nil {
|
||||
c.SetInvalidParamWithErr("config", err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("patchConfig", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if !c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleWritePermissions) {
|
||||
c.SetPermissionError(model.SysconsoleWritePermissions...)
|
||||
return
|
||||
}
|
||||
|
||||
appCfg := c.App.Config()
|
||||
if *appCfg.ServiceSettings.SiteURL != "" && cfg.ServiceSettings.SiteURL != nil && *cfg.ServiceSettings.SiteURL == "" {
|
||||
c.Err = model.NewAppError("patchConfig", "api.config.update_config.clear_siteurl.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
filterFn := func(structField reflect.StructField, base, patch reflect.Value) bool {
|
||||
return writeFilter(c, structField)
|
||||
}
|
||||
|
||||
// Do not allow plugin uploads to be toggled through the API
|
||||
if cfg.PluginSettings.EnableUploads != nil && *cfg.PluginSettings.EnableUploads != *appCfg.PluginSettings.EnableUploads {
|
||||
c.Err = model.NewAppError("patchConfig", "api.config.update_config.not_allowed_security.app_error", map[string]any{"Name": "PluginSettings.EnableUploads"}, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Do not allow marketplace URL to be toggled if plugin uploads are disabled.
|
||||
if cfg.PluginSettings.MarketplaceURL != nil && cfg.PluginSettings.EnableUploads != nil {
|
||||
// Breaking it down to 2 conditions to make it simple.
|
||||
if *cfg.PluginSettings.MarketplaceURL != *appCfg.PluginSettings.MarketplaceURL && !*cfg.PluginSettings.EnableUploads {
|
||||
c.Err = model.NewAppError("patchConfig", "api.config.update_config.not_allowed_security.app_error", map[string]any{"Name": "PluginSettings.MarketplaceURL"}, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// There are some settings that cannot be changed in a cloud env
|
||||
if c.App.Channels().License().IsCloud() {
|
||||
if cfg.ComplianceSettings.Directory != nil && *appCfg.ComplianceSettings.Directory != *cfg.ComplianceSettings.Directory {
|
||||
c.Err = model.NewAppError("patchConfig", "api.config.update_config.not_allowed_security.app_error", map[string]any{"Name": "ComplianceSettings.Directory"}, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.MessageExportSettings.EnableExport != nil {
|
||||
c.App.HandleMessageExportConfig(cfg, appCfg)
|
||||
}
|
||||
|
||||
updatedCfg, err := config.Merge(appCfg, cfg, &utils.MergeConfig{
|
||||
StructFieldFilter: filterFn,
|
||||
})
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("patchConfig", "api.config.update_config.restricted_merge.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
appErr := updatedCfg.IsValid()
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
oldCfg, newCfg, appErr := c.App.SaveConfig(updatedCfg, true)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
diffs, err := config.Diff(oldCfg, newCfg)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("patchConfig", "api.config.patch_config.diff.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventPriorState(&diffs)
|
||||
|
||||
newCfg.Sanitize()
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
cfg, err = config.Merge(&model.Config{}, newCfg, &utils.MergeConfig{
|
||||
StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
|
||||
return readFilter(c, structField)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("patchConfig", "api.config.patch_config.restricted_merge.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
if c.App.Channels().License().IsCloud() {
|
||||
js, err := cfg.ToJSONFiltered(model.ConfigAccessTagType, model.ConfigAccessTagCloudRestrictable)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("patchConfig", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
w.Write(js)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(cfg); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func makeFilterConfigByPermission(accessType filterType) func(c *Context, structField reflect.StructField) bool {
|
||||
return func(c *Context, structField reflect.StructField) bool {
|
||||
if structField.Type.Kind() == reflect.Struct {
|
||||
return true
|
||||
}
|
||||
|
||||
tagPermissions := strings.Split(structField.Tag.Get("access"), ",")
|
||||
|
||||
// If there are no access tag values and the role has manage_system, no need to continue
|
||||
// checking permissions.
|
||||
if len(tagPermissions) == 0 {
|
||||
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// one iteration for write_restrictable value, it could be anywhere in the order of values
|
||||
for _, val := range tagPermissions {
|
||||
tagValue := strings.TrimSpace(val)
|
||||
if tagValue == "" {
|
||||
continue
|
||||
}
|
||||
// ConfigAccessTagWriteRestrictable trumps all other permissions
|
||||
if tagValue == model.ConfigAccessTagWriteRestrictable || tagValue == model.ConfigAccessTagCloudRestrictable {
|
||||
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin && accessType == FilterTypeWrite {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// another iteration for permissions checks of other tag values
|
||||
for _, val := range tagPermissions {
|
||||
tagValue := strings.TrimSpace(val)
|
||||
if tagValue == "" {
|
||||
continue
|
||||
}
|
||||
if tagValue == model.ConfigAccessTagWriteRestrictable {
|
||||
continue
|
||||
}
|
||||
if tagValue == model.ConfigAccessTagCloudRestrictable {
|
||||
continue
|
||||
}
|
||||
if tagValue == model.ConfigAccessTagAnySysConsoleRead && accessType == FilterTypeRead &&
|
||||
c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleReadPermissions) {
|
||||
return true
|
||||
}
|
||||
|
||||
permissionID := fmt.Sprintf("sysconsole_%s_%s", accessType, tagValue)
|
||||
if permission, ok := permissionMap[permissionID]; ok {
|
||||
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), permission) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
mlog.Warn("Unrecognized config permissions tag value.", mlog.String("tag_value", permissionID))
|
||||
}
|
||||
}
|
||||
|
||||
// with manage_system, default to allow, otherwise default not-allow
|
||||
return c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,861 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app"
|
||||
"github.com/mattermost/mattermost-server/v6/config"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
func TestGetConfig(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
client := th.Client
|
||||
|
||||
_, resp, err := client.GetConfig()
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||
cfg, _, err := client.GetConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotEqual(t, "", cfg.TeamSettings.SiteName)
|
||||
|
||||
if *cfg.LdapSettings.BindPassword != model.FakeSetting && *cfg.LdapSettings.BindPassword != "" {
|
||||
require.FailNow(t, "did not sanitize properly")
|
||||
}
|
||||
require.Equal(t, model.FakeSetting, *cfg.FileSettings.PublicLinkSalt, "did not sanitize properly")
|
||||
|
||||
if *cfg.FileSettings.AmazonS3SecretAccessKey != model.FakeSetting && *cfg.FileSettings.AmazonS3SecretAccessKey != "" {
|
||||
require.FailNow(t, "did not sanitize properly")
|
||||
}
|
||||
if *cfg.EmailSettings.SMTPPassword != model.FakeSetting && *cfg.EmailSettings.SMTPPassword != "" {
|
||||
require.FailNow(t, "did not sanitize properly")
|
||||
}
|
||||
if *cfg.GitLabSettings.Secret != model.FakeSetting && *cfg.GitLabSettings.Secret != "" {
|
||||
require.FailNow(t, "did not sanitize properly")
|
||||
}
|
||||
require.Equal(t, model.FakeSetting, *cfg.SqlSettings.DataSource, "did not sanitize properly")
|
||||
require.Equal(t, model.FakeSetting, *cfg.SqlSettings.AtRestEncryptKey, "did not sanitize properly")
|
||||
if !strings.Contains(strings.Join(cfg.SqlSettings.DataSourceReplicas, " "), model.FakeSetting) && len(cfg.SqlSettings.DataSourceReplicas) != 0 {
|
||||
require.FailNow(t, "did not sanitize properly")
|
||||
}
|
||||
if !strings.Contains(strings.Join(cfg.SqlSettings.DataSourceSearchReplicas, " "), model.FakeSetting) && len(cfg.SqlSettings.DataSourceSearchReplicas) != 0 {
|
||||
require.FailNow(t, "did not sanitize properly")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetConfigWithAccessTag(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
// set some values so that we know they're not blank
|
||||
mockVaryByHeader := model.NewId()
|
||||
mockSupportEmail := model.NewId() + "@mattermost.com"
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.RateLimitSettings.VaryByHeader = mockVaryByHeader
|
||||
cfg.SupportSettings.SupportEmail = &mockSupportEmail
|
||||
})
|
||||
|
||||
th.Client.Login(th.BasicUser.Username, th.BasicUser.Password)
|
||||
|
||||
// add read sysconsole environment config
|
||||
th.AddPermissionToRole(model.PermissionSysconsoleReadEnvironmentRateLimiting.Id, model.SystemUserRoleId)
|
||||
defer th.RemovePermissionFromRole(model.PermissionSysconsoleReadEnvironmentRateLimiting.Id, model.SystemUserRoleId)
|
||||
|
||||
cfg, _, err := th.Client.GetConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("Cannot read value without permission", func(t *testing.T) {
|
||||
assert.Nil(t, cfg.SupportSettings.SupportEmail)
|
||||
})
|
||||
|
||||
t.Run("Can read value with permission", func(t *testing.T) {
|
||||
assert.Equal(t, mockVaryByHeader, cfg.RateLimitSettings.VaryByHeader)
|
||||
})
|
||||
|
||||
t.Run("Contains Feature Flags", func(t *testing.T) {
|
||||
assert.NotNil(t, cfg.FeatureFlags)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetConfigAnyFlagsAccess(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(th.BasicUser.Username, th.BasicUser.Password)
|
||||
_, resp, _ := th.Client.GetConfig()
|
||||
|
||||
t.Run("Check permissions error with no sysconsole read permission", func(t *testing.T) {
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
// add read sysconsole environment config
|
||||
th.AddPermissionToRole(model.PermissionSysconsoleReadEnvironmentRateLimiting.Id, model.SystemUserRoleId)
|
||||
defer th.RemovePermissionFromRole(model.PermissionSysconsoleReadEnvironmentRateLimiting.Id, model.SystemUserRoleId)
|
||||
|
||||
cfg, _, err := th.Client.GetConfig()
|
||||
require.NoError(t, err)
|
||||
t.Run("Can read value with permission", func(t *testing.T) {
|
||||
assert.NotNil(t, cfg.FeatureFlags)
|
||||
})
|
||||
}
|
||||
|
||||
func TestReloadConfig(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
client := th.Client
|
||||
|
||||
t.Run("as system user", func(t *testing.T) {
|
||||
resp, err := client.ReloadConfig()
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||
_, err := client.ReloadConfig()
|
||||
require.NoError(t, err)
|
||||
}, "as system admin and local mode")
|
||||
|
||||
t.Run("as restricted system admin", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true })
|
||||
|
||||
resp, err := client.ReloadConfig()
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateConfig(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
client := th.Client
|
||||
|
||||
cfg, _, err := th.SystemAdminClient.GetConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, resp, err := client.UpdateConfig(cfg)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||
SiteName := th.App.Config().TeamSettings.SiteName
|
||||
|
||||
*cfg.TeamSettings.SiteName = "MyFancyName"
|
||||
cfg, _, err = client.UpdateConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "MyFancyName", *cfg.TeamSettings.SiteName, "It should update the SiteName")
|
||||
|
||||
//Revert the change
|
||||
cfg.TeamSettings.SiteName = SiteName
|
||||
cfg, _, err = client.UpdateConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, SiteName, cfg.TeamSettings.SiteName, "It should update the SiteName")
|
||||
|
||||
t.Run("Should set defaults for missing fields", func(t *testing.T) {
|
||||
_, err = th.SystemAdminClient.DoAPIPut("/config", "{}")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Should fail with validation error if invalid config setting is passed", func(t *testing.T) {
|
||||
//Revert the change
|
||||
badcfg := cfg.Clone()
|
||||
badcfg.PasswordSettings.MinimumLength = model.NewInt(4)
|
||||
badcfg.PasswordSettings.MinimumLength = model.NewInt(4)
|
||||
_, resp, err = client.UpdateConfig(badcfg)
|
||||
require.Error(t, err)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
CheckErrorID(t, err, "model.config.is_valid.password_length.app_error")
|
||||
})
|
||||
|
||||
t.Run("Should not be able to modify PluginSettings.EnableUploads", func(t *testing.T) {
|
||||
oldEnableUploads := *th.App.Config().PluginSettings.EnableUploads
|
||||
*cfg.PluginSettings.EnableUploads = !oldEnableUploads
|
||||
|
||||
cfg, _, err = client.UpdateConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, oldEnableUploads, *cfg.PluginSettings.EnableUploads)
|
||||
assert.Equal(t, oldEnableUploads, *th.App.Config().PluginSettings.EnableUploads)
|
||||
|
||||
cfg.PluginSettings.EnableUploads = nil
|
||||
cfg, _, err = client.UpdateConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, oldEnableUploads, *cfg.PluginSettings.EnableUploads)
|
||||
assert.Equal(t, oldEnableUploads, *th.App.Config().PluginSettings.EnableUploads)
|
||||
})
|
||||
|
||||
t.Run("Should not be able to modify PluginSettings.SignaturePublicKeyFiles", func(t *testing.T) {
|
||||
oldPublicKeys := th.App.Config().PluginSettings.SignaturePublicKeyFiles
|
||||
cfg.PluginSettings.SignaturePublicKeyFiles = append(cfg.PluginSettings.SignaturePublicKeyFiles, "new_signature")
|
||||
|
||||
cfg, _, err = client.UpdateConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, oldPublicKeys, cfg.PluginSettings.SignaturePublicKeyFiles)
|
||||
assert.Equal(t, oldPublicKeys, th.App.Config().PluginSettings.SignaturePublicKeyFiles)
|
||||
|
||||
cfg.PluginSettings.SignaturePublicKeyFiles = nil
|
||||
cfg, _, err = client.UpdateConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, oldPublicKeys, cfg.PluginSettings.SignaturePublicKeyFiles)
|
||||
assert.Equal(t, oldPublicKeys, th.App.Config().PluginSettings.SignaturePublicKeyFiles)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Should not be able to modify PluginSettings.MarketplaceURL if EnableUploads is disabled", func(t *testing.T) {
|
||||
oldURL := "hello.com"
|
||||
newURL := "new.com"
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.EnableUploads = false
|
||||
*cfg.PluginSettings.MarketplaceURL = oldURL
|
||||
})
|
||||
|
||||
cfg2 := th.App.Config().Clone()
|
||||
*cfg2.PluginSettings.MarketplaceURL = newURL
|
||||
|
||||
cfg2, _, err = th.SystemAdminClient.UpdateConfig(cfg2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, oldURL, *cfg2.PluginSettings.MarketplaceURL)
|
||||
|
||||
// Allowing uploads
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.EnableUploads = true
|
||||
*cfg.PluginSettings.MarketplaceURL = oldURL
|
||||
})
|
||||
|
||||
cfg2 = th.App.Config().Clone()
|
||||
*cfg2.PluginSettings.MarketplaceURL = newURL
|
||||
|
||||
cfg2, _, err = th.SystemAdminClient.UpdateConfig(cfg2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, newURL, *cfg2.PluginSettings.MarketplaceURL)
|
||||
})
|
||||
|
||||
t.Run("Should not be able to modify ComplianceSettings.Directory in cloud", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
defer th.App.Srv().RemoveLicense()
|
||||
|
||||
cfg2 := th.App.Config().Clone()
|
||||
*cfg2.ComplianceSettings.Directory = "hellodir"
|
||||
|
||||
_, resp, err = th.SystemAdminClient.UpdateConfig(cfg2)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("System Admin should not be able to clear Site URL", func(t *testing.T) {
|
||||
siteURL := cfg.ServiceSettings.SiteURL
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SiteURL = siteURL })
|
||||
|
||||
nonEmptyURL := "http://localhost"
|
||||
cfg.ServiceSettings.SiteURL = &nonEmptyURL
|
||||
|
||||
// Set the SiteURL
|
||||
cfg, _, err = th.SystemAdminClient.UpdateConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, nonEmptyURL, *cfg.ServiceSettings.SiteURL)
|
||||
|
||||
// Check that the Site URL can't be cleared
|
||||
cfg.ServiceSettings.SiteURL = sToP("")
|
||||
cfg, resp, err = th.SystemAdminClient.UpdateConfig(cfg)
|
||||
require.Error(t, err)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
CheckErrorID(t, err, "api.config.update_config.clear_siteurl.app_error")
|
||||
// Check that the Site URL wasn't cleared
|
||||
cfg, _, err = th.SystemAdminClient.GetConfig()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, nonEmptyURL, *cfg.ServiceSettings.SiteURL)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetConfigWithoutManageSystemPermission(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
th.Client.Login(th.BasicUser.Username, th.BasicUser.Password)
|
||||
|
||||
t.Run("any sysconsole read permission provides config read access", func(t *testing.T) {
|
||||
// forbidden by default
|
||||
_, resp, err := th.Client.GetConfig()
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
// add any sysconsole read permission
|
||||
th.AddPermissionToRole(model.SysconsoleReadPermissions[0].Id, model.SystemUserRoleId)
|
||||
_, _, err = th.Client.GetConfig()
|
||||
// should be readable now
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateConfigWithoutManageSystemPermission(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
th.Client.Login(th.BasicUser.Username, th.BasicUser.Password)
|
||||
|
||||
// add read sysconsole integrations config
|
||||
th.AddPermissionToRole(model.PermissionSysconsoleReadIntegrationsIntegrationManagement.Id, model.SystemUserRoleId)
|
||||
defer th.RemovePermissionFromRole(model.PermissionSysconsoleReadIntegrationsIntegrationManagement.Id, model.SystemUserRoleId)
|
||||
|
||||
t.Run("sysconsole read permission does not provides config write access", func(t *testing.T) {
|
||||
// should be readable because has a sysconsole read permission
|
||||
cfg, _, err := th.Client.GetConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, resp, err := th.Client.UpdateConfig(cfg)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("the wrong write permission does not grant access", func(t *testing.T) {
|
||||
// should be readable because has a sysconsole read permission
|
||||
cfg, _, err := th.SystemAdminClient.GetConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
originalValue := *cfg.ServiceSettings.AllowCorsFrom
|
||||
|
||||
// add the wrong write permission
|
||||
th.AddPermissionToRole(model.PermissionSysconsoleWriteAboutEditionAndLicense.Id, model.SystemUserRoleId)
|
||||
defer th.RemovePermissionFromRole(model.PermissionSysconsoleWriteAboutEditionAndLicense.Id, model.SystemUserRoleId)
|
||||
|
||||
// try update a config value allowed by sysconsole WRITE integrations
|
||||
mockVal := model.NewId()
|
||||
cfg.ServiceSettings.AllowCorsFrom = &mockVal
|
||||
_, _, err = th.Client.UpdateConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// ensure the config setting was not updated
|
||||
cfg, _, err = th.SystemAdminClient.GetConfig()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, *cfg.ServiceSettings.AllowCorsFrom, originalValue)
|
||||
})
|
||||
|
||||
t.Run("config value is writeable by specific system console permission", func(t *testing.T) {
|
||||
// should be readable because has a sysconsole read permission
|
||||
cfg, _, err := th.SystemAdminClient.GetConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
th.AddPermissionToRole(model.PermissionSysconsoleWriteIntegrationsCors.Id, model.SystemUserRoleId)
|
||||
defer th.RemovePermissionFromRole(model.PermissionSysconsoleWriteIntegrationsCors.Id, model.SystemUserRoleId)
|
||||
th.AddPermissionToRole(model.PermissionSysconsoleReadIntegrationsCors.Id, model.SystemUserRoleId)
|
||||
defer th.RemovePermissionFromRole(model.PermissionSysconsoleReadIntegrationsCors.Id, model.SystemUserRoleId)
|
||||
|
||||
// try update a config value allowed by sysconsole WRITE integrations
|
||||
mockVal := model.NewId()
|
||||
cfg.ServiceSettings.AllowCorsFrom = &mockVal
|
||||
_, _, err = th.Client.UpdateConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// ensure the config setting was updated
|
||||
cfg, _, err = th.Client.GetConfig()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, *cfg.ServiceSettings.AllowCorsFrom, mockVal)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateConfigMessageExportSpecialHandling(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
messageExportEnabled := *th.App.Config().MessageExportSettings.EnableExport
|
||||
messageExportTimestamp := *th.App.Config().MessageExportSettings.ExportFromTimestamp
|
||||
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.MessageExportSettings.EnableExport = messageExportEnabled
|
||||
*cfg.MessageExportSettings.ExportFromTimestamp = messageExportTimestamp
|
||||
})
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.MessageExportSettings.EnableExport = false
|
||||
*cfg.MessageExportSettings.ExportFromTimestamp = int64(0)
|
||||
})
|
||||
|
||||
// Turn it on, timestamp should be updated.
|
||||
cfg, _, err := th.SystemAdminClient.GetConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
*cfg.MessageExportSettings.EnableExport = true
|
||||
_, _, err = th.SystemAdminClient.UpdateConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, *th.App.Config().MessageExportSettings.EnableExport)
|
||||
assert.NotEqual(t, int64(0), *th.App.Config().MessageExportSettings.ExportFromTimestamp)
|
||||
|
||||
// Turn it off, timestamp should be cleared.
|
||||
cfg, _, err = th.SystemAdminClient.GetConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
*cfg.MessageExportSettings.EnableExport = false
|
||||
_, _, err = th.SystemAdminClient.UpdateConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, *th.App.Config().MessageExportSettings.EnableExport)
|
||||
assert.Equal(t, int64(0), *th.App.Config().MessageExportSettings.ExportFromTimestamp)
|
||||
|
||||
// Set a value from the config file.
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.MessageExportSettings.EnableExport = false
|
||||
*cfg.MessageExportSettings.ExportFromTimestamp = int64(12345)
|
||||
})
|
||||
|
||||
// Turn it on, timestamp should *not* be updated.
|
||||
cfg, _, err = th.SystemAdminClient.GetConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
*cfg.MessageExportSettings.EnableExport = true
|
||||
_, _, err = th.SystemAdminClient.UpdateConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, *th.App.Config().MessageExportSettings.EnableExport)
|
||||
assert.Equal(t, int64(12345), *th.App.Config().MessageExportSettings.ExportFromTimestamp)
|
||||
|
||||
// Turn it off, timestamp should be cleared.
|
||||
cfg, _, err = th.SystemAdminClient.GetConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
*cfg.MessageExportSettings.EnableExport = false
|
||||
_, _, err = th.SystemAdminClient.UpdateConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, *th.App.Config().MessageExportSettings.EnableExport)
|
||||
assert.Equal(t, int64(0), *th.App.Config().MessageExportSettings.ExportFromTimestamp)
|
||||
}
|
||||
|
||||
func TestUpdateConfigRestrictSystemAdmin(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true })
|
||||
|
||||
t.Run("Restrict flag should be honored for sysadmin", func(t *testing.T) {
|
||||
originalCfg, _, err := th.SystemAdminClient.GetConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := originalCfg.Clone()
|
||||
*cfg.TeamSettings.SiteName = "MyFancyName" // Allowed
|
||||
*cfg.ServiceSettings.SiteURL = "http://example.com" // Ignored
|
||||
|
||||
returnedCfg, _, err := th.SystemAdminClient.UpdateConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "MyFancyName", *returnedCfg.TeamSettings.SiteName)
|
||||
require.Equal(t, *originalCfg.ServiceSettings.SiteURL, *returnedCfg.ServiceSettings.SiteURL)
|
||||
|
||||
actualCfg, _, err := th.SystemAdminClient.GetConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, returnedCfg, actualCfg)
|
||||
})
|
||||
|
||||
t.Run("Restrict flag should be ignored by local mode", func(t *testing.T) {
|
||||
originalCfg, _, err := th.LocalClient.GetConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := originalCfg.Clone()
|
||||
*cfg.TeamSettings.SiteName = "MyFancyName" // Allowed
|
||||
*cfg.ServiceSettings.SiteURL = "http://example.com" // Ignored
|
||||
|
||||
returnedCfg, _, err := th.LocalClient.UpdateConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "MyFancyName", *returnedCfg.TeamSettings.SiteName)
|
||||
require.Equal(t, "http://example.com", *returnedCfg.ServiceSettings.SiteURL)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateConfigDiffInAuditRecord(t *testing.T) {
|
||||
logFile, err := os.CreateTemp("", "adv.log")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(logFile.Name())
|
||||
|
||||
os.Setenv("MM_EXPERIMENTALAUDITSETTINGS_FILEENABLED", "true")
|
||||
os.Setenv("MM_EXPERIMENTALAUDITSETTINGS_FILENAME", logFile.Name())
|
||||
defer os.Unsetenv("MM_EXPERIMENTALAUDITSETTINGS_FILEENABLED")
|
||||
defer os.Unsetenv("MM_EXPERIMENTALAUDITSETTINGS_FILENAME")
|
||||
|
||||
options := []app.Option{app.WithLicense(model.NewTestLicense("advanced_logging"))}
|
||||
th := SetupWithServerOptions(t, options)
|
||||
defer th.TearDown()
|
||||
|
||||
cfg, _, err := th.SystemAdminClient.GetConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
timeoutVal := *cfg.ServiceSettings.ReadTimeout
|
||||
cfg.ServiceSettings.ReadTimeout = model.NewInt(timeoutVal + 1)
|
||||
cfg, _, err = th.SystemAdminClient.UpdateConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.ServiceSettings.ReadTimeout = model.NewInt(timeoutVal)
|
||||
})
|
||||
require.Equal(t, timeoutVal+1, *cfg.ServiceSettings.ReadTimeout)
|
||||
|
||||
// Forcing a flush before attempting to read log's content.
|
||||
err = th.Server.Audit.Flush()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, logFile.Sync())
|
||||
|
||||
data, err := io.ReadAll(logFile)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, data)
|
||||
|
||||
require.Contains(t, string(data),
|
||||
fmt.Sprintf(`"config_diffs":[{"actual_val":%d,"base_val":%d,"path":"ServiceSettings.ReadTimeout"}]`,
|
||||
timeoutVal+1, timeoutVal))
|
||||
}
|
||||
|
||||
func TestGetEnvironmentConfig(t *testing.T) {
|
||||
os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://example.mattermost.com")
|
||||
os.Setenv("MM_SERVICESETTINGS_ENABLECUSTOMEMOJI", "true")
|
||||
defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
|
||||
defer os.Unsetenv("MM_SERVICESETTINGS_ENABLECUSTOMEMOJI")
|
||||
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("as system admin", func(t *testing.T) {
|
||||
SystemAdminClient := th.SystemAdminClient
|
||||
|
||||
envConfig, _, err := SystemAdminClient.GetEnvironmentConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
serviceSettings, ok := envConfig["ServiceSettings"]
|
||||
require.True(t, ok, "should've returned ServiceSettings")
|
||||
|
||||
serviceSettingsAsMap, ok := serviceSettings.(map[string]any)
|
||||
require.True(t, ok, "should've returned ServiceSettings as a map")
|
||||
|
||||
siteURL, ok := serviceSettingsAsMap["SiteURL"]
|
||||
require.True(t, ok, "should've returned ServiceSettings.SiteURL")
|
||||
|
||||
siteURLAsBool, ok := siteURL.(bool)
|
||||
require.True(t, ok, "should've returned ServiceSettings.SiteURL as a boolean")
|
||||
require.True(t, siteURLAsBool, "should've returned ServiceSettings.SiteURL as true")
|
||||
|
||||
enableCustomEmoji, ok := serviceSettingsAsMap["EnableCustomEmoji"]
|
||||
require.True(t, ok, "should've returned ServiceSettings.EnableCustomEmoji")
|
||||
|
||||
enableCustomEmojiAsBool, ok := enableCustomEmoji.(bool)
|
||||
require.True(t, ok, "should've returned ServiceSettings.EnableCustomEmoji as a boolean")
|
||||
require.True(t, enableCustomEmojiAsBool, "should've returned ServiceSettings.EnableCustomEmoji as true")
|
||||
|
||||
_, ok = envConfig["TeamSettings"]
|
||||
require.False(t, ok, "should not have returned TeamSettings")
|
||||
})
|
||||
|
||||
t.Run("as team admin", func(t *testing.T) {
|
||||
TeamAdminClient := th.CreateClient()
|
||||
th.LoginTeamAdminWithClient(TeamAdminClient)
|
||||
|
||||
envConfig, _, err := TeamAdminClient.GetEnvironmentConfig()
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, envConfig)
|
||||
})
|
||||
|
||||
t.Run("as regular user", func(t *testing.T) {
|
||||
client := th.Client
|
||||
|
||||
envConfig, _, err := client.GetEnvironmentConfig()
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, envConfig)
|
||||
})
|
||||
|
||||
t.Run("as not-regular user", func(t *testing.T) {
|
||||
client := th.CreateClient()
|
||||
|
||||
_, resp, err := client.GetEnvironmentConfig()
|
||||
require.Error(t, err)
|
||||
CheckUnauthorizedStatus(t, resp)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetOldClientConfig(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
testKey := "supersecretkey"
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.GoogleDeveloperKey = testKey })
|
||||
|
||||
t.Run("with session", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.GoogleDeveloperKey = testKey
|
||||
})
|
||||
|
||||
client := th.Client
|
||||
|
||||
config, _, err := client.GetOldClientConfig("")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotEmpty(t, config["Version"], "config not returned correctly")
|
||||
require.Equal(t, testKey, config["GoogleDeveloperKey"])
|
||||
})
|
||||
|
||||
t.Run("without session", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.GoogleDeveloperKey = testKey
|
||||
})
|
||||
|
||||
client := th.CreateClient()
|
||||
|
||||
config, _, err := client.GetOldClientConfig("")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotEmpty(t, config["Version"], "config not returned correctly")
|
||||
require.Empty(t, config["GoogleDeveloperKey"], "config should be missing developer key")
|
||||
})
|
||||
|
||||
t.Run("missing format", func(t *testing.T) {
|
||||
client := th.Client
|
||||
|
||||
resp, err := client.DoAPIGet("/config/client", "")
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("invalid format", func(t *testing.T) {
|
||||
client := th.Client
|
||||
|
||||
resp, err := client.DoAPIGet("/config/client?format=junk", "")
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPatchConfig(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("config is missing", func(t *testing.T) {
|
||||
_, response, err := th.Client.PatchConfig(nil)
|
||||
require.Error(t, err)
|
||||
CheckBadRequestStatus(t, response)
|
||||
})
|
||||
|
||||
t.Run("user is not system admin", func(t *testing.T) {
|
||||
_, response, err := th.Client.PatchConfig(&model.Config{})
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, response)
|
||||
})
|
||||
|
||||
t.Run("should not update the restricted fields when restrict toggle is on for sysadmin", func(t *testing.T) {
|
||||
*th.App.Config().ExperimentalSettings.RestrictSystemAdmin = true
|
||||
|
||||
config := model.Config{LogSettings: model.LogSettings{
|
||||
ConsoleLevel: model.NewString("INFO"),
|
||||
}}
|
||||
|
||||
updatedConfig, _, _ := th.SystemAdminClient.PatchConfig(&config)
|
||||
|
||||
assert.Equal(t, "DEBUG", *updatedConfig.LogSettings.ConsoleLevel)
|
||||
})
|
||||
|
||||
t.Run("should not bypass the restrict toggle if local client", func(t *testing.T) {
|
||||
*th.App.Config().ExperimentalSettings.RestrictSystemAdmin = true
|
||||
|
||||
config := model.Config{LogSettings: model.LogSettings{
|
||||
ConsoleLevel: model.NewString("INFO"),
|
||||
}}
|
||||
|
||||
oldConfig, _, _ := th.LocalClient.GetConfig()
|
||||
updatedConfig, _, _ := th.LocalClient.PatchConfig(&config)
|
||||
|
||||
assert.Equal(t, "INFO", *updatedConfig.LogSettings.ConsoleLevel)
|
||||
// reset the config
|
||||
_, _, err := th.LocalClient.UpdateConfig(oldConfig)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||
t.Run("check if config is valid", func(t *testing.T) {
|
||||
config := model.Config{PasswordSettings: model.PasswordSettings{
|
||||
MinimumLength: model.NewInt(4),
|
||||
}}
|
||||
|
||||
_, response, err := client.PatchConfig(&config)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, response.StatusCode)
|
||||
assert.Error(t, err)
|
||||
CheckErrorID(t, err, "model.config.is_valid.password_length.app_error")
|
||||
})
|
||||
|
||||
t.Run("should patch the config", func(t *testing.T) {
|
||||
*th.App.Config().ExperimentalSettings.RestrictSystemAdmin = false
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.ExperimentalDefaultChannels = []string{"some-channel"} })
|
||||
|
||||
oldConfig, _, err := client.GetConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, *oldConfig.PasswordSettings.Lowercase)
|
||||
assert.NotEqual(t, 15, *oldConfig.PasswordSettings.MinimumLength)
|
||||
assert.Equal(t, "DEBUG", *oldConfig.LogSettings.ConsoleLevel)
|
||||
assert.True(t, oldConfig.PluginSettings.PluginStates["com.mattermost.nps"].Enable)
|
||||
|
||||
states := make(map[string]*model.PluginState)
|
||||
states["com.mattermost.nps"] = &model.PluginState{Enable: *model.NewBool(false)}
|
||||
config := model.Config{PasswordSettings: model.PasswordSettings{
|
||||
Lowercase: model.NewBool(true),
|
||||
MinimumLength: model.NewInt(15),
|
||||
}, LogSettings: model.LogSettings{
|
||||
ConsoleLevel: model.NewString("INFO"),
|
||||
},
|
||||
TeamSettings: model.TeamSettings{
|
||||
ExperimentalDefaultChannels: []string{"another-channel"},
|
||||
},
|
||||
PluginSettings: model.PluginSettings{
|
||||
PluginStates: states,
|
||||
},
|
||||
}
|
||||
|
||||
_, response, err := client.PatchConfig(&config)
|
||||
require.NoError(t, err)
|
||||
|
||||
updatedConfig, _, err := client.GetConfig()
|
||||
require.NoError(t, err)
|
||||
assert.True(t, *updatedConfig.PasswordSettings.Lowercase)
|
||||
assert.Equal(t, "INFO", *updatedConfig.LogSettings.ConsoleLevel)
|
||||
assert.Equal(t, []string{"another-channel"}, updatedConfig.TeamSettings.ExperimentalDefaultChannels)
|
||||
assert.False(t, updatedConfig.PluginSettings.PluginStates["com.mattermost.nps"].Enable)
|
||||
assert.Equal(t, "no-cache, no-store, must-revalidate", response.Header.Get("Cache-Control"))
|
||||
|
||||
// reset the config
|
||||
_, _, err = client.UpdateConfig(oldConfig)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("should sanitize config", func(t *testing.T) {
|
||||
config := model.Config{PasswordSettings: model.PasswordSettings{
|
||||
Symbol: model.NewBool(true),
|
||||
}}
|
||||
|
||||
updatedConfig, _, err := client.PatchConfig(&config)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, model.FakeSetting, *updatedConfig.SqlSettings.DataSource)
|
||||
})
|
||||
|
||||
t.Run("not allowing to toggle enable uploads for plugin via api", func(t *testing.T) {
|
||||
config := model.Config{PluginSettings: model.PluginSettings{
|
||||
EnableUploads: model.NewBool(true),
|
||||
}}
|
||||
|
||||
updatedConfig, resp, err := client.PatchConfig(&config)
|
||||
if client == th.LocalClient {
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
assert.Equal(t, true, *updatedConfig.PluginSettings.EnableUploads)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Should not be able to modify PluginSettings.MarketplaceURL if EnableUploads is disabled", func(t *testing.T) {
|
||||
oldURL := "hello.com"
|
||||
newURL := "new.com"
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.EnableUploads = false
|
||||
*cfg.PluginSettings.MarketplaceURL = oldURL
|
||||
})
|
||||
|
||||
cfg := th.App.Config().Clone()
|
||||
*cfg.PluginSettings.MarketplaceURL = newURL
|
||||
|
||||
_, _, err := th.SystemAdminClient.PatchConfig(cfg)
|
||||
require.Error(t, err)
|
||||
|
||||
// Allowing uploads
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.EnableUploads = true
|
||||
*cfg.PluginSettings.MarketplaceURL = oldURL
|
||||
})
|
||||
|
||||
cfg = th.App.Config().Clone()
|
||||
*cfg.PluginSettings.MarketplaceURL = newURL
|
||||
|
||||
cfg, _, err = th.SystemAdminClient.PatchConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, newURL, *cfg.PluginSettings.MarketplaceURL)
|
||||
})
|
||||
|
||||
t.Run("System Admin should not be able to clear Site URL", func(t *testing.T) {
|
||||
cfg, _, err := th.SystemAdminClient.GetConfig()
|
||||
require.NoError(t, err)
|
||||
siteURL := cfg.ServiceSettings.SiteURL
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SiteURL = siteURL })
|
||||
|
||||
// Set the SiteURL
|
||||
nonEmptyURL := "http://localhost"
|
||||
config := model.Config{
|
||||
ServiceSettings: model.ServiceSettings{
|
||||
SiteURL: model.NewString(nonEmptyURL),
|
||||
},
|
||||
}
|
||||
updatedConfig, _, err := th.SystemAdminClient.PatchConfig(&config)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, nonEmptyURL, *updatedConfig.ServiceSettings.SiteURL)
|
||||
|
||||
// Check that the Site URL can't be cleared
|
||||
config = model.Config{
|
||||
ServiceSettings: model.ServiceSettings{
|
||||
SiteURL: model.NewString(""),
|
||||
},
|
||||
}
|
||||
_, resp, err := th.SystemAdminClient.PatchConfig(&config)
|
||||
require.Error(t, err)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
CheckErrorID(t, err, "api.config.update_config.clear_siteurl.app_error")
|
||||
|
||||
// Check that the Site URL wasn't cleared
|
||||
cfg, _, err = th.SystemAdminClient.GetConfig()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, nonEmptyURL, *cfg.ServiceSettings.SiteURL)
|
||||
|
||||
// Check that sending an empty config returns no error.
|
||||
_, _, err = th.SystemAdminClient.PatchConfig(&model.Config{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigrateConfig(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("LocalClient", func(t *testing.T) {
|
||||
cfg := &model.Config{}
|
||||
cfg.SetDefaults()
|
||||
|
||||
file, err := json.MarshalIndent(cfg, "", " ")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.WriteFile("from.json", file, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer os.Remove("from.json")
|
||||
|
||||
f, err := config.NewStoreFromDSN("from.json", false, nil, false)
|
||||
require.NoError(t, err)
|
||||
defer f.RemoveFile("from.json")
|
||||
|
||||
_, err = config.NewStoreFromDSN("to.json", false, nil, true)
|
||||
require.NoError(t, err)
|
||||
defer f.RemoveFile("to.json")
|
||||
|
||||
_, err = th.LocalClient.MigrateConfig("from.json", "to.json")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,497 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitDataRetention() {
|
||||
api.BaseRoutes.DataRetention.Handle("/policy", api.APISessionRequired(getGlobalPolicy)).Methods("GET")
|
||||
api.BaseRoutes.DataRetention.Handle("/policies", api.APISessionRequired(getPolicies)).Methods("GET")
|
||||
api.BaseRoutes.DataRetention.Handle("/policies_count", api.APISessionRequired(getPoliciesCount)).Methods("GET")
|
||||
api.BaseRoutes.DataRetention.Handle("/policies", api.APISessionRequired(createPolicy)).Methods("POST")
|
||||
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}", api.APISessionRequired(getPolicy)).Methods("GET")
|
||||
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}", api.APISessionRequired(patchPolicy)).Methods("PATCH")
|
||||
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}", api.APISessionRequired(deletePolicy)).Methods("DELETE")
|
||||
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/teams", api.APISessionRequired(getTeamsForPolicy)).Methods("GET")
|
||||
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/teams", api.APISessionRequired(addTeamsToPolicy)).Methods("POST")
|
||||
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/teams", api.APISessionRequired(removeTeamsFromPolicy)).Methods("DELETE")
|
||||
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/teams/search", api.APISessionRequired(searchTeamsInPolicy)).Methods("POST")
|
||||
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/channels", api.APISessionRequired(getChannelsForPolicy)).Methods("GET")
|
||||
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/channels", api.APISessionRequired(addChannelsToPolicy)).Methods("POST")
|
||||
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/channels", api.APISessionRequired(removeChannelsFromPolicy)).Methods("DELETE")
|
||||
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/channels/search", api.APISessionRequired(searchChannelsInPolicy)).Methods("POST")
|
||||
api.BaseRoutes.User.Handle("/data_retention/team_policies", api.APISessionRequired(getTeamPoliciesForUser)).Methods("GET")
|
||||
api.BaseRoutes.User.Handle("/data_retention/channel_policies", api.APISessionRequired(getChannelPoliciesForUser)).Methods("GET")
|
||||
}
|
||||
|
||||
func getGlobalPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// No permission check required.
|
||||
|
||||
policy, appErr := c.App.GetGlobalRetentionPolicy()
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(policy)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getGlobalPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func getPolicies(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
|
||||
return
|
||||
}
|
||||
|
||||
limit := c.Params.PerPage
|
||||
offset := c.Params.Page * limit
|
||||
|
||||
policies, appErr := c.App.GetRetentionPolicies(offset, limit)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(policies)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getPolicies", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func getPoliciesCount(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
|
||||
return
|
||||
}
|
||||
|
||||
count, appErr := c.App.GetRetentionPoliciesCount()
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
body := struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
}{count}
|
||||
err := json.NewEncoder(w).Encode(body)
|
||||
if err != nil {
|
||||
c.Logger.Warn("Error writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
|
||||
return
|
||||
}
|
||||
|
||||
c.RequirePolicyId()
|
||||
policy, appErr := c.App.GetRetentionPolicy(c.Params.PolicyId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(policy)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func createPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var policy model.RetentionPolicyWithTeamAndChannelIDs
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&policy); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("policy", jsonErr)
|
||||
return
|
||||
}
|
||||
auditRec := c.MakeAuditRecord("createPolicy", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameterAuditable(auditRec, "policy", &policy)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
|
||||
return
|
||||
}
|
||||
|
||||
newPolicy, appErr := c.App.CreateRetentionPolicy(&policy)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventResultState(newPolicy)
|
||||
auditRec.AddEventObjectType("policy")
|
||||
js, err := json.Marshal(newPolicy)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("createPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
auditRec.Success()
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func patchPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var patch model.RetentionPolicyWithTeamAndChannelIDs
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&patch); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("policy", jsonErr)
|
||||
return
|
||||
}
|
||||
c.RequirePolicyId()
|
||||
patch.ID = c.Params.PolicyId
|
||||
|
||||
auditRec := c.MakeAuditRecord("patchPolicy", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameterAuditable(auditRec, "patch", &patch)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
|
||||
return
|
||||
}
|
||||
|
||||
policy, appErr := c.App.PatchRetentionPolicy(&patch)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventResultState(policy)
|
||||
auditRec.AddEventObjectType("retention_policy")
|
||||
|
||||
js, err := json.Marshal(policy)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("patchPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
auditRec.Success()
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func deletePolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequirePolicyId()
|
||||
policyId := c.Params.PolicyId
|
||||
|
||||
auditRec := c.MakeAuditRecord("deletePolicy", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "policy_id", policyId)
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
|
||||
return
|
||||
}
|
||||
|
||||
err := c.App.DeleteRetentionPolicy(policyId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func getTeamsForPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
|
||||
return
|
||||
}
|
||||
|
||||
c.RequirePolicyId()
|
||||
policyId := c.Params.PolicyId
|
||||
limit := c.Params.PerPage
|
||||
offset := c.Params.Page * limit
|
||||
|
||||
teams, appErr := c.App.GetTeamsForRetentionPolicy(policyId, offset, limit)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(teams)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getTeamsForPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func searchTeamsInPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequirePolicyId()
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
|
||||
return
|
||||
}
|
||||
|
||||
var props model.TeamSearch
|
||||
if err := json.NewDecoder(r.Body).Decode(&props); err != nil {
|
||||
c.SetInvalidParamWithErr("team_search", err)
|
||||
return
|
||||
}
|
||||
|
||||
props.PolicyID = model.NewString(c.Params.PolicyId)
|
||||
props.IncludePolicyID = model.NewBool(true)
|
||||
|
||||
teams, _, appErr := c.App.SearchAllTeams(&props)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
c.App.SanitizeTeams(*c.AppContext.Session(), teams)
|
||||
|
||||
js, err := json.Marshal(teams)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("searchTeamsInPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func addTeamsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequirePolicyId()
|
||||
policyId := c.Params.PolicyId
|
||||
var teamIDs []string
|
||||
jsonErr := json.NewDecoder(r.Body).Decode(&teamIDs)
|
||||
if jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("team_ids", jsonErr)
|
||||
return
|
||||
}
|
||||
auditRec := c.MakeAuditRecord("addTeamsToPolicy", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "policy_id", policyId)
|
||||
audit.AddEventParameter(auditRec, "team_ids", teamIDs)
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
|
||||
return
|
||||
}
|
||||
|
||||
err := c.App.AddTeamsToRetentionPolicy(policyId, teamIDs)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func removeTeamsFromPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequirePolicyId()
|
||||
policyId := c.Params.PolicyId
|
||||
var teamIDs []string
|
||||
jsonErr := json.NewDecoder(r.Body).Decode(&teamIDs)
|
||||
if jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("team_ids", jsonErr)
|
||||
return
|
||||
}
|
||||
auditRec := c.MakeAuditRecord("removeTeamsFromPolicy", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "policy_id", policyId)
|
||||
audit.AddEventParameter(auditRec, "team_ids", teamIDs)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
|
||||
return
|
||||
}
|
||||
|
||||
err := c.App.RemoveTeamsFromRetentionPolicy(policyId, teamIDs)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func getChannelsForPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
|
||||
return
|
||||
}
|
||||
|
||||
c.RequirePolicyId()
|
||||
policyId := c.Params.PolicyId
|
||||
limit := c.Params.PerPage
|
||||
offset := c.Params.Page * limit
|
||||
|
||||
channels, appErr := c.App.GetChannelsForRetentionPolicy(policyId, offset, limit)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(channels)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getChannelsForPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func searchChannelsInPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequirePolicyId()
|
||||
var props *model.ChannelSearch
|
||||
err := json.NewDecoder(r.Body).Decode(&props)
|
||||
if err != nil {
|
||||
c.SetInvalidParamWithErr("channel_search", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
|
||||
return
|
||||
}
|
||||
|
||||
opts := model.ChannelSearchOpts{
|
||||
PolicyID: c.Params.PolicyId,
|
||||
IncludePolicyID: true,
|
||||
Deleted: props.Deleted,
|
||||
IncludeDeleted: props.IncludeDeleted,
|
||||
Public: props.Public,
|
||||
Private: props.Private,
|
||||
TeamIds: props.TeamIds,
|
||||
}
|
||||
|
||||
channels, _, appErr := c.App.SearchAllChannels(c.AppContext, props.Term, opts)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
channelsJSON, jsonErr := json.Marshal(channels)
|
||||
if jsonErr != nil {
|
||||
c.Err = model.NewAppError("searchChannelsInPolicy", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(channelsJSON)
|
||||
}
|
||||
|
||||
func addChannelsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequirePolicyId()
|
||||
policyId := c.Params.PolicyId
|
||||
var channelIDs []string
|
||||
jsonErr := json.NewDecoder(r.Body).Decode(&channelIDs)
|
||||
if jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("channel_ids", jsonErr)
|
||||
return
|
||||
}
|
||||
auditRec := c.MakeAuditRecord("addChannelsToPolicy", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "policy_id", policyId)
|
||||
audit.AddEventParameter(auditRec, "channel_ids", channelIDs)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
|
||||
return
|
||||
}
|
||||
|
||||
err := c.App.AddChannelsToRetentionPolicy(policyId, channelIDs)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func removeChannelsFromPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequirePolicyId()
|
||||
policyId := c.Params.PolicyId
|
||||
var channelIDs []string
|
||||
jsonErr := json.NewDecoder(r.Body).Decode(&channelIDs)
|
||||
if jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("channel_ids", jsonErr)
|
||||
return
|
||||
}
|
||||
auditRec := c.MakeAuditRecord("removeChannelsFromPolicy", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "policy_id", policyId)
|
||||
audit.AddEventParameter(auditRec, "channel_ids", channelIDs)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
|
||||
return
|
||||
}
|
||||
|
||||
err := c.App.RemoveChannelsFromRetentionPolicy(policyId, channelIDs)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func getTeamPoliciesForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
userID := c.Params.UserId
|
||||
limit := c.Params.PerPage
|
||||
offset := c.Params.Page * limit
|
||||
|
||||
if userID != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
policies, err := c.App.GetTeamPoliciesForUser(userID, offset, limit)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
js, jsonErr := json.Marshal(policies)
|
||||
if jsonErr != nil {
|
||||
c.Err = model.NewAppError("getTeamPoliciesForUser", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func getChannelPoliciesForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
userID := c.Params.UserId
|
||||
limit := c.Params.PerPage
|
||||
offset := c.Params.Page * limit
|
||||
|
||||
if userID != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
policies, err := c.App.GetChannelPoliciesForUser(userID, offset, limit)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
js, jsonErr := json.Marshal(policies)
|
||||
if jsonErr != nil {
|
||||
c.Err = model.NewAppError("getChannelPoliciesForUser", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Write(js)
|
||||
}
|
||||
297
api4/emoji.go
297
api4/emoji.go
|
|
@ -1,297 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app"
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/web"
|
||||
)
|
||||
|
||||
const (
|
||||
EmojiMaxAutocompleteItems = 100
|
||||
)
|
||||
|
||||
func (api *API) InitEmoji() {
|
||||
api.BaseRoutes.Emojis.Handle("", api.APISessionRequired(createEmoji)).Methods("POST")
|
||||
api.BaseRoutes.Emojis.Handle("", api.APISessionRequired(getEmojiList)).Methods("GET")
|
||||
api.BaseRoutes.Emojis.Handle("/search", api.APISessionRequired(searchEmojis)).Methods("POST")
|
||||
api.BaseRoutes.Emojis.Handle("/autocomplete", api.APISessionRequired(autocompleteEmojis)).Methods("GET")
|
||||
api.BaseRoutes.Emoji.Handle("", api.APISessionRequired(deleteEmoji)).Methods("DELETE")
|
||||
api.BaseRoutes.Emoji.Handle("", api.APISessionRequired(getEmoji)).Methods("GET")
|
||||
api.BaseRoutes.EmojiByName.Handle("", api.APISessionRequired(getEmojiByName)).Methods("GET")
|
||||
api.BaseRoutes.Emoji.Handle("/image", api.APISessionRequiredTrustRequester(getEmojiImage)).Methods("GET")
|
||||
}
|
||||
|
||||
func createEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
defer io.Copy(io.Discard, r.Body)
|
||||
|
||||
if !*c.App.Config().ServiceSettings.EnableCustomEmoji {
|
||||
c.Err = model.NewAppError("createEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
if r.ContentLength > app.MaxEmojiFileSize {
|
||||
c.Err = model.NewAppError("createEmoji", "api.emoji.create.too_large.app_error", nil, "", http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseMultipartForm(app.MaxEmojiFileSize); err != nil {
|
||||
c.Err = model.NewAppError("createEmoji", "api.emoji.create.parse.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("createEmoji", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
// Allow any user with CREATE_EMOJIS permission at Team level to create emojis at system level
|
||||
memberships, err := c.App.GetTeamMembersForUser(c.AppContext.Session().UserId, "", true)
|
||||
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateEmojis) {
|
||||
hasPermission := false
|
||||
for _, membership := range memberships {
|
||||
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), membership.TeamId, model.PermissionCreateEmojis) {
|
||||
hasPermission = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasPermission {
|
||||
c.SetPermissionError(model.PermissionCreateEmojis)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
m := r.MultipartForm
|
||||
props := m.Value
|
||||
|
||||
if len(props["emoji"]) == 0 {
|
||||
c.SetInvalidParam("emoji")
|
||||
return
|
||||
}
|
||||
|
||||
var emoji model.Emoji
|
||||
if jsonErr := json.Unmarshal([]byte(props["emoji"][0]), &emoji); jsonErr != nil {
|
||||
c.SetInvalidParam("emoji")
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventResultState(&emoji)
|
||||
auditRec.AddEventObjectType("emoji")
|
||||
|
||||
newEmoji, err := c.App.CreateEmoji(c.AppContext, c.AppContext.Session().UserId, &emoji, m)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
if err := json.NewEncoder(w).Encode(newEmoji); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getEmojiList(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !*c.App.Config().ServiceSettings.EnableCustomEmoji {
|
||||
c.Err = model.NewAppError("getEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
sort := r.URL.Query().Get("sort")
|
||||
if sort != "" && sort != model.EmojiSortByName {
|
||||
c.SetInvalidURLParam("sort")
|
||||
return
|
||||
}
|
||||
|
||||
listEmoji, err := c.App.GetEmojiList(c.AppContext, c.Params.Page, c.Params.PerPage, sort)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(listEmoji); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func deleteEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireEmojiId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("deleteEmoji", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
emoji, err := c.App.GetEmoji(c.AppContext, c.Params.EmojiId)
|
||||
if err != nil {
|
||||
audit.AddEventParameter(auditRec, "emoji_id", c.Params.EmojiId)
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
auditRec.AddEventPriorState(emoji)
|
||||
auditRec.AddEventObjectType("emoji")
|
||||
|
||||
// Allow any user with DELETE_EMOJIS permission at Team level to delete emojis at system level
|
||||
memberships, err := c.App.GetTeamMembersForUser(c.AppContext.Session().UserId, "", true)
|
||||
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionDeleteEmojis) {
|
||||
hasPermission := false
|
||||
for _, membership := range memberships {
|
||||
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), membership.TeamId, model.PermissionDeleteEmojis) {
|
||||
hasPermission = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasPermission {
|
||||
c.SetPermissionError(model.PermissionDeleteEmojis)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if c.AppContext.Session().UserId != emoji.CreatorId {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionDeleteOthersEmojis) {
|
||||
hasPermission := false
|
||||
for _, membership := range memberships {
|
||||
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), membership.TeamId, model.PermissionDeleteOthersEmojis) {
|
||||
hasPermission = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasPermission {
|
||||
c.SetPermissionError(model.PermissionDeleteOthersEmojis)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = c.App.DeleteEmoji(c.AppContext, emoji)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func getEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireEmojiId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !*c.App.Config().ServiceSettings.EnableCustomEmoji {
|
||||
c.Err = model.NewAppError("getEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
emoji, err := c.App.GetEmoji(c.AppContext, c.Params.EmojiId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(emoji); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getEmojiByName(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireEmojiName()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
emoji, err := c.App.GetEmojiByName(c.AppContext, c.Params.EmojiName)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(emoji); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getEmojiImage(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireEmojiId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !*c.App.Config().ServiceSettings.EnableCustomEmoji {
|
||||
c.Err = model.NewAppError("getEmojiImage", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
image, imageType, err := c.App.GetEmojiImage(c.AppContext, c.Params.EmojiId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "image/"+imageType)
|
||||
w.Header().Set("Cache-Control", "max-age=2592000, private")
|
||||
w.Write(image)
|
||||
}
|
||||
|
||||
func searchEmojis(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var emojiSearch model.EmojiSearch
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&emojiSearch); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("term", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
if emojiSearch.Term == "" {
|
||||
c.SetInvalidParam("term")
|
||||
return
|
||||
}
|
||||
|
||||
emojis, err := c.App.SearchEmoji(c.AppContext, emojiSearch.Term, emojiSearch.PrefixOnly, web.PerPageMaximum)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(emojis); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func autocompleteEmojis(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
name := r.URL.Query().Get("name")
|
||||
|
||||
if name == "" {
|
||||
c.SetInvalidURLParam("name")
|
||||
return
|
||||
}
|
||||
|
||||
emojis, err := c.App.SearchEmoji(c.AppContext, name, true, EmojiMaxAutocompleteItems)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(emojis); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
func (api *API) InitExport() {
|
||||
api.BaseRoutes.Exports.Handle("", api.APISessionRequired(listExports)).Methods("GET")
|
||||
api.BaseRoutes.Export.Handle("", api.APISessionRequired(deleteExport)).Methods("DELETE")
|
||||
api.BaseRoutes.Export.Handle("", api.APISessionRequired(downloadExport)).Methods("GET")
|
||||
}
|
||||
|
||||
func listExports(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.IsSystemAdmin() {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
exports, appErr := c.App.ListExports()
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
data, err := json.Marshal(exports)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("listImports", "app.export.marshal.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
func deleteExport(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
auditRec := c.MakeAuditRecord("deleteExport", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "export_name", c.Params.ExportName)
|
||||
|
||||
if !c.IsSystemAdmin() {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.DeleteExport(c.Params.ExportName); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func downloadExport(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.IsSystemAdmin() {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
filePath := filepath.Join(*c.App.Config().ExportSettings.Directory, c.Params.ExportName)
|
||||
if ok, err := c.App.FileExists(filePath); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
} else if !ok {
|
||||
c.Err = model.NewAppError("downloadExport", "api.export.export_not_found.app_error", nil, "", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
file, err := c.App.FileReader(filePath)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
http.ServeContent(w, r, c.Params.ExportName, time.Time{}, file)
|
||||
}
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/utils/fileutils"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestListExports(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("no permissions", func(t *testing.T) {
|
||||
exports, _, err := th.Client.ListExports()
|
||||
require.Error(t, err)
|
||||
CheckErrorID(t, err, "api.context.permissions.app_error")
|
||||
require.Nil(t, exports)
|
||||
})
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
|
||||
exports, _, err := c.ListExports()
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, exports)
|
||||
}, "no exports")
|
||||
|
||||
dataDir, found := fileutils.FindDir("data")
|
||||
require.True(t, found)
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
|
||||
exportDir := filepath.Join(dataDir, *th.App.Config().ExportSettings.Directory)
|
||||
err := os.Mkdir(exportDir, 0700)
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(exportDir)
|
||||
|
||||
f, err := os.Create(filepath.Join(exportDir, "export.zip"))
|
||||
require.NoError(t, err)
|
||||
f.Close()
|
||||
|
||||
exports, _, err := c.ListExports()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, exports, 1)
|
||||
require.Equal(t, exports[0], "export.zip")
|
||||
}, "expected exports")
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
|
||||
value := *th.App.Config().ExportSettings.Directory
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExportSettings.Directory = value + "new" })
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExportSettings.Directory = value })
|
||||
|
||||
exportDir := filepath.Join(dataDir, value+"new")
|
||||
err := os.Mkdir(exportDir, 0700)
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(exportDir)
|
||||
|
||||
exports, _, err := c.ListExports()
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, exports)
|
||||
|
||||
f, err := os.Create(filepath.Join(exportDir, "export.zip"))
|
||||
require.NoError(t, err)
|
||||
f.Close()
|
||||
|
||||
exports, _, err = c.ListExports()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, exports, 1)
|
||||
require.Equal(t, "export.zip", exports[0])
|
||||
}, "change export directory")
|
||||
}
|
||||
|
||||
func TestDeleteExport(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("no permissions", func(t *testing.T) {
|
||||
_, err := th.Client.DeleteExport("export.zip")
|
||||
require.Error(t, err)
|
||||
CheckErrorID(t, err, "api.context.permissions.app_error")
|
||||
})
|
||||
|
||||
dataDir, found := fileutils.FindDir("data")
|
||||
require.True(t, found)
|
||||
exportDir := filepath.Join(dataDir, *th.App.Config().ExportSettings.Directory)
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
|
||||
err := os.Mkdir(exportDir, 0700)
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(exportDir)
|
||||
exportName := "export.zip"
|
||||
f, err := os.Create(filepath.Join(exportDir, exportName))
|
||||
require.NoError(t, err)
|
||||
f.Close()
|
||||
|
||||
exports, _, err := c.ListExports()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, exports, 1)
|
||||
require.Equal(t, exports[0], exportName)
|
||||
|
||||
_, err = c.DeleteExport(exportName)
|
||||
require.NoError(t, err)
|
||||
|
||||
exports, _, err = c.ListExports()
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, exports)
|
||||
|
||||
// verify idempotence
|
||||
_, err = c.DeleteExport(exportName)
|
||||
require.NoError(t, err)
|
||||
}, "successfully delete export")
|
||||
}
|
||||
|
||||
func TestDownloadExport(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("no permissions", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
n, _, err := th.Client.DownloadExport("export.zip", &buf, 0)
|
||||
require.Error(t, err)
|
||||
CheckErrorID(t, err, "api.context.permissions.app_error")
|
||||
require.Zero(t, n)
|
||||
})
|
||||
|
||||
dataDir, found := fileutils.FindDir("data")
|
||||
require.True(t, found)
|
||||
exportDir := filepath.Join(dataDir, *th.App.Config().ExportSettings.Directory)
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
|
||||
var buf bytes.Buffer
|
||||
n, _, err := c.DownloadExport("export.zip", &buf, 0)
|
||||
require.Error(t, err)
|
||||
CheckErrorID(t, err, "api.export.export_not_found.app_error")
|
||||
require.Zero(t, n)
|
||||
}, "not found")
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
|
||||
err := os.Mkdir(exportDir, 0700)
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(exportDir)
|
||||
|
||||
data := randomBytes(t, 1024*1024)
|
||||
var buf bytes.Buffer
|
||||
exportName := "export.zip"
|
||||
err = os.WriteFile(filepath.Join(exportDir, exportName), data, 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
n, _, err := c.DownloadExport(exportName, &buf, 0)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(data), int(n))
|
||||
require.Equal(t, data, buf.Bytes())
|
||||
}, "full download")
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
|
||||
err := os.Mkdir(exportDir, 0700)
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(exportDir)
|
||||
|
||||
data := randomBytes(t, 1024*1024)
|
||||
var buf bytes.Buffer
|
||||
exportName := "export.zip"
|
||||
err = os.WriteFile(filepath.Join(exportDir, exportName), data, 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
offset := 1024 * 512
|
||||
n, _, err := c.DownloadExport(exportName, &buf, int64(offset))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(data)-offset, int(n))
|
||||
require.Equal(t, data[offset:], buf.Bytes())
|
||||
}, "download with offset")
|
||||
}
|
||||
|
||||
func BenchmarkDownloadExport(b *testing.B) {
|
||||
th := Setup(b)
|
||||
defer th.TearDown()
|
||||
|
||||
dataDir, found := fileutils.FindDir("data")
|
||||
require.True(b, found)
|
||||
exportDir := filepath.Join(dataDir, *th.App.Config().ExportSettings.Directory)
|
||||
|
||||
err := os.Mkdir(exportDir, 0700)
|
||||
require.NoError(b, err)
|
||||
defer os.RemoveAll(exportDir)
|
||||
|
||||
exportName := "export.zip"
|
||||
f, err := os.Create(filepath.Join(exportDir, exportName))
|
||||
require.NoError(b, err)
|
||||
f.Close()
|
||||
|
||||
err = os.Truncate(filepath.Join(exportDir, exportName), 1024*1024*1024)
|
||||
require.NoError(b, err)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
outFilePath := filepath.Join(dataDir, fmt.Sprintf("export%d.zip", i))
|
||||
outFile, _ := os.Create(outFilePath)
|
||||
th.SystemAdminClient.DownloadExport(exportName, outFile, 0)
|
||||
outFile.Close()
|
||||
os.Remove(outFilePath)
|
||||
}
|
||||
}
|
||||
758
api4/file.go
758
api4/file.go
|
|
@ -1,758 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app"
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/web"
|
||||
"github.com/mattermost/mattermost-server/v6/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
FileTeamId = "noteam"
|
||||
|
||||
PreviewImageType = "image/jpeg"
|
||||
ThumbnailImageType = "image/jpeg"
|
||||
)
|
||||
|
||||
const maxMultipartFormDataBytes = 10 * 1024 // 10Kb
|
||||
|
||||
func (api *API) InitFile() {
|
||||
api.BaseRoutes.Files.Handle("", api.APISessionRequired(uploadFileStream)).Methods("POST")
|
||||
api.BaseRoutes.Files.Handle("/search", api.APISessionRequired(searchFilesForUser)).Methods("POST")
|
||||
api.BaseRoutes.File.Handle("", api.APISessionRequiredTrustRequester(getFile)).Methods("GET")
|
||||
api.BaseRoutes.File.Handle("/thumbnail", api.APISessionRequiredTrustRequester(getFileThumbnail)).Methods("GET")
|
||||
api.BaseRoutes.File.Handle("/link", api.APISessionRequired(getFileLink)).Methods("GET")
|
||||
api.BaseRoutes.File.Handle("/preview", api.APISessionRequiredTrustRequester(getFilePreview)).Methods("GET")
|
||||
api.BaseRoutes.File.Handle("/info", api.APISessionRequired(getFileInfo)).Methods("GET")
|
||||
|
||||
api.BaseRoutes.Team.Handle("/files/search", api.APISessionRequiredDisableWhenBusy(searchFilesInTeam)).Methods("POST")
|
||||
|
||||
api.BaseRoutes.PublicFile.Handle("", api.APIHandler(getPublicFile)).Methods("GET")
|
||||
|
||||
}
|
||||
|
||||
func parseMultipartRequestHeader(req *http.Request) (boundary string, err error) {
|
||||
v := req.Header.Get("Content-Type")
|
||||
if v == "" {
|
||||
return "", http.ErrNotMultipart
|
||||
}
|
||||
d, params, err := mime.ParseMediaType(v)
|
||||
if err != nil || d != "multipart/form-data" {
|
||||
return "", http.ErrNotMultipart
|
||||
}
|
||||
boundary, ok := params["boundary"]
|
||||
if !ok {
|
||||
return "", http.ErrMissingBoundary
|
||||
}
|
||||
|
||||
return boundary, nil
|
||||
}
|
||||
|
||||
func multipartReader(req *http.Request, stream io.Reader) (*multipart.Reader, error) {
|
||||
boundary, err := parseMultipartRequestHeader(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if stream != nil {
|
||||
return multipart.NewReader(stream, boundary), nil
|
||||
}
|
||||
|
||||
return multipart.NewReader(req.Body, boundary), nil
|
||||
}
|
||||
|
||||
func uploadFileStream(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !*c.App.Config().FileSettings.EnableFileAttachments {
|
||||
c.Err = model.NewAppError("uploadFileStream",
|
||||
"api.file.attachments.disabled.app_error",
|
||||
nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the post as a regular form (in practice, use the URL values
|
||||
// since we never expect a real application/x-www-form-urlencoded
|
||||
// form).
|
||||
if r.Form == nil {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("uploadFileStream",
|
||||
"api.file.upload_file.read_request.app_error",
|
||||
nil, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if r.ContentLength == 0 {
|
||||
c.Err = model.NewAppError("uploadFileStream",
|
||||
"api.file.upload_file.read_request.app_error",
|
||||
nil, "Content-Length should not be 0", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
timestamp := time.Now()
|
||||
var fileUploadResponse *model.FileUploadResponse
|
||||
|
||||
_, err := parseMultipartRequestHeader(r)
|
||||
switch err {
|
||||
case nil:
|
||||
fileUploadResponse = uploadFileMultipart(c, r, nil, timestamp)
|
||||
|
||||
case http.ErrNotMultipart:
|
||||
fileUploadResponse = uploadFileSimple(c, r, timestamp)
|
||||
|
||||
default:
|
||||
c.Err = model.NewAppError("uploadFileStream",
|
||||
"api.file.upload_file.read_request.app_error",
|
||||
nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Write the response values to the output upon return
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(fileUploadResponse); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
// uploadFileSimple uploads a file from a simple POST with the file in the request body
|
||||
func uploadFileSimple(c *Context, r *http.Request, timestamp time.Time) *model.FileUploadResponse {
|
||||
// Simple POST with the file in the body and all metadata in the args.
|
||||
c.RequireChannelId()
|
||||
c.RequireFilename()
|
||||
if c.Err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("uploadFileSimple", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
|
||||
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionUploadFile) {
|
||||
c.SetPermissionError(model.PermissionUploadFile)
|
||||
return nil
|
||||
}
|
||||
|
||||
clientId := r.Form.Get("client_id")
|
||||
audit.AddEventParameter(auditRec, "client_id", clientId)
|
||||
|
||||
info, appErr := c.App.UploadFileX(c.AppContext, c.Params.ChannelId, c.Params.Filename, r.Body,
|
||||
app.UploadFileSetTeamId(FileTeamId),
|
||||
app.UploadFileSetUserId(c.AppContext.Session().UserId),
|
||||
app.UploadFileSetTimestamp(timestamp),
|
||||
app.UploadFileSetContentLength(r.ContentLength),
|
||||
app.UploadFileSetClientId(clientId))
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return nil
|
||||
}
|
||||
audit.AddEventParameterAuditable(auditRec, "file", info)
|
||||
|
||||
fileUploadResponse := &model.FileUploadResponse{
|
||||
FileInfos: []*model.FileInfo{info},
|
||||
}
|
||||
if clientId != "" {
|
||||
fileUploadResponse.ClientIds = []string{clientId}
|
||||
}
|
||||
auditRec.Success()
|
||||
return fileUploadResponse
|
||||
}
|
||||
|
||||
// uploadFileMultipart parses and uploads file(s) from a mime/multipart
|
||||
// request. It pre-buffers up to the first part which is either the (a)
|
||||
// `channel_id` value, or (b) a file. Then in case of (a) it re-processes the
|
||||
// entire message recursively calling itself in stream mode. In case of (b) it
|
||||
// calls to uploadFileMultipartLegacy for legacy support
|
||||
func uploadFileMultipart(c *Context, r *http.Request, asStream io.Reader, timestamp time.Time) *model.FileUploadResponse {
|
||||
|
||||
expectClientIds := true
|
||||
var clientIds []string
|
||||
resp := model.FileUploadResponse{
|
||||
FileInfos: []*model.FileInfo{},
|
||||
ClientIds: []string{},
|
||||
}
|
||||
|
||||
var buf *bytes.Buffer
|
||||
var mr *multipart.Reader
|
||||
var err error
|
||||
if asStream == nil {
|
||||
// We need to buffer until we get the channel_id, or the first file.
|
||||
buf = &bytes.Buffer{}
|
||||
mr, err = multipartReader(r, io.TeeReader(r.Body, buf))
|
||||
} else {
|
||||
mr, err = multipartReader(r, asStream)
|
||||
}
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("uploadFileMultipart",
|
||||
"api.file.upload_file.read_request.app_error",
|
||||
nil, err.Error(), http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
|
||||
nFiles := 0
|
||||
NextPart:
|
||||
for {
|
||||
part, err := mr.NextPart()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("uploadFileMultipart",
|
||||
"api.file.upload_file.read_request.app_error",
|
||||
nil, err.Error(), http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse any form fields in the multipart.
|
||||
formname := part.FormName()
|
||||
if formname == "" {
|
||||
continue
|
||||
}
|
||||
filename := part.FileName()
|
||||
if filename == "" {
|
||||
var b bytes.Buffer
|
||||
_, err = io.CopyN(&b, part, maxMultipartFormDataBytes)
|
||||
if err != nil && err != io.EOF {
|
||||
c.Err = model.NewAppError("uploadFileMultipart",
|
||||
"api.file.upload_file.read_form_value.app_error",
|
||||
map[string]any{"Formname": formname},
|
||||
err.Error(), http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
v := b.String()
|
||||
|
||||
switch formname {
|
||||
case "channel_id":
|
||||
if c.Params.ChannelId != "" && c.Params.ChannelId != v {
|
||||
c.Err = model.NewAppError("uploadFileMultipart",
|
||||
"api.file.upload_file.multiple_channel_ids.app_error",
|
||||
nil, "", http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
if v != "" {
|
||||
c.Params.ChannelId = v
|
||||
}
|
||||
|
||||
// Got channel_id, re-process the entire post
|
||||
// in the streaming mode.
|
||||
if asStream == nil {
|
||||
return uploadFileMultipart(c, r, io.MultiReader(buf, r.Body), timestamp)
|
||||
}
|
||||
|
||||
case "client_ids":
|
||||
if !expectClientIds {
|
||||
c.SetInvalidParam("client_ids")
|
||||
return nil
|
||||
}
|
||||
clientIds = append(clientIds, v)
|
||||
|
||||
default:
|
||||
c.SetInvalidParam(formname)
|
||||
return nil
|
||||
}
|
||||
|
||||
continue NextPart
|
||||
}
|
||||
|
||||
// A file part.
|
||||
|
||||
if c.Params.ChannelId == "" && asStream == nil {
|
||||
// Got file before channel_id, fall back to legacy buffered mode
|
||||
mr, err = multipartReader(r, io.MultiReader(buf, r.Body))
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("uploadFileMultipart",
|
||||
"api.file.upload_file.read_request.app_error",
|
||||
nil, err.Error(), http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
|
||||
return uploadFileMultipartLegacy(c, mr, timestamp)
|
||||
}
|
||||
|
||||
c.RequireChannelId()
|
||||
if c.Err != nil {
|
||||
return nil
|
||||
}
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionUploadFile) {
|
||||
c.SetPermissionError(model.PermissionUploadFile)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If there's no clientIds when the first file comes, expect
|
||||
// none later.
|
||||
if nFiles == 0 && len(clientIds) == 0 {
|
||||
expectClientIds = false
|
||||
}
|
||||
|
||||
// Must have a exactly one client ID for each file.
|
||||
clientId := ""
|
||||
if expectClientIds {
|
||||
if nFiles >= len(clientIds) {
|
||||
c.SetInvalidParam("client_ids")
|
||||
return nil
|
||||
}
|
||||
|
||||
clientId = clientIds[nFiles]
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("uploadFileMultipart", audit.Fail)
|
||||
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
|
||||
audit.AddEventParameter(auditRec, "client_id", clientId)
|
||||
|
||||
info, appErr := c.App.UploadFileX(c.AppContext, c.Params.ChannelId, filename, part,
|
||||
app.UploadFileSetTeamId(FileTeamId),
|
||||
app.UploadFileSetUserId(c.AppContext.Session().UserId),
|
||||
app.UploadFileSetTimestamp(timestamp),
|
||||
app.UploadFileSetContentLength(-1),
|
||||
app.UploadFileSetClientId(clientId))
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
c.LogAuditRec(auditRec)
|
||||
return nil
|
||||
}
|
||||
audit.AddEventParameterAuditable(auditRec, "file", info)
|
||||
|
||||
auditRec.Success()
|
||||
c.LogAuditRec(auditRec)
|
||||
|
||||
// add to the response
|
||||
resp.FileInfos = append(resp.FileInfos, info)
|
||||
if expectClientIds {
|
||||
resp.ClientIds = append(resp.ClientIds, clientId)
|
||||
}
|
||||
|
||||
nFiles++
|
||||
}
|
||||
|
||||
// Verify that the number of ClientIds matched the number of files.
|
||||
if expectClientIds && len(clientIds) != nFiles {
|
||||
c.Err = model.NewAppError("uploadFileMultipart",
|
||||
"api.file.upload_file.incorrect_number_of_client_ids.app_error",
|
||||
map[string]any{"NumClientIds": len(clientIds), "NumFiles": nFiles},
|
||||
"", http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
|
||||
return &resp
|
||||
}
|
||||
|
||||
// uploadFileMultipartLegacy reads, buffers, and then uploads the message,
|
||||
// borrowing from http.ParseMultipartForm. If successful it returns a
|
||||
// *model.FileUploadResponse filled in with the individual model.FileInfo's.
|
||||
func uploadFileMultipartLegacy(c *Context, mr *multipart.Reader,
|
||||
timestamp time.Time) *model.FileUploadResponse {
|
||||
|
||||
// Parse the entire form.
|
||||
form, err := mr.ReadForm(*c.App.Config().FileSettings.MaxFileSize)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("uploadFileMultipartLegacy",
|
||||
"api.file.upload_file.read_request.app_error",
|
||||
nil, err.Error(), http.StatusInternalServerError)
|
||||
return nil
|
||||
}
|
||||
|
||||
// get and validate the channel Id, permission to upload there.
|
||||
if len(form.Value["channel_id"]) == 0 {
|
||||
c.SetInvalidParam("channel_id")
|
||||
return nil
|
||||
}
|
||||
channelId := form.Value["channel_id"][0]
|
||||
c.Params.ChannelId = channelId
|
||||
c.RequireChannelId()
|
||||
if c.Err != nil {
|
||||
return nil
|
||||
}
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelId, model.PermissionUploadFile) {
|
||||
c.SetPermissionError(model.PermissionUploadFile)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check that we have either no client IDs, or one per file.
|
||||
clientIds := form.Value["client_ids"]
|
||||
fileHeaders := form.File["files"]
|
||||
if len(clientIds) != 0 && len(clientIds) != len(fileHeaders) {
|
||||
c.Err = model.NewAppError("uploadFilesMultipartBuffered",
|
||||
"api.file.upload_file.incorrect_number_of_client_ids.app_error",
|
||||
map[string]any{"NumClientIds": len(clientIds), "NumFiles": len(fileHeaders)},
|
||||
"", http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
|
||||
resp := model.FileUploadResponse{
|
||||
FileInfos: []*model.FileInfo{},
|
||||
ClientIds: []string{},
|
||||
}
|
||||
|
||||
for i, fileHeader := range fileHeaders {
|
||||
f, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("uploadFileMultipartLegacy",
|
||||
"api.file.upload_file.read_request.app_error",
|
||||
nil, err.Error(), http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
|
||||
clientId := ""
|
||||
if len(clientIds) > 0 {
|
||||
clientId = clientIds[i]
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("uploadFileMultipartLegacy", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "channel_id", channelId)
|
||||
audit.AddEventParameter(auditRec, "client_id", clientId)
|
||||
|
||||
info, appErr := c.App.UploadFileX(c.AppContext, c.Params.ChannelId, fileHeader.Filename, f,
|
||||
app.UploadFileSetTeamId(FileTeamId),
|
||||
app.UploadFileSetUserId(c.AppContext.Session().UserId),
|
||||
app.UploadFileSetTimestamp(timestamp),
|
||||
app.UploadFileSetContentLength(-1),
|
||||
app.UploadFileSetClientId(clientId))
|
||||
f.Close()
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
c.LogAuditRec(auditRec)
|
||||
return nil
|
||||
}
|
||||
audit.AddEventParameterAuditable(auditRec, "file", info)
|
||||
|
||||
auditRec.Success()
|
||||
c.LogAuditRec(auditRec)
|
||||
|
||||
resp.FileInfos = append(resp.FileInfos, info)
|
||||
if clientId != "" {
|
||||
resp.ClientIds = append(resp.ClientIds, clientId)
|
||||
}
|
||||
}
|
||||
|
||||
return &resp
|
||||
}
|
||||
|
||||
func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireFileId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
forceDownload, _ := strconv.ParseBool(r.URL.Query().Get("download"))
|
||||
|
||||
auditRec := c.MakeAuditRecord("getFile", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "force_download", forceDownload)
|
||||
|
||||
info, err := c.App.GetFileInfo(c.Params.FileId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
setInaccessibleFileHeader(w, err)
|
||||
return
|
||||
}
|
||||
audit.AddEventParameterAuditable(auditRec, "file", info)
|
||||
|
||||
if info.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), info.PostId, model.PermissionReadChannel) {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
|
||||
fileReader, err := c.App.FileReader(info.Path)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
c.Err.StatusCode = http.StatusNotFound
|
||||
return
|
||||
}
|
||||
defer fileReader.Close()
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
web.WriteFileResponse(info.Name, info.MimeType, info.Size, time.Unix(0, info.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, forceDownload, w, r)
|
||||
}
|
||||
|
||||
func getFileThumbnail(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireFileId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
forceDownload, _ := strconv.ParseBool(r.URL.Query().Get("download"))
|
||||
info, err := c.App.GetFileInfo(c.Params.FileId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
setInaccessibleFileHeader(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if info.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), info.PostId, model.PermissionReadChannel) {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
|
||||
if info.ThumbnailPath == "" {
|
||||
c.Err = model.NewAppError("getFileThumbnail", "api.file.get_file_thumbnail.no_thumbnail.app_error", nil, "file_id="+info.Id, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
fileReader, err := c.App.FileReader(info.ThumbnailPath)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
c.Err.StatusCode = http.StatusNotFound
|
||||
return
|
||||
}
|
||||
defer fileReader.Close()
|
||||
|
||||
web.WriteFileResponse(info.Name, ThumbnailImageType, 0, time.Unix(0, info.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, forceDownload, w, r)
|
||||
}
|
||||
|
||||
func getFileLink(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireFileId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !*c.App.Config().FileSettings.EnablePublicLink {
|
||||
c.Err = model.NewAppError("getPublicLink", "api.file.get_public_link.disabled.app_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("getFileLink", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
info, err := c.App.GetFileInfo(c.Params.FileId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
setInaccessibleFileHeader(w, err)
|
||||
return
|
||||
}
|
||||
audit.AddEventParameterAuditable(auditRec, "file", info)
|
||||
|
||||
if info.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), info.PostId, model.PermissionReadChannel) {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
|
||||
if info.PostId == "" {
|
||||
c.Err = model.NewAppError("getPublicLink", "api.file.get_public_link.no_post.app_error", nil, "file_id="+info.Id, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
resp := make(map[string]string)
|
||||
link := c.App.GeneratePublicLink(c.GetSiteURLHeader(), info)
|
||||
resp["link"] = link
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
w.Write([]byte(model.MapToJSON(resp)))
|
||||
}
|
||||
|
||||
func getFilePreview(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireFileId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
forceDownload, _ := strconv.ParseBool(r.URL.Query().Get("download"))
|
||||
info, err := c.App.GetFileInfo(c.Params.FileId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
setInaccessibleFileHeader(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if info.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), info.PostId, model.PermissionReadChannel) {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
|
||||
if info.PreviewPath == "" {
|
||||
c.Err = model.NewAppError("getFilePreview", "api.file.get_file_preview.no_preview.app_error", nil, "file_id="+info.Id, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
fileReader, err := c.App.FileReader(info.PreviewPath)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
c.Err.StatusCode = http.StatusNotFound
|
||||
return
|
||||
}
|
||||
defer fileReader.Close()
|
||||
|
||||
web.WriteFileResponse(info.Name, PreviewImageType, 0, time.Unix(0, info.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, forceDownload, w, r)
|
||||
}
|
||||
|
||||
func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireFileId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
info, err := c.App.GetFileInfo(c.Params.FileId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
setInaccessibleFileHeader(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if info.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), info.PostId, model.PermissionReadChannel) {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", "max-age=2592000, private")
|
||||
if err := json.NewEncoder(w).Encode(info); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireFileId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !*c.App.Config().FileSettings.EnablePublicLink {
|
||||
c.Err = model.NewAppError("getPublicFile", "api.file.get_public_link.disabled.app_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
info, err := c.App.GetFileInfo(c.Params.FileId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
setInaccessibleFileHeader(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
hash := r.URL.Query().Get("h")
|
||||
|
||||
if hash == "" {
|
||||
c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest)
|
||||
utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey())
|
||||
return
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare([]byte(hash), []byte(app.GeneratePublicLinkHash(info.Id, *c.App.Config().FileSettings.PublicLinkSalt))) != 1 {
|
||||
c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest)
|
||||
utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey())
|
||||
return
|
||||
}
|
||||
|
||||
fileReader, err := c.App.FileReader(info.Path)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
c.Err.StatusCode = http.StatusNotFound
|
||||
return
|
||||
}
|
||||
defer fileReader.Close()
|
||||
|
||||
web.WriteFileResponse(info.Name, info.MimeType, info.Size, time.Unix(0, info.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, false, w, r)
|
||||
}
|
||||
|
||||
func searchFilesInTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireTeamId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
|
||||
c.SetPermissionError(model.PermissionViewTeam)
|
||||
return
|
||||
}
|
||||
|
||||
searchFiles(c, w, r, c.Params.TeamId)
|
||||
}
|
||||
|
||||
func searchFilesForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if c.App.Config().FeatureFlags.CommandPalette {
|
||||
searchFiles(c, w, r, "")
|
||||
}
|
||||
}
|
||||
|
||||
func searchFiles(c *Context, w http.ResponseWriter, r *http.Request, teamID string) {
|
||||
var params model.SearchParameter
|
||||
jsonErr := json.NewDecoder(r.Body).Decode(¶ms)
|
||||
if jsonErr != nil {
|
||||
c.Err = model.NewAppError("searchFiles", "api.post.search_files.invalid_body.app_error", nil, "", http.StatusBadRequest).Wrap(jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
if params.Terms == nil || *params.Terms == "" {
|
||||
c.SetInvalidParam("terms")
|
||||
return
|
||||
}
|
||||
terms := *params.Terms
|
||||
|
||||
timeZoneOffset := 0
|
||||
if params.TimeZoneOffset != nil {
|
||||
timeZoneOffset = *params.TimeZoneOffset
|
||||
}
|
||||
|
||||
isOrSearch := false
|
||||
if params.IsOrSearch != nil {
|
||||
isOrSearch = *params.IsOrSearch
|
||||
}
|
||||
|
||||
page := 0
|
||||
if params.Page != nil {
|
||||
page = *params.Page
|
||||
}
|
||||
|
||||
perPage := 60
|
||||
if params.PerPage != nil {
|
||||
perPage = *params.PerPage
|
||||
}
|
||||
|
||||
includeDeletedChannels := false
|
||||
if params.IncludeDeletedChannels != nil {
|
||||
includeDeletedChannels = *params.IncludeDeletedChannels
|
||||
}
|
||||
|
||||
modifier := ""
|
||||
if params.Modifier != nil {
|
||||
modifier = *params.Modifier
|
||||
}
|
||||
if modifier != "" && modifier != model.ModifierFiles && modifier != model.ModifierMessages {
|
||||
c.SetInvalidParam("modifier")
|
||||
return
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
results, err := c.App.SearchFilesInTeamForUser(c.AppContext, terms, c.AppContext.Session().UserId, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage, modifier)
|
||||
|
||||
elapsedTime := float64(time.Since(startTime)) / float64(time.Second)
|
||||
metrics := c.App.Metrics()
|
||||
if metrics != nil {
|
||||
metrics.IncrementFilesSearchCounter()
|
||||
metrics.ObserveFilesSearchDuration(elapsedTime)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
if err := json.NewEncoder(w).Encode(results); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func setInaccessibleFileHeader(w http.ResponseWriter, appErr *model.AppError) {
|
||||
// File is inaccessible due to cloud plan's limit.
|
||||
if appErr.Id == "app.file.cloud.get.app_error" {
|
||||
w.Header().Set(model.HeaderFirstInaccessibleFileTime, "1")
|
||||
}
|
||||
}
|
||||
1267
api4/file_test.go
1267
api4/file_test.go
File diff suppressed because it is too large
Load diff
192
api4/graphql.go
192
api4/graphql.go
|
|
@ -1,192 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/graph-gophers/dataloader/v6"
|
||||
graphql "github.com/graph-gophers/graphql-go"
|
||||
gqlerrors "github.com/graph-gophers/graphql-go/errors"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/web"
|
||||
)
|
||||
|
||||
type graphQLInput struct {
|
||||
Query string `json:"query"`
|
||||
OperationName string `json:"operationName"`
|
||||
Variables map[string]any `json:"variables"`
|
||||
}
|
||||
|
||||
// Unique type to hold our context.
|
||||
type ctxKey int
|
||||
|
||||
const (
|
||||
webCtx ctxKey = 0
|
||||
rolesLoaderCtx ctxKey = 1
|
||||
channelsLoaderCtx ctxKey = 2
|
||||
teamsLoaderCtx ctxKey = 3
|
||||
usersLoaderCtx ctxKey = 4
|
||||
)
|
||||
|
||||
const loaderBatchCapacity = web.PerPageMaximum
|
||||
|
||||
//go:embed schema.graphqls
|
||||
var schemaRaw string
|
||||
|
||||
func (api *API) InitGraphQL() error {
|
||||
// Guard with a feature flag.
|
||||
if !api.srv.Config().FeatureFlags.GraphQL {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
opts := []graphql.SchemaOpt{
|
||||
graphql.UseFieldResolvers(),
|
||||
graphql.Logger(mlog.NewGraphQLLogger(api.srv.Log())),
|
||||
graphql.MaxParallelism(loaderBatchCapacity), // This is dangerous if the query
|
||||
// uses any non-dataloader backed object. So we need to be a bit careful here.
|
||||
}
|
||||
|
||||
if isProd() {
|
||||
opts = append(opts,
|
||||
// MaxDepth cannot be moved as a general param
|
||||
// because otherwise introspection also doesn't work
|
||||
// with just a depth of 4.
|
||||
graphql.MaxDepth(4),
|
||||
graphql.DisableIntrospection(),
|
||||
)
|
||||
}
|
||||
|
||||
api.schema, err = graphql.ParseSchema(schemaRaw, &resolver{}, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
api.BaseRoutes.APIRoot5.Handle("/graphql", api.APIHandlerTrustRequester(graphiQL)).Methods("GET")
|
||||
api.BaseRoutes.APIRoot5.Handle("/graphql", api.APISessionRequired(api.graphQL)).Methods("POST")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) graphQL(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var response *graphql.Response
|
||||
defer func() {
|
||||
if response != nil {
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Limit bodies to 100KiB.
|
||||
// We need to enforce a lower limit than the file upload size,
|
||||
// to prevent the library doing unnecessary parsing.
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 102400)
|
||||
|
||||
var params graphQLInput
|
||||
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
||||
err2 := gqlerrors.Errorf("invalid request body: %v", err)
|
||||
response = &graphql.Response{Errors: []*gqlerrors.QueryError{err2}}
|
||||
return
|
||||
}
|
||||
|
||||
if isProd() && params.OperationName == "" {
|
||||
err2 := gqlerrors.Errorf("operation name not passed")
|
||||
response = &graphql.Response{Errors: []*gqlerrors.QueryError{err2}}
|
||||
return
|
||||
}
|
||||
|
||||
c.GraphQLOperationName = params.OperationName
|
||||
|
||||
// Populate the context with required info.
|
||||
reqCtx := r.Context()
|
||||
reqCtx = context.WithValue(reqCtx, webCtx, c)
|
||||
|
||||
rolesLoader := dataloader.NewBatchedLoader(graphQLRolesLoader, dataloader.WithBatchCapacity(loaderBatchCapacity))
|
||||
reqCtx = context.WithValue(reqCtx, rolesLoaderCtx, rolesLoader)
|
||||
|
||||
channelsLoader := dataloader.NewBatchedLoader(graphQLChannelsLoader, dataloader.WithBatchCapacity(loaderBatchCapacity))
|
||||
reqCtx = context.WithValue(reqCtx, channelsLoaderCtx, channelsLoader)
|
||||
|
||||
teamsLoader := dataloader.NewBatchedLoader(graphQLTeamsLoader, dataloader.WithBatchCapacity(loaderBatchCapacity))
|
||||
reqCtx = context.WithValue(reqCtx, teamsLoaderCtx, teamsLoader)
|
||||
|
||||
usersLoader := dataloader.NewBatchedLoader(graphQLUsersLoader, dataloader.WithBatchCapacity(loaderBatchCapacity))
|
||||
reqCtx = context.WithValue(reqCtx, usersLoaderCtx, usersLoader)
|
||||
|
||||
response = api.schema.Exec(reqCtx,
|
||||
params.Query,
|
||||
params.OperationName,
|
||||
params.Variables)
|
||||
|
||||
if len(response.Errors) > 0 {
|
||||
logFunc := mlog.Error
|
||||
for _, gqlErr := range response.Errors {
|
||||
if gqlErr.Err != nil {
|
||||
if appErr, ok := gqlErr.Err.(*model.AppError); ok && appErr.StatusCode < http.StatusInternalServerError {
|
||||
logFunc = mlog.Debug
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
logFunc("Error executing request", mlog.String("operation", params.OperationName),
|
||||
mlog.Array("errors", response.Errors))
|
||||
}
|
||||
}
|
||||
|
||||
func graphiQL(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write(graphiqlPage)
|
||||
}
|
||||
|
||||
var graphiqlPage = []byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>GraphiQL editor | Mattermost</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.css" integrity="sha256-gSgd+on4bTXigueyd/NSRNAy4cBY42RAVNaXnQDjOW8=" crossorigin="anonymous"/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js" integrity="sha256-OI3N9zCKabDov2rZFzl8lJUXCcP7EmsGcGoP6DMXQCo=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/2.0.3/fetch.min.js" integrity="sha256-aB35laj7IZhLTx58xw/Gm1EKOoJJKZt6RY+bH1ReHxs=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.production.min.js" integrity="sha256-wouRkivKKXA3y6AuyFwcDcF50alCNV8LbghfYCH6Z98=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.production.min.js" integrity="sha256-9hrJxD4IQsWHdNpzLkJKYGiY/SEZFJJSUqyeZPNKd8g=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.js" integrity="sha256-oeWyQyKKUurcnbFRsfeSgrdOpXXiRYopnPjTVZ+6UmI=" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body style="width: 100%; height: 100%; margin: 0; overflow: hidden;">
|
||||
<div id="graphiql" style="height: 100vh;">Loading...</div>
|
||||
<script>
|
||||
function graphQLFetcher(graphQLParams) {
|
||||
return fetch("/api/v5/graphql", {
|
||||
method: "post",
|
||||
body: JSON.stringify(graphQLParams),
|
||||
credentials: "include",
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
}).then(function (response) {
|
||||
return response.text();
|
||||
}).then(function (responseBody) {
|
||||
try {
|
||||
return JSON.parse(responseBody);
|
||||
} catch (error) {
|
||||
return responseBody;
|
||||
}
|
||||
});
|
||||
}
|
||||
ReactDOM.render(
|
||||
React.createElement(GraphiQL, {fetcher: graphQLFetcher}),
|
||||
document.getElementById("graphiql")
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
|
||||
// isProd is a helper function to apply prod-specific graphQL validations.
|
||||
func isProd() bool {
|
||||
return model.BuildNumber != "dev"
|
||||
}
|
||||
1351
api4/group.go
1351
api4/group.go
File diff suppressed because it is too large
Load diff
226
api4/handlers.go
226
api4/handlers.go
|
|
@ -1,226 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/gziphandler"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/web"
|
||||
)
|
||||
|
||||
type Context = web.Context
|
||||
|
||||
type handlerFunc func(*Context, http.ResponseWriter, *http.Request)
|
||||
|
||||
// APIHandler provides a handler for API endpoints which do not require the user to be logged in order for access to be
|
||||
// granted.
|
||||
func (api *API) APIHandler(h handlerFunc) http.Handler {
|
||||
handler := &web.Handler{
|
||||
Srv: api.srv,
|
||||
HandleFunc: h,
|
||||
HandlerName: web.GetHandlerName(h),
|
||||
RequireSession: false,
|
||||
TrustRequester: false,
|
||||
RequireMfa: false,
|
||||
IsStatic: false,
|
||||
IsLocal: false,
|
||||
}
|
||||
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
|
||||
return gziphandler.GzipHandler(handler)
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
// APISessionRequired provides a handler for API endpoints which require the user to be logged in in order for access to
|
||||
// be granted.
|
||||
func (api *API) APISessionRequired(h handlerFunc) http.Handler {
|
||||
handler := &web.Handler{
|
||||
Srv: api.srv,
|
||||
HandleFunc: h,
|
||||
HandlerName: web.GetHandlerName(h),
|
||||
RequireSession: true,
|
||||
TrustRequester: false,
|
||||
RequireMfa: true,
|
||||
IsStatic: false,
|
||||
IsLocal: false,
|
||||
}
|
||||
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
|
||||
return gziphandler.GzipHandler(handler)
|
||||
}
|
||||
return handler
|
||||
|
||||
}
|
||||
|
||||
// CloudAPIKeyRequired provides a handler for webhook endpoints to access Cloud installations from CWS
|
||||
func (api *API) CloudAPIKeyRequired(h handlerFunc) http.Handler {
|
||||
handler := &web.Handler{
|
||||
Srv: api.srv,
|
||||
HandleFunc: h,
|
||||
HandlerName: web.GetHandlerName(h),
|
||||
RequireSession: false,
|
||||
RequireCloudKey: true,
|
||||
TrustRequester: false,
|
||||
RequireMfa: false,
|
||||
IsStatic: false,
|
||||
IsLocal: false,
|
||||
}
|
||||
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
|
||||
return gziphandler.GzipHandler(handler)
|
||||
}
|
||||
return handler
|
||||
|
||||
}
|
||||
|
||||
// RemoteClusterTokenRequired provides a handler for remote cluster requests to /remotecluster endpoints.
|
||||
func (api *API) RemoteClusterTokenRequired(h handlerFunc) http.Handler {
|
||||
handler := &web.Handler{
|
||||
Srv: api.srv,
|
||||
HandleFunc: h,
|
||||
HandlerName: web.GetHandlerName(h),
|
||||
RequireSession: false,
|
||||
RequireCloudKey: false,
|
||||
RequireRemoteClusterToken: true,
|
||||
TrustRequester: false,
|
||||
RequireMfa: false,
|
||||
IsStatic: false,
|
||||
IsLocal: false,
|
||||
}
|
||||
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
|
||||
return gziphandler.GzipHandler(handler)
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
// APISessionRequiredMfa provides a handler for API endpoints which require a logged-in user session but when accessed,
|
||||
// if MFA is enabled, the MFA process is not yet complete, and therefore the requirement to have completed the MFA
|
||||
// authentication must be waived.
|
||||
func (api *API) APISessionRequiredMfa(h handlerFunc) http.Handler {
|
||||
handler := &web.Handler{
|
||||
Srv: api.srv,
|
||||
HandleFunc: h,
|
||||
HandlerName: web.GetHandlerName(h),
|
||||
RequireSession: true,
|
||||
TrustRequester: false,
|
||||
RequireMfa: false,
|
||||
IsStatic: false,
|
||||
IsLocal: false,
|
||||
}
|
||||
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
|
||||
return gziphandler.GzipHandler(handler)
|
||||
}
|
||||
return handler
|
||||
|
||||
}
|
||||
|
||||
// APIHandlerTrustRequester provides a handler for API endpoints which do not require the user to be logged in and are
|
||||
// allowed to be requested directly rather than via javascript/XMLHttpRequest, such as site branding images or the
|
||||
// websocket.
|
||||
func (api *API) APIHandlerTrustRequester(h handlerFunc) http.Handler {
|
||||
handler := &web.Handler{
|
||||
Srv: api.srv,
|
||||
HandleFunc: h,
|
||||
HandlerName: web.GetHandlerName(h),
|
||||
RequireSession: false,
|
||||
TrustRequester: true,
|
||||
RequireMfa: false,
|
||||
IsStatic: false,
|
||||
IsLocal: false,
|
||||
}
|
||||
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
|
||||
return gziphandler.GzipHandler(handler)
|
||||
}
|
||||
return handler
|
||||
|
||||
}
|
||||
|
||||
// APISessionRequiredTrustRequester provides a handler for API endpoints which do require the user to be logged in and
|
||||
// are allowed to be requested directly rather than via javascript/XMLHttpRequest, such as emoji or file uploads.
|
||||
func (api *API) APISessionRequiredTrustRequester(h handlerFunc) http.Handler {
|
||||
handler := &web.Handler{
|
||||
Srv: api.srv,
|
||||
HandleFunc: h,
|
||||
HandlerName: web.GetHandlerName(h),
|
||||
RequireSession: true,
|
||||
TrustRequester: true,
|
||||
RequireMfa: true,
|
||||
IsStatic: false,
|
||||
IsLocal: false,
|
||||
}
|
||||
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
|
||||
return gziphandler.GzipHandler(handler)
|
||||
}
|
||||
return handler
|
||||
|
||||
}
|
||||
|
||||
// DisableWhenBusy provides a handler for API endpoints which should be disabled when the server is under load,
|
||||
// responding with HTTP 503 (Service Unavailable).
|
||||
func (api *API) APISessionRequiredDisableWhenBusy(h handlerFunc) http.Handler {
|
||||
handler := &web.Handler{
|
||||
Srv: api.srv,
|
||||
HandleFunc: h,
|
||||
HandlerName: web.GetHandlerName(h),
|
||||
RequireSession: true,
|
||||
TrustRequester: false,
|
||||
RequireMfa: false,
|
||||
IsStatic: false,
|
||||
IsLocal: false,
|
||||
DisableWhenBusy: true,
|
||||
}
|
||||
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
|
||||
return gziphandler.GzipHandler(handler)
|
||||
}
|
||||
return handler
|
||||
|
||||
}
|
||||
|
||||
// APILocal provides a handler for API endpoints to be used in local
|
||||
// mode, this is, through a UNIX socket and without an authenticated
|
||||
// session, but with one that has no user set and no permission
|
||||
// restrictions
|
||||
func (api *API) APILocal(h handlerFunc) http.Handler {
|
||||
handler := &web.Handler{
|
||||
Srv: api.srv,
|
||||
HandleFunc: h,
|
||||
HandlerName: web.GetHandlerName(h),
|
||||
RequireSession: false,
|
||||
TrustRequester: false,
|
||||
RequireMfa: false,
|
||||
IsStatic: false,
|
||||
IsLocal: true,
|
||||
}
|
||||
|
||||
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
|
||||
return gziphandler.GzipHandler(handler)
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
func requireLicense(c *Context) *model.AppError {
|
||||
if c.App.Channels().License() == nil {
|
||||
err := model.NewAppError("", "api.license_error", nil, "", http.StatusNotImplemented)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func minimumProfessionalLicense(c *Context) *model.AppError {
|
||||
lic := c.App.Srv().License()
|
||||
if lic == nil || (lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise) {
|
||||
err := model.NewAppError("", model.NoTranslation, nil, "license is neither professional nor enterprise", http.StatusNotImplemented)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func rejectGuests(c *Context) *model.AppError {
|
||||
if c.AppContext.Session().Props[model.SessionPropIsGuest] == "true" {
|
||||
err := model.NewAppError("", model.NoTranslation, nil, "insufficient permissions as a guest user", http.StatusNotImplemented)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitImport() {
|
||||
api.BaseRoutes.Imports.Handle("", api.APISessionRequired(listImports)).Methods("GET")
|
||||
}
|
||||
|
||||
func listImports(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.IsSystemAdmin() {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
imports, appErr := c.App.ListImports()
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(imports); err != nil {
|
||||
c.Logger.Warn("Error writing imports", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/utils/fileutils"
|
||||
)
|
||||
|
||||
func TestListImports(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
testsDir, _ := fileutils.FindDir("tests")
|
||||
require.NotEmpty(t, testsDir)
|
||||
|
||||
uploadNewImport := func(c *model.Client4, t *testing.T) string {
|
||||
file, err := os.Open(testsDir + "/import_test.zip")
|
||||
require.NoError(t, err)
|
||||
|
||||
info, err := file.Stat()
|
||||
require.NoError(t, err)
|
||||
|
||||
us := &model.UploadSession{
|
||||
Filename: info.Name(),
|
||||
FileSize: info.Size(),
|
||||
Type: model.UploadTypeImport,
|
||||
}
|
||||
|
||||
if c == th.LocalClient {
|
||||
us.UserId = model.UploadNoUserID
|
||||
}
|
||||
|
||||
u, _, err := c.CreateUpload(us)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, u)
|
||||
|
||||
finfo, _, err := c.UploadData(u.Id, file)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, finfo)
|
||||
|
||||
return u.Id
|
||||
}
|
||||
|
||||
t.Run("no permissions", func(t *testing.T) {
|
||||
imports, _, err := th.Client.ListImports()
|
||||
require.Error(t, err)
|
||||
CheckErrorID(t, err, "api.context.permissions.app_error")
|
||||
require.Nil(t, imports)
|
||||
})
|
||||
|
||||
dataDir, found := fileutils.FindDir("data")
|
||||
require.True(t, found)
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
|
||||
imports, _, err := c.ListImports()
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, imports)
|
||||
}, "no imports")
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
|
||||
id := uploadNewImport(c, t)
|
||||
id2 := uploadNewImport(c, t)
|
||||
|
||||
importDir := filepath.Join(dataDir, "import")
|
||||
f, err := os.Create(filepath.Join(importDir, "import.zip.tmp"))
|
||||
require.NoError(t, err)
|
||||
f.Close()
|
||||
|
||||
imports, _, err := c.ListImports()
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, imports)
|
||||
require.Len(t, imports, 2)
|
||||
require.Contains(t, imports, id+"_import_test.zip")
|
||||
require.Contains(t, imports, id2+"_import_test.zip")
|
||||
|
||||
require.NoError(t, os.RemoveAll(importDir))
|
||||
}, "expected imports")
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ImportSettings.Directory = "import_new" })
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ImportSettings.Directory = "import" })
|
||||
|
||||
importDir := filepath.Join(dataDir, "import_new")
|
||||
|
||||
imports, _, err := c.ListImports()
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, imports)
|
||||
|
||||
id := uploadNewImport(c, t)
|
||||
imports, _, err = c.ListImports()
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, imports)
|
||||
require.Len(t, imports, 1)
|
||||
require.Equal(t, id+"_import_test.zip", imports[0])
|
||||
|
||||
require.NoError(t, os.RemoveAll(importDir))
|
||||
}, "change import directory")
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,130 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitAction() {
|
||||
api.BaseRoutes.Post.Handle("/actions/{action_id:[A-Za-z0-9]+}", api.APISessionRequired(doPostAction)).Methods("POST")
|
||||
|
||||
api.BaseRoutes.APIRoot.Handle("/actions/dialogs/open", api.APIHandler(openDialog)).Methods("POST")
|
||||
api.BaseRoutes.APIRoot.Handle("/actions/dialogs/submit", api.APISessionRequired(submitDialog)).Methods("POST")
|
||||
}
|
||||
|
||||
func doPostAction(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequirePostId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var actionRequest model.DoPostActionRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&actionRequest)
|
||||
if err != nil {
|
||||
c.Logger.Warn("Error decoding the action request", mlog.Err(err))
|
||||
}
|
||||
|
||||
var cookie *model.PostActionCookie
|
||||
if actionRequest.Cookie != "" {
|
||||
cookie = &model.PostActionCookie{}
|
||||
cookieStr := ""
|
||||
cookieStr, err = model.DecryptPostActionCookie(actionRequest.Cookie, c.App.PostActionCookieSecret())
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("DoPostAction", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal([]byte(cookieStr), &cookie)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("DoPostAction", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), cookie.ChannelId, model.PermissionReadChannel) {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var appErr *model.AppError
|
||||
resp := &model.PostActionAPIResponse{Status: "OK"}
|
||||
|
||||
resp.TriggerId, appErr = c.App.DoPostActionWithCookie(c.AppContext, c.Params.PostId, c.Params.ActionId, c.AppContext.Session().UserId,
|
||||
actionRequest.SelectedOption, cookie)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
c.Logger.Warn("Error writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func openDialog(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var dialog model.OpenDialogRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&dialog)
|
||||
if err != nil {
|
||||
c.SetInvalidParamWithErr("dialog", err)
|
||||
return
|
||||
}
|
||||
|
||||
if dialog.URL == "" {
|
||||
c.SetInvalidParam("url")
|
||||
return
|
||||
}
|
||||
|
||||
if appErr := c.App.OpenInteractiveDialog(dialog); appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func submitDialog(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var submit model.SubmitDialogRequest
|
||||
|
||||
jsonErr := json.NewDecoder(r.Body).Decode(&submit)
|
||||
if jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("dialog", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
if submit.URL == "" {
|
||||
c.SetInvalidParam("url")
|
||||
return
|
||||
}
|
||||
|
||||
submit.UserId = c.AppContext.Session().UserId
|
||||
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), submit.ChannelId, model.PermissionReadChannel) {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), submit.TeamId, model.PermissionViewTeam) {
|
||||
c.SetPermissionError(model.PermissionViewTeam)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := c.App.SubmitInteractiveDialog(c.AppContext, submit)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
b, _ := json.Marshal(resp)
|
||||
|
||||
w.Write(b)
|
||||
}
|
||||
250
api4/job.go
250
api4/job.go
|
|
@ -1,250 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/web"
|
||||
)
|
||||
|
||||
func (api *API) InitJob() {
|
||||
api.BaseRoutes.Jobs.Handle("", api.APISessionRequired(getJobs)).Methods("GET")
|
||||
api.BaseRoutes.Jobs.Handle("", api.APISessionRequired(createJob)).Methods("POST")
|
||||
api.BaseRoutes.Jobs.Handle("/{job_id:[A-Za-z0-9]+}", api.APISessionRequired(getJob)).Methods("GET")
|
||||
api.BaseRoutes.Jobs.Handle("/{job_id:[A-Za-z0-9]+}/download", api.APISessionRequiredTrustRequester(downloadJob)).Methods("GET")
|
||||
api.BaseRoutes.Jobs.Handle("/{job_id:[A-Za-z0-9]+}/cancel", api.APISessionRequired(cancelJob)).Methods("POST")
|
||||
api.BaseRoutes.Jobs.Handle("/type/{job_type:[A-Za-z0-9_-]+}", api.APISessionRequired(getJobsByType)).Methods("GET")
|
||||
}
|
||||
|
||||
func getJob(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireJobId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
job, err := c.App.GetJob(c.Params.JobId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
hasPermission, permissionRequired := c.App.SessionHasPermissionToReadJob(*c.AppContext.Session(), job.Type)
|
||||
if permissionRequired == nil {
|
||||
c.Err = model.NewAppError("getJob", "api.job.retrieve.nopermissions", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !hasPermission {
|
||||
c.SetPermissionError(permissionRequired)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(job); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func downloadJob(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
config := c.App.Config()
|
||||
const FilePath = "export"
|
||||
const FileMime = "application/zip"
|
||||
|
||||
c.RequireJobId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !*config.MessageExportSettings.DownloadExportResults {
|
||||
c.Err = model.NewAppError("downloadExportResultsNotEnabled", "app.job.download_export_results_not_enabled", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
job, err := c.App.GetJob(c.Params.JobId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
// Currently, this endpoint only supports downloading the compliance report.
|
||||
// If you need to download another job type, you will need to alter this section of the code to accommodate it.
|
||||
if job.Type == model.JobTypeMessageExport && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionDownloadComplianceExportResult) {
|
||||
c.SetPermissionError(model.PermissionDownloadComplianceExportResult)
|
||||
return
|
||||
} else if job.Type != model.JobTypeMessageExport {
|
||||
c.Err = model.NewAppError("unableToDownloadJob", "api.job.unable_to_download_job.incorrect_job_type", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
isDownloadable, _ := strconv.ParseBool(job.Data["is_downloadable"])
|
||||
if !isDownloadable {
|
||||
c.Err = model.NewAppError("unableToDownloadJob", "api.job.unable_to_download_job", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
fileName := job.Id + ".zip"
|
||||
filePath := filepath.Join(FilePath, fileName)
|
||||
fileReader, err := c.App.FileReader(filePath)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
c.Err.StatusCode = http.StatusNotFound
|
||||
return
|
||||
}
|
||||
defer fileReader.Close()
|
||||
|
||||
// We are able to pass 0 for content size due to the fact that Golang's serveContent (https://golang.org/src/net/http/fs.go)
|
||||
// already sets that for us
|
||||
web.WriteFileResponse(fileName, FileMime, 0, time.Unix(0, job.LastActivityAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, true, w, r)
|
||||
}
|
||||
|
||||
func createJob(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var job model.Job
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&job); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("job", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("createJob", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameterAuditable(auditRec, "job", &job)
|
||||
|
||||
hasPermission, permissionRequired := c.App.SessionHasPermissionToCreateJob(*c.AppContext.Session(), &job)
|
||||
if permissionRequired == nil {
|
||||
c.Err = model.NewAppError("unableToCreateJob", "api.job.unable_to_create_job.incorrect_job_type", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if !hasPermission {
|
||||
c.SetPermissionError(permissionRequired)
|
||||
return
|
||||
}
|
||||
|
||||
rjob, err := c.App.CreateJob(&job)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(rjob)
|
||||
auditRec.AddEventObjectType("job")
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(rjob); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getJobs(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var validJobTypes []string
|
||||
for _, jobType := range model.AllJobTypes {
|
||||
hasPermission, permissionRequired := c.App.SessionHasPermissionToReadJob(*c.AppContext.Session(), jobType)
|
||||
if permissionRequired == nil {
|
||||
mlog.Warn("The job types of a job you are trying to retrieve does not contain permissions", mlog.String("jobType", jobType))
|
||||
continue
|
||||
}
|
||||
if hasPermission {
|
||||
validJobTypes = append(validJobTypes, jobType)
|
||||
}
|
||||
}
|
||||
if len(validJobTypes) == 0 {
|
||||
c.SetPermissionError()
|
||||
return
|
||||
}
|
||||
|
||||
jobs, appErr := c.App.GetJobsByTypesPage(validJobTypes, c.Params.Page, c.Params.PerPage)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(jobs)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getJobs", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func getJobsByType(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireJobType()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hasPermission, permissionRequired := c.App.SessionHasPermissionToReadJob(*c.AppContext.Session(), c.Params.JobType)
|
||||
if permissionRequired == nil {
|
||||
c.Err = model.NewAppError("getJobsByType", "api.job.retrieve.nopermissions", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !hasPermission {
|
||||
c.SetPermissionError(permissionRequired)
|
||||
return
|
||||
}
|
||||
|
||||
jobs, appErr := c.App.GetJobsByTypePage(c.Params.JobType, c.Params.Page, c.Params.PerPage)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(jobs)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getJobsByType", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func cancelJob(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireJobId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("cancelJob", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "job_id", c.Params.JobId)
|
||||
|
||||
job, err := c.App.GetJob(c.Params.JobId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventPriorState(job)
|
||||
auditRec.AddEventObjectType("job")
|
||||
|
||||
// if permission to create, permission to cancel, same permission
|
||||
hasPermission, permissionRequired := c.App.SessionHasPermissionToCreateJob(*c.AppContext.Session(), job)
|
||||
if permissionRequired == nil {
|
||||
c.Err = model.NewAppError("unableToCancelJob", "api.job.unable_to_create_job.incorrect_job_type", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if !hasPermission {
|
||||
c.SetPermissionError(permissionRequired)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.CancelJob(c.Params.JobId); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
453
api4/ldap.go
453
api4/ldap.go
|
|
@ -1,453 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
type mixedUnlinkedGroup struct {
|
||||
Id *string `json:"mattermost_group_id"`
|
||||
DisplayName string `json:"name"`
|
||||
RemoteId string `json:"primary_key"`
|
||||
HasSyncables *bool `json:"has_syncables"`
|
||||
}
|
||||
|
||||
func (api *API) InitLdap() {
|
||||
api.BaseRoutes.LDAP.Handle("/sync", api.APISessionRequired(syncLdap)).Methods("POST")
|
||||
api.BaseRoutes.LDAP.Handle("/test", api.APISessionRequired(testLdap)).Methods("POST")
|
||||
api.BaseRoutes.LDAP.Handle("/migrateid", api.APISessionRequired(migrateIdLdap)).Methods("POST")
|
||||
|
||||
// GET /api/v4/ldap/groups?page=0&per_page=1000
|
||||
api.BaseRoutes.LDAP.Handle("/groups", api.APISessionRequired(getLdapGroups)).Methods("GET")
|
||||
|
||||
// POST /api/v4/ldap/groups/:remote_id/link
|
||||
api.BaseRoutes.LDAP.Handle(`/groups/{remote_id}/link`, api.APISessionRequired(linkLdapGroup)).Methods("POST")
|
||||
|
||||
// DELETE /api/v4/ldap/groups/:remote_id/link
|
||||
api.BaseRoutes.LDAP.Handle(`/groups/{remote_id}/link`, api.APISessionRequired(unlinkLdapGroup)).Methods("DELETE")
|
||||
|
||||
api.BaseRoutes.LDAP.Handle("/certificate/public", api.APISessionRequired(addLdapPublicCertificate)).Methods("POST")
|
||||
api.BaseRoutes.LDAP.Handle("/certificate/private", api.APISessionRequired(addLdapPrivateCertificate)).Methods("POST")
|
||||
|
||||
api.BaseRoutes.LDAP.Handle("/certificate/public", api.APISessionRequired(removeLdapPublicCertificate)).Methods("DELETE")
|
||||
api.BaseRoutes.LDAP.Handle("/certificate/private", api.APISessionRequired(removeLdapPrivateCertificate)).Methods("DELETE")
|
||||
|
||||
api.BaseRoutes.LDAP.Handle("/users/{user_id}/group_sync_memberships", api.APISessionRequired(addUserToGroupSyncables)).Methods("POST")
|
||||
}
|
||||
|
||||
func syncLdap(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAP {
|
||||
c.Err = model.NewAppError("Api4.syncLdap", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
type LdapSyncOptions struct {
|
||||
IncludeRemovedMembers bool `json:"include_removed_members"`
|
||||
}
|
||||
var opts LdapSyncOptions
|
||||
err := json.NewDecoder(r.Body).Decode(&opts)
|
||||
if err != nil {
|
||||
c.Logger.Warn("Error decoding LDAP sync options", mlog.Err(err))
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("syncLdap", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateLdapSyncJob) {
|
||||
c.SetPermissionError(model.PermissionCreateLdapSyncJob)
|
||||
return
|
||||
}
|
||||
|
||||
c.App.SyncLdap(opts.IncludeRemovedMembers)
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func testLdap(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAP {
|
||||
c.Err = model.NewAppError("Api4.testLdap", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionTestLdap) {
|
||||
c.SetPermissionError(model.PermissionTestLdap)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.TestLdap(); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func getLdapGroups(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementGroups)
|
||||
return
|
||||
}
|
||||
|
||||
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
|
||||
c.Err = model.NewAppError("Api4.getLdapGroups", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
opts := model.LdapGroupSearchOpts{
|
||||
Q: c.Params.Q,
|
||||
}
|
||||
if c.Params.IsLinked != nil {
|
||||
opts.IsLinked = c.Params.IsLinked
|
||||
}
|
||||
if c.Params.IsConfigured != nil {
|
||||
opts.IsConfigured = c.Params.IsConfigured
|
||||
}
|
||||
|
||||
groups, total, appErr := c.App.GetAllLdapGroupsPage(c.Params.Page, c.Params.PerPage, opts)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
mugs := []*mixedUnlinkedGroup{}
|
||||
for _, group := range groups {
|
||||
mug := &mixedUnlinkedGroup{
|
||||
DisplayName: group.DisplayName,
|
||||
RemoteId: group.GetRemoteId(),
|
||||
}
|
||||
if len(group.Id) == 26 {
|
||||
mug.Id = &group.Id
|
||||
mug.HasSyncables = &group.HasSyncables
|
||||
}
|
||||
mugs = append(mugs, mug)
|
||||
}
|
||||
|
||||
b, err := json.Marshal(struct {
|
||||
Count int `json:"count"`
|
||||
Groups []*mixedUnlinkedGroup `json:"groups"`
|
||||
}{Count: total, Groups: mugs})
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getLdapGroups", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func linkLdapGroup(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireRemoteId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementGroups) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementGroups)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("linkLdapGroup", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "remote_id", c.Params.RemoteId)
|
||||
|
||||
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
|
||||
c.Err = model.NewAppError("Api4.linkLdapGroup", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
ldapGroup, appErr := c.App.GetLdapGroup(c.Params.RemoteId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if ldapGroup == nil {
|
||||
c.Err = model.NewAppError("Api4.linkLdapGroup", "api.ldap_group.not_found", nil, "", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
group, appErr := c.App.GetGroupByRemoteID(ldapGroup.GetRemoteId(), model.GroupSourceLdap)
|
||||
if appErr != nil && appErr.Id != "app.group.no_rows" {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
if group != nil {
|
||||
audit.AddEventParameterAuditable(auditRec, "group", group)
|
||||
}
|
||||
|
||||
var status int
|
||||
var newOrUpdatedGroup *model.Group
|
||||
|
||||
// Truncate display name if necessary
|
||||
var displayName string
|
||||
if len(ldapGroup.DisplayName) > model.GroupDisplayNameMaxLength {
|
||||
displayName = ldapGroup.DisplayName[:model.GroupDisplayNameMaxLength]
|
||||
} else {
|
||||
displayName = ldapGroup.DisplayName
|
||||
}
|
||||
|
||||
// Group has been previously linked
|
||||
if group != nil {
|
||||
if group.DeleteAt == 0 {
|
||||
newOrUpdatedGroup = group
|
||||
} else {
|
||||
group.DeleteAt = 0
|
||||
group.DisplayName = displayName
|
||||
group.RemoteId = ldapGroup.RemoteId
|
||||
newOrUpdatedGroup, appErr = c.App.UpdateGroup(group)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
auditRec.AddEventResultState(newOrUpdatedGroup)
|
||||
auditRec.AddEventObjectType("group")
|
||||
}
|
||||
status = http.StatusOK
|
||||
} else {
|
||||
// Group has never been linked
|
||||
//
|
||||
// For group mentions implementation, the Name column will no longer be set by default.
|
||||
// Instead it will be set and saved in the web app when Group Mentions is enabled.
|
||||
newGroup := &model.Group{
|
||||
DisplayName: displayName,
|
||||
RemoteId: ldapGroup.RemoteId,
|
||||
Source: model.GroupSourceLdap,
|
||||
}
|
||||
newOrUpdatedGroup, appErr = c.App.CreateGroup(newGroup)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
auditRec.AddEventResultState(newOrUpdatedGroup)
|
||||
auditRec.AddEventObjectType("group")
|
||||
status = http.StatusCreated
|
||||
}
|
||||
|
||||
b, err := json.Marshal(newOrUpdatedGroup)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.linkLdapGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
w.WriteHeader(status)
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func unlinkLdapGroup(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireRemoteId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("unlinkLdapGroup", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "remote_id", c.Params.RemoteId)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementGroups) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementGroups)
|
||||
return
|
||||
}
|
||||
|
||||
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
|
||||
c.Err = model.NewAppError("Api4.unlinkLdapGroup", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
group, err := c.App.GetGroupByRemoteID(c.Params.RemoteId, model.GroupSourceLdap)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
auditRec.AddEventPriorState(group)
|
||||
auditRec.AddEventObjectType("group")
|
||||
|
||||
if group.DeleteAt == 0 {
|
||||
deletedGroup, err := c.App.DeleteGroup(group.Id)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
auditRec.AddEventResultState(deletedGroup)
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func migrateIdLdap(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
props := model.StringInterfaceFromJSON(r.Body)
|
||||
toAttribute, ok := props["toAttribute"].(string)
|
||||
if !ok || toAttribute == "" {
|
||||
c.SetInvalidParam("toAttribute")
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("idMigrateLdap", audit.Fail)
|
||||
audit.AddEventParameter(auditRec, "to_attribute", toAttribute)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAP {
|
||||
c.Err = model.NewAppError("Api4.idMigrateLdap", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.MigrateIdLDAP(toAttribute); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func parseLdapCertificateRequest(r *http.Request, maxFileSize int64) (*multipart.FileHeader, *model.AppError) {
|
||||
err := r.ParseMultipartForm(maxFileSize)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("addLdapCertificate", "api.admin.add_certificate.parseform.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
m := r.MultipartForm
|
||||
|
||||
fileArray, ok := m.File["certificate"]
|
||||
if !ok {
|
||||
return nil, model.NewAppError("addLdapCertificate", "api.admin.add_certificate.no_file.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if len(fileArray) <= 0 {
|
||||
return nil, model.NewAppError("addLdapCertificate", "api.admin.add_certificate.array.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return fileArray[0], nil
|
||||
}
|
||||
|
||||
func addLdapPublicCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionAddLdapPublicCert) {
|
||||
c.SetPermissionError(model.PermissionAddLdapPublicCert)
|
||||
return
|
||||
}
|
||||
|
||||
fileData, err := parseLdapCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("addLdapPublicCertificate", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
|
||||
|
||||
if err := c.App.AddLdapPublicCertificate(fileData); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func addLdapPrivateCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionAddLdapPrivateCert) {
|
||||
c.SetPermissionError(model.PermissionAddLdapPrivateCert)
|
||||
return
|
||||
}
|
||||
|
||||
fileData, err := parseLdapCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("addLdapPrivateCertificate", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
|
||||
|
||||
if err := c.App.AddLdapPrivateCertificate(fileData); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func removeLdapPublicCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRemoveLdapPublicCert) {
|
||||
c.SetPermissionError(model.PermissionRemoveLdapPublicCert)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("removeLdapPublicCertificate", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if err := c.App.RemoveLdapPublicCertificate(); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func removeLdapPrivateCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRemoveLdapPrivateCert) {
|
||||
c.SetPermissionError(model.PermissionRemoveLdapPrivateCert)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("removeLdapPrivateCertificate", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if err := c.App.RemoveLdapPrivateCertificate(); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
// addUserToGroupSyncables creates memberships—for the given user—to all of their group syncables (i.e. channels or teams).
|
||||
// For each group the user is a member of, for each channel and/or team that group is associated with, the user will be added.
|
||||
func addUserToGroupSyncables(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementGroups) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementGroups)
|
||||
return
|
||||
}
|
||||
|
||||
user, appErr := c.App.GetUser(c.Params.UserId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if user.AuthService != model.UserAuthServiceLdap {
|
||||
c.Err = model.NewAppError("addUserToGroupSyncables", "api.user.add_user_to_group_syncables.not_ldap_user.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("addUserToGroupSyncables", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
params := model.CreateDefaultMembershipParams{Since: 0, ReAddRemovedMembers: true, ScopedUserID: &user.Id}
|
||||
err := c.App.CreateDefaultMemberships(c.AppContext, params)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("addUserToGroupSyncables", "api.admin.syncables_error", nil, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
403
api4/license.go
403
api4/license.go
|
|
@ -1,403 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
b64 "encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/utils"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
func (api *API) InitLicense() {
|
||||
api.BaseRoutes.APIRoot.Handle("/trial-license", api.APISessionRequired(requestTrialLicense)).Methods("POST")
|
||||
api.BaseRoutes.APIRoot.Handle("/trial-license/prev", api.APISessionRequired(getPrevTrialLicense)).Methods("GET")
|
||||
api.BaseRoutes.APIRoot.Handle("/license", api.APISessionRequired(addLicense)).Methods("POST")
|
||||
api.BaseRoutes.APIRoot.Handle("/license", api.APISessionRequired(removeLicense)).Methods("DELETE")
|
||||
api.BaseRoutes.APIRoot.Handle("/license/renewal", api.APISessionRequired(requestRenewalLink)).Methods("GET")
|
||||
api.BaseRoutes.APIRoot.Handle("/license/client", api.APIHandler(getClientLicense)).Methods("GET")
|
||||
api.BaseRoutes.APIRoot.Handle("/license/review", api.APISessionRequired(requestTrueUpReview)).Methods("POST")
|
||||
api.BaseRoutes.APIRoot.Handle("/license/review/status", api.APISessionRequired(trueUpReviewStatus)).Methods("GET")
|
||||
}
|
||||
|
||||
func getClientLicense(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
format := r.URL.Query().Get("format")
|
||||
|
||||
if format == "" {
|
||||
c.Err = model.NewAppError("getClientLicense", "api.license.client.old_format.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if format != "old" {
|
||||
c.SetInvalidParam("format")
|
||||
return
|
||||
}
|
||||
|
||||
var clientLicense map[string]string
|
||||
|
||||
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadLicenseInformation) {
|
||||
clientLicense = c.App.Srv().ClientLicense()
|
||||
} else {
|
||||
clientLicense = c.App.Srv().GetSanitizedClientLicense()
|
||||
}
|
||||
|
||||
w.Write([]byte(model.MapToJSON(clientLicense)))
|
||||
}
|
||||
|
||||
func addLicense(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
auditRec := c.MakeAuditRecord("addLicense", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) {
|
||||
c.SetPermissionError(model.PermissionManageLicenseInformation)
|
||||
return
|
||||
}
|
||||
|
||||
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
|
||||
c.Err = model.NewAppError("addLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
m := r.MultipartForm
|
||||
|
||||
fileArray, ok := m.File["license"]
|
||||
if !ok {
|
||||
c.Err = model.NewAppError("addLicense", "api.license.add_license.no_file.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(fileArray) <= 0 {
|
||||
c.Err = model.NewAppError("addLicense", "api.license.add_license.array.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
fileData := fileArray[0]
|
||||
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
|
||||
|
||||
file, err := fileData.Open()
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("addLicense", "api.license.add_license.open.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
io.Copy(buf, file)
|
||||
|
||||
licenseBytes := buf.Bytes()
|
||||
license, appErr := utils.LicenseValidator.LicenseFromBytes(licenseBytes)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
// skip the restrictions if license is a sanctioned trial
|
||||
if !license.IsSanctionedTrial() && license.IsTrialLicense() {
|
||||
lm := c.App.Srv().Platform().LicenseManager()
|
||||
if lm == nil {
|
||||
c.Err = model.NewAppError("addLicense", "api.license.upgrade_needed.app_error", nil, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
canStartTrialLicense, err := lm.CanStartTrial()
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("addLicense", "api.license.add_license.open.app_error", nil, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if !canStartTrialLicense {
|
||||
c.Err = model.NewAppError("addLicense", "api.license.request-trial.can-start-trial.not-allowed", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
license, appErr = c.App.Srv().SaveLicense(licenseBytes)
|
||||
if appErr != nil {
|
||||
if appErr.Id == model.ExpiredLicenseError {
|
||||
c.LogAudit("failed - expired or non-started license")
|
||||
} else if appErr.Id == model.InvalidLicenseError {
|
||||
c.LogAudit("failed - invalid license")
|
||||
} else {
|
||||
c.LogAudit("failed - unable to save license")
|
||||
}
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if c.App.Channels().License().IsCloud() {
|
||||
// If cloud, invalidate the caches when a new license is loaded
|
||||
defer c.App.Srv().Cloud.HandleLicenseChange()
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
c.LogAudit("success")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(license); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func removeLicense(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
auditRec := c.MakeAuditRecord("removeLicense", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) {
|
||||
c.SetPermissionError(model.PermissionManageLicenseInformation)
|
||||
return
|
||||
}
|
||||
|
||||
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
|
||||
c.Err = model.NewAppError("removeLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.Srv().RemoveLicense(); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
c.LogAudit("success")
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func requestTrialLicense(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
auditRec := c.MakeAuditRecord("requestTrialLicense", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) {
|
||||
c.SetPermissionError(model.PermissionManageLicenseInformation)
|
||||
return
|
||||
}
|
||||
|
||||
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
|
||||
c.Err = model.NewAppError("requestTrialLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if c.App.Srv().Platform().LicenseManager() == nil {
|
||||
c.Err = model.NewAppError("requestTrialLicense", "api.license.upgrade_needed.app_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
canStartTrialLicense, err := c.App.Srv().Platform().LicenseManager().CanStartTrial()
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("requestTrialLicense", "api.license.request-trial.can-start-trial.error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if !canStartTrialLicense {
|
||||
c.Err = model.NewAppError("requestTrialLicense", "api.license.request-trial.can-start-trial.not-allowed", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var trialRequest struct {
|
||||
Users int `json:"users"`
|
||||
TermsAccepted bool `json:"terms_accepted"`
|
||||
ReceiveEmailsAccepted bool `json:"receive_emails_accepted"`
|
||||
}
|
||||
|
||||
b, readErr := io.ReadAll(r.Body)
|
||||
if readErr != nil {
|
||||
c.Err = model.NewAppError("requestTrialLicense", "api.license.request-trial.bad-request", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
json.Unmarshal(b, &trialRequest)
|
||||
|
||||
if err := c.App.Channels().RequestTrialLicense(c.AppContext.Session().UserId, trialRequest.Users, trialRequest.TermsAccepted, trialRequest.ReceiveEmailsAccepted); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
c.LogAudit("success")
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func requestRenewalLink(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
auditRec := c.MakeAuditRecord("requestRenewalLink", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) {
|
||||
c.SetPermissionError(model.PermissionManageLicenseInformation)
|
||||
return
|
||||
}
|
||||
|
||||
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
|
||||
c.Err = model.NewAppError("requestRenewalLink", "api.restricted_system_admin", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
renewalLink, token, err := c.App.Srv().GenerateLicenseRenewalLink()
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if c.App.Cloud() == nil {
|
||||
c.Err = model.NewAppError("requestRenewalLink", "api.license.upgrade_needed.app_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// check if it is possible to renew license on the portal with generated token
|
||||
status, e := c.App.Cloud().GetLicenseSelfServeStatus(c.AppContext.Session().UserId, token)
|
||||
if e != nil {
|
||||
c.Err = model.NewAppError("requestRenewalLink", "api.license.request_renewal_link.cannot_renew_on_cws", nil, e.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if !status.IsRenewable {
|
||||
c.Err = model.NewAppError("requestRenewalLink", "api.license.request_renewal_link.cannot_renew_on_cws", nil, "License is not self-serve renewable", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
c.LogAudit("success")
|
||||
|
||||
_, werr := w.Write([]byte(fmt.Sprintf(`{"renewal_link": "%s"}`, renewalLink)))
|
||||
if werr != nil {
|
||||
c.Err = model.NewAppError("requestRenewalLink", "api.license.request_renewal_link.app_error", nil, werr.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func getPrevTrialLicense(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if c.App.Srv().Platform().LicenseManager() == nil {
|
||||
c.Err = model.NewAppError("getPrevTrialLicense", "api.license.upgrade_needed.app_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
license, err := c.App.Srv().Platform().LicenseManager().GetPrevTrial()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var clientLicense map[string]string
|
||||
|
||||
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadLicenseInformation) {
|
||||
clientLicense = utils.GetClientLicense(license)
|
||||
} else {
|
||||
clientLicense = utils.GetSanitizedClientLicense(utils.GetClientLicense(license))
|
||||
}
|
||||
|
||||
w.Write([]byte(model.MapToJSON(clientLicense)))
|
||||
}
|
||||
|
||||
func requestTrueUpReview(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// Only admins can request a true up review.
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageLicenseInformation)
|
||||
return
|
||||
}
|
||||
|
||||
license := c.App.Channels().License()
|
||||
if license == nil {
|
||||
c.Err = model.NewAppError("requestTrueUpReview", "api.license.true_up_review.license_required", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
if license.IsCloud() {
|
||||
c.Err = model.NewAppError("requestTrueUpReview", "api.license.true_up_review.not_allowed_for_cloud", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
status, appErr := c.App.GetOrCreateTrueUpReviewStatus()
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
// If a true up review has already been submitted for the current due date, complete the request
|
||||
// with no errors.
|
||||
if status.Completed {
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
profileMap, err := c.App.GetTrueUpProfile()
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("requestTrueUpReview", "api.license.true_up_review.get_status_error", nil, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
profileMapJson, err := json.Marshal(profileMap)
|
||||
if err != nil {
|
||||
c.SetJSONEncodingError(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Do not send true-up review data if the user has already requested one for the quarter.
|
||||
// And only send a true-up review via as a one-time telemetry request if telemetry is disabled.
|
||||
telemetryEnabled := c.App.Config().LogSettings.EnableDiagnostics
|
||||
if telemetryEnabled != nil && !*telemetryEnabled {
|
||||
// Send telemetry data
|
||||
c.App.Srv().GetTelemetryService().SendTelemetry(model.TrueUpReviewTelemetryName, profileMap)
|
||||
|
||||
// Update the review status to reflect the completion.
|
||||
status.Completed = true
|
||||
c.App.Srv().Store().TrueUpReview().Update(status)
|
||||
}
|
||||
|
||||
// Encode to string rather than byte[] otherwise json.Marshal will encode it further.
|
||||
encodedData := b64.StdEncoding.EncodeToString(profileMapJson)
|
||||
responseContent := struct {
|
||||
Content string `json:"content"`
|
||||
}{Content: encodedData}
|
||||
response, _ := json.Marshal(responseContent)
|
||||
|
||||
w.Write(response)
|
||||
}
|
||||
|
||||
func trueUpReviewStatus(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// Only admins can request a true up review.
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageLicenseInformation)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for license
|
||||
license := c.App.Channels().License()
|
||||
if license == nil {
|
||||
c.Err = model.NewAppError("cloudTrueUpReviewNotAllowed", "api.license.true_up_review.license_required", nil, "True up review requires a license", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
if license.IsCloud() {
|
||||
c.Err = model.NewAppError("cloudTrueUpReviewNotAllowed", "api.license.true_up_review.not_allowed_for_cloud", nil, "True up review is not allowed for cloud instances", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
status, appErr := c.App.GetOrCreateTrueUpReviewStatus()
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
}
|
||||
|
||||
json, err := json.Marshal(status)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("trueUpReviewStatus", "api.marshal_error", nil, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(json)
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/testlib"
|
||||
)
|
||||
|
||||
var replicaFlag bool
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if f := flag.Lookup("mysql-replica"); f == nil {
|
||||
flag.BoolVar(&replicaFlag, "mysql-replica", false, "")
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
var options = testlib.HelperOptions{
|
||||
EnableStore: true,
|
||||
EnableResources: true,
|
||||
WithReadReplica: replicaFlag,
|
||||
}
|
||||
|
||||
mainHelper = testlib.NewMainHelperWithOptions(&options)
|
||||
defer mainHelper.Close()
|
||||
|
||||
mainHelper.Main(m)
|
||||
}
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
package api4
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNotifyAdmin(t *testing.T) {
|
||||
t.Run("error when plan is unknown when notifying on upgrade", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic().InitLogin()
|
||||
defer th.TearDown()
|
||||
|
||||
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||
RequiredPlan: "Unknown plan",
|
||||
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, err.Error(), ": Unable to save notify data.")
|
||||
require.Equal(t, http.StatusInternalServerError, statusCode)
|
||||
|
||||
})
|
||||
|
||||
t.Run("error when plan is unknown when notifying to trial", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic().InitLogin()
|
||||
defer th.TearDown()
|
||||
|
||||
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||
RequiredPlan: "Unknown plan",
|
||||
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||
TrialNotification: true,
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, err.Error(), ": Unable to save notify data.")
|
||||
require.Equal(t, http.StatusInternalServerError, statusCode)
|
||||
|
||||
})
|
||||
|
||||
t.Run("error when feature is unknown when notifying on upgrade", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic().InitLogin()
|
||||
defer th.TearDown()
|
||||
|
||||
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||
RequiredFeature: "Unknown feature",
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, err.Error(), ": Unable to save notify data.")
|
||||
require.Equal(t, http.StatusInternalServerError, statusCode)
|
||||
})
|
||||
|
||||
t.Run("error when feature is unknown when notifying to trial", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic().InitLogin()
|
||||
defer th.TearDown()
|
||||
|
||||
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||
RequiredFeature: "Unknown feature",
|
||||
TrialNotification: true,
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, err.Error(), ": Unable to save notify data.")
|
||||
require.Equal(t, http.StatusInternalServerError, statusCode)
|
||||
})
|
||||
|
||||
t.Run("error when user tries to notify again on same feature within the cool off period", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic().InitLogin()
|
||||
defer th.TearDown()
|
||||
|
||||
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, statusCode)
|
||||
|
||||
// second attempt to notify for all professional features
|
||||
statusCode, err = th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
require.Equal(t, err.Error(), ": Already notified admin")
|
||||
require.Equal(t, http.StatusForbidden, statusCode)
|
||||
})
|
||||
|
||||
t.Run("successfully save upgrade notification", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic().InitLogin()
|
||||
defer th.TearDown()
|
||||
|
||||
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, statusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTriggerNotifyAdmin(t *testing.T) {
|
||||
t.Run("error when EnableAPITriggerAdminNotifications is not true", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic().InitLogin()
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPITriggerAdminNotifications = false })
|
||||
|
||||
statusCode, err := th.SystemAdminClient.TriggerNotifyAdmin(&model.NotifyAdminToUpgradeRequest{})
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, err.Error(), ": Internal error during cloud api request.")
|
||||
require.Equal(t, http.StatusForbidden, statusCode)
|
||||
|
||||
})
|
||||
|
||||
t.Run("error when non admins try to trigger notifications", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic().InitLogin()
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPITriggerAdminNotifications = true })
|
||||
|
||||
statusCode, err := th.Client.TriggerNotifyAdmin(&model.NotifyAdminToUpgradeRequest{})
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, err.Error(), ": You do not have the appropriate permissions.")
|
||||
require.Equal(t, http.StatusForbidden, statusCode)
|
||||
})
|
||||
|
||||
t.Run("happy path", func(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPITriggerAdminNotifications = true })
|
||||
|
||||
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, statusCode)
|
||||
|
||||
statusCode, err = th.SystemAdminClient.TriggerNotifyAdmin(&model.NotifyAdminToUpgradeRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, statusCode)
|
||||
})
|
||||
}
|
||||
312
api4/oauth.go
312
api4/oauth.go
|
|
@ -1,312 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitOAuth() {
|
||||
api.BaseRoutes.OAuthApps.Handle("", api.APISessionRequired(createOAuthApp)).Methods("POST")
|
||||
api.BaseRoutes.OAuthApp.Handle("", api.APISessionRequired(updateOAuthApp)).Methods("PUT")
|
||||
api.BaseRoutes.OAuthApps.Handle("", api.APISessionRequired(getOAuthApps)).Methods("GET")
|
||||
api.BaseRoutes.OAuthApp.Handle("", api.APISessionRequired(getOAuthApp)).Methods("GET")
|
||||
api.BaseRoutes.OAuthApp.Handle("/info", api.APISessionRequired(getOAuthAppInfo)).Methods("GET")
|
||||
api.BaseRoutes.OAuthApp.Handle("", api.APISessionRequired(deleteOAuthApp)).Methods("DELETE")
|
||||
api.BaseRoutes.OAuthApp.Handle("/regen_secret", api.APISessionRequired(regenerateOAuthAppSecret)).Methods("POST")
|
||||
|
||||
api.BaseRoutes.User.Handle("/oauth/apps/authorized", api.APISessionRequired(getAuthorizedOAuthApps)).Methods("GET")
|
||||
}
|
||||
|
||||
func createOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var oauthApp model.OAuthApp
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&oauthApp); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("oauth_app", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("createOAuthApp", audit.Fail)
|
||||
audit.AddEventParameterAuditable(auditRec, "oauth_app", &oauthApp)
|
||||
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
|
||||
c.SetPermissionError(model.PermissionManageOAuth)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
oauthApp.IsTrusted = false
|
||||
}
|
||||
|
||||
oauthApp.CreatorId = c.AppContext.Session().UserId
|
||||
|
||||
rapp, err := c.App.CreateOAuthApp(&oauthApp)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(rapp)
|
||||
auditRec.AddEventObjectType("oauth_app")
|
||||
c.LogAudit("client_id=" + rapp.Id)
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(rapp); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func updateOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireAppId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("updateOAuthApp", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "oauth_app_id", c.Params.AppId)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
|
||||
c.SetPermissionError(model.PermissionManageOAuth)
|
||||
return
|
||||
}
|
||||
|
||||
var oauthApp model.OAuthApp
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&oauthApp); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("oauth_app", jsonErr)
|
||||
return
|
||||
}
|
||||
audit.AddEventParameterAuditable(auditRec, "oauth_app", &oauthApp)
|
||||
|
||||
// The app being updated in the payload must be the same one as indicated in the URL.
|
||||
if oauthApp.Id != c.Params.AppId {
|
||||
c.SetInvalidParam("app_id")
|
||||
return
|
||||
}
|
||||
|
||||
oldOAuthApp, err := c.App.GetOAuthApp(c.Params.AppId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
auditRec.AddEventPriorState(oldOAuthApp)
|
||||
|
||||
if c.AppContext.Session().UserId != oldOAuthApp.CreatorId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
|
||||
c.SetPermissionError(model.PermissionManageSystemWideOAuth)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
oauthApp.IsTrusted = oldOAuthApp.IsTrusted
|
||||
}
|
||||
|
||||
updatedOAuthApp, err := c.App.UpdateOAuthApp(oldOAuthApp, &oauthApp)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventResultState(updatedOAuthApp)
|
||||
auditRec.AddEventObjectType("oauth_app")
|
||||
auditRec.Success()
|
||||
c.LogAudit("success")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(updatedOAuthApp); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getOAuthApps(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
|
||||
c.Err = model.NewAppError("getOAuthApps", "api.command.admin_only.app_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
var apps []*model.OAuthApp
|
||||
var appErr *model.AppError
|
||||
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
|
||||
apps, appErr = c.App.GetOAuthApps(c.Params.Page, c.Params.PerPage)
|
||||
} else if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
|
||||
apps, appErr = c.App.GetOAuthAppsByCreator(c.AppContext.Session().UserId, c.Params.Page, c.Params.PerPage)
|
||||
} else {
|
||||
c.SetPermissionError(model.PermissionManageOAuth)
|
||||
return
|
||||
}
|
||||
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(apps)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getOAuthApps", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func getOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireAppId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
|
||||
c.SetPermissionError(model.PermissionManageOAuth)
|
||||
return
|
||||
}
|
||||
|
||||
oauthApp, err := c.App.GetOAuthApp(c.Params.AppId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if oauthApp.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
|
||||
c.SetPermissionError(model.PermissionManageSystemWideOAuth)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(oauthApp); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getOAuthAppInfo(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireAppId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
oauthApp, err := c.App.GetOAuthApp(c.Params.AppId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
oauthApp.Sanitize()
|
||||
if err := json.NewEncoder(w).Encode(oauthApp); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func deleteOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireAppId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("deleteOAuthApp", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "oauth_app_id", c.Params.AppId)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
|
||||
c.SetPermissionError(model.PermissionManageOAuth)
|
||||
return
|
||||
}
|
||||
|
||||
oauthApp, err := c.App.GetOAuthApp(c.Params.AppId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
auditRec.AddEventPriorState(oauthApp)
|
||||
auditRec.AddEventObjectType("oauth_app")
|
||||
|
||||
if c.AppContext.Session().UserId != oauthApp.CreatorId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
|
||||
c.SetPermissionError(model.PermissionManageSystemWideOAuth)
|
||||
return
|
||||
}
|
||||
|
||||
err = c.App.DeleteOAuthApp(oauthApp.Id)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
c.LogAudit("success")
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func regenerateOAuthAppSecret(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireAppId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("regenerateOAuthAppSecret", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "oauth_app_id", c.Params.AppId)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
|
||||
c.SetPermissionError(model.PermissionManageOAuth)
|
||||
return
|
||||
}
|
||||
|
||||
oauthApp, err := c.App.GetOAuthApp(c.Params.AppId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
auditRec.AddEventPriorState(oauthApp)
|
||||
auditRec.AddEventObjectType("oauth_app")
|
||||
|
||||
if oauthApp.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
|
||||
c.SetPermissionError(model.PermissionManageSystemWideOAuth)
|
||||
return
|
||||
}
|
||||
|
||||
oauthApp, err = c.App.RegenerateOAuthAppSecret(oauthApp)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventResultState(oauthApp)
|
||||
auditRec.Success()
|
||||
c.LogAudit("success")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(oauthApp); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getAuthorizedOAuthApps(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
|
||||
apps, appErr := c.App.GetAuthorizedAppsForUser(c.Params.UserId, c.Params.Page, c.Params.PerPage)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(apps)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getAuthorizedOAuthApps", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(js)
|
||||
}
|
||||
472
api4/plugin.go
472
api4/plugin.go
|
|
@ -1,472 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/store"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
MaximumPluginFileSize = 50 * 1024 * 1024
|
||||
)
|
||||
|
||||
func (api *API) InitPlugin() {
|
||||
api.BaseRoutes.Plugins.Handle("", api.APISessionRequired(uploadPlugin)).Methods("POST")
|
||||
api.BaseRoutes.Plugins.Handle("", api.APISessionRequired(getPlugins)).Methods("GET")
|
||||
api.BaseRoutes.Plugin.Handle("", api.APISessionRequired(removePlugin)).Methods("DELETE")
|
||||
api.BaseRoutes.Plugins.Handle("/install_from_url", api.APISessionRequired(installPluginFromURL)).Methods("POST")
|
||||
api.BaseRoutes.Plugins.Handle("/marketplace", api.APISessionRequired(installMarketplacePlugin)).Methods("POST")
|
||||
|
||||
api.BaseRoutes.Plugins.Handle("/statuses", api.APISessionRequired(getPluginStatuses)).Methods("GET")
|
||||
api.BaseRoutes.Plugin.Handle("/enable", api.APISessionRequired(enablePlugin)).Methods("POST")
|
||||
api.BaseRoutes.Plugin.Handle("/disable", api.APISessionRequired(disablePlugin)).Methods("POST")
|
||||
|
||||
api.BaseRoutes.Plugins.Handle("/webapp", api.APIHandler(getWebappPlugins)).Methods("GET")
|
||||
|
||||
api.BaseRoutes.Plugins.Handle("/marketplace", api.APISessionRequired(getMarketplacePlugins)).Methods("GET")
|
||||
|
||||
api.BaseRoutes.Plugins.Handle("/marketplace/first_admin_visit", api.APIHandler(setFirstAdminVisitMarketplaceStatus)).Methods("POST")
|
||||
api.BaseRoutes.Plugins.Handle("/marketplace/first_admin_visit", api.APISessionRequired(getFirstAdminVisitMarketplaceStatus)).Methods("GET")
|
||||
}
|
||||
|
||||
func uploadPlugin(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
config := c.App.Config()
|
||||
if !*config.PluginSettings.Enable || !*config.PluginSettings.EnableUploads || *config.PluginSettings.RequirePluginSignature {
|
||||
c.Err = model.NewAppError("uploadPlugin", "app.plugin.upload_disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("uploadPlugin", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseMultipartForm(MaximumPluginFileSize); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
m := r.MultipartForm
|
||||
|
||||
pluginArray, ok := m.File["plugin"]
|
||||
if !ok {
|
||||
c.Err = model.NewAppError("uploadPlugin", "api.plugin.upload.no_file.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(pluginArray) <= 0 {
|
||||
c.Err = model.NewAppError("uploadPlugin", "api.plugin.upload.array.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
audit.AddEventParameter(auditRec, "filename", pluginArray[0].Filename)
|
||||
|
||||
file, err := pluginArray[0].Open()
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("uploadPlugin", "api.plugin.upload.file.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
force := false
|
||||
if len(m.Value["force"]) > 0 && m.Value["force"][0] == "true" {
|
||||
force = true
|
||||
}
|
||||
|
||||
installPlugin(c, w, file, force)
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
func installPluginFromURL(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !*c.App.Config().PluginSettings.Enable ||
|
||||
*c.App.Config().PluginSettings.RequirePluginSignature ||
|
||||
!*c.App.Config().PluginSettings.EnableUploads {
|
||||
c.Err = model.NewAppError("installPluginFromURL", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("installPluginFromURL", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
|
||||
return
|
||||
}
|
||||
|
||||
force, _ := strconv.ParseBool(r.URL.Query().Get("force"))
|
||||
downloadURL := r.URL.Query().Get("plugin_download_url")
|
||||
audit.AddEventParameter(auditRec, "url", downloadURL)
|
||||
|
||||
pluginFileBytes, err := c.App.DownloadFromURL(downloadURL)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("installPluginFromURL", "api.plugin.install.download_failed.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
installPlugin(c, w, bytes.NewReader(pluginFileBytes), force)
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
func installMarketplacePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !*c.App.Config().PluginSettings.Enable {
|
||||
c.Err = model.NewAppError("installMarketplacePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
if !*c.App.Config().PluginSettings.EnableMarketplace {
|
||||
c.Err = model.NewAppError("installMarketplacePlugin", "app.plugin.marketplace_disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("installMarketplacePlugin", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
|
||||
return
|
||||
}
|
||||
|
||||
pluginRequest, err := model.PluginRequestFromReader(r.Body)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("installMarketplacePlugin", "app.plugin.marketplace_plugin_request.app_error", nil, err.Error(), http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
audit.AddEventParameter(auditRec, "plugin_id", pluginRequest.Id)
|
||||
|
||||
// Always install the latest compatible version
|
||||
// https://mattermost.atlassian.net/browse/MM-41981
|
||||
pluginRequest.Version = ""
|
||||
|
||||
manifest, appErr := c.App.Channels().InstallMarketplacePlugin(pluginRequest)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddMeta("plugin_name", manifest.Name)
|
||||
auditRec.AddMeta("plugin_desc", manifest.Description)
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(manifest); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getPlugins(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !*c.App.Config().PluginSettings.Enable {
|
||||
c.Err = model.NewAppError("getPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadPlugins) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadPlugins)
|
||||
return
|
||||
}
|
||||
|
||||
response, err := c.App.GetPlugins()
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getPluginStatuses(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !*c.App.Config().PluginSettings.Enable {
|
||||
c.Err = model.NewAppError("getPluginStatuses", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadPlugins) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadPlugins)
|
||||
return
|
||||
}
|
||||
|
||||
response, err := c.App.GetClusterPluginStatuses()
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func removePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequirePluginId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !*c.App.Config().PluginSettings.Enable {
|
||||
c.Err = model.NewAppError("removePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("removePlugin", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "plugin_id", c.Params.PluginId)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
|
||||
return
|
||||
}
|
||||
|
||||
err := c.App.Channels().RemovePlugin(c.Params.PluginId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func getWebappPlugins(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !*c.App.Config().PluginSettings.Enable {
|
||||
c.Err = model.NewAppError("getWebappPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
manifests, appErr := c.App.GetActivePluginManifests()
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
clientManifests := []*model.Manifest{}
|
||||
for _, m := range manifests {
|
||||
if m.HasClient() {
|
||||
manifest := m.ClientManifest()
|
||||
|
||||
// There is no reason to expose the SettingsSchema in this API call; it's not used in the webapp.
|
||||
manifest.SettingsSchema = nil
|
||||
clientManifests = append(clientManifests, manifest)
|
||||
}
|
||||
}
|
||||
|
||||
js, err := json.Marshal(clientManifests)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getWebappPlugins", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func getMarketplacePlugins(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !*c.App.Config().PluginSettings.Enable {
|
||||
c.Err = model.NewAppError("getMarketplacePlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
if !*c.App.Config().PluginSettings.EnableMarketplace {
|
||||
c.Err = model.NewAppError("getMarketplacePlugins", "app.plugin.marketplace_disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
filter, err := parseMarketplacePluginFilter(r.URL)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getMarketplacePlugins", "app.plugin.marshal.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
// if we are looking for remote only, we don't need to check for permissions
|
||||
if !filter.RemoteOnly && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadPlugins) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadPlugins)
|
||||
return
|
||||
}
|
||||
|
||||
plugins, appErr := c.App.GetMarketplacePlugins(filter)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
json, err := json.Marshal(plugins)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getMarketplacePlugins", "app.plugin.marshal.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(json)
|
||||
}
|
||||
|
||||
func enablePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequirePluginId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !*c.App.Config().PluginSettings.Enable {
|
||||
c.Err = model.NewAppError("activatePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("enablePlugin", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "plugin_id", c.Params.PluginId)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.EnablePlugin(c.Params.PluginId); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func disablePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequirePluginId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !*c.App.Config().PluginSettings.Enable {
|
||||
c.Err = model.NewAppError("deactivatePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("disablePlugin", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "plugin_id", c.Params.PluginId)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.DisablePlugin(c.Params.PluginId); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func parseMarketplacePluginFilter(u *url.URL) (*model.MarketplacePluginFilter, error) {
|
||||
page, err := parseInt(u, "page", 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
perPage, err := parseInt(u, "per_page", 100)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filter := u.Query().Get("filter")
|
||||
serverVersion := u.Query().Get("server_version")
|
||||
localOnly, _ := strconv.ParseBool(u.Query().Get("local_only"))
|
||||
remoteOnly, _ := strconv.ParseBool(u.Query().Get("remote_only"))
|
||||
|
||||
if localOnly && remoteOnly {
|
||||
return nil, errors.New("local_only and remote_only cannot be both true")
|
||||
}
|
||||
|
||||
return &model.MarketplacePluginFilter{
|
||||
Page: page,
|
||||
PerPage: perPage,
|
||||
Filter: filter,
|
||||
ServerVersion: serverVersion,
|
||||
LocalOnly: localOnly,
|
||||
RemoteOnly: remoteOnly,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func installPlugin(c *Context, w http.ResponseWriter, plugin io.ReadSeeker, force bool) {
|
||||
manifest, appErr := c.App.InstallPlugin(plugin, force)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(manifest); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func setFirstAdminVisitMarketplaceStatus(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
auditRec := c.MakeAuditRecord("setFirstAdminVisitMarketplaceStatus", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
firstAdminVisitMarketplaceObj := model.System{
|
||||
Name: model.SystemFirstAdminVisitMarketplace,
|
||||
Value: "true",
|
||||
}
|
||||
|
||||
if err := c.App.Srv().Store().System().SaveOrUpdate(&firstAdminVisitMarketplaceObj); err != nil {
|
||||
c.Err = model.NewAppError("setFirstAdminVisitMarketplaceStatus", "api.error_set_first_admin_visit_marketplace_status", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
message := model.NewWebSocketEvent(model.WebsocketFirstAdminVisitMarketplaceStatusReceived, "", "", "", nil, "")
|
||||
message.Add("firstAdminVisitMarketplaceStatus", firstAdminVisitMarketplaceObj.Value)
|
||||
c.App.Publish(message)
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func getFirstAdminVisitMarketplaceStatus(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
auditRec := c.MakeAuditRecord("getFirstAdminVisitMarketplaceStatus", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
firstAdminVisitMarketplaceObj, err := c.App.Srv().Store().System().GetByName(model.SystemFirstAdminVisitMarketplace)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
firstAdminVisitMarketplaceObj = &model.System{
|
||||
Name: model.SystemFirstAdminVisitMarketplace,
|
||||
Value: "false",
|
||||
}
|
||||
default:
|
||||
c.Err = model.NewAppError("getFirstAdminVisitMarketplaceStatus", "api.error_get_first_admin_visit_marketplace_status", nil, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
if err := json.NewEncoder(w).Encode(firstAdminVisitMarketplaceObj); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
1886
api4/plugin_test.go
1886
api4/plugin_test.go
File diff suppressed because it is too large
Load diff
1124
api4/post.go
1124
api4/post.go
File diff suppressed because it is too large
Load diff
3799
api4/post_test.go
3799
api4/post_test.go
File diff suppressed because it is too large
Load diff
|
|
@ -1,164 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitPreference() {
|
||||
api.BaseRoutes.Preferences.Handle("", api.APISessionRequired(getPreferences)).Methods("GET")
|
||||
api.BaseRoutes.Preferences.Handle("", api.APISessionRequired(updatePreferences)).Methods("PUT")
|
||||
api.BaseRoutes.Preferences.Handle("/delete", api.APISessionRequired(deletePreferences)).Methods("POST")
|
||||
api.BaseRoutes.Preferences.Handle("/{category:[A-Za-z0-9_]+}", api.APISessionRequired(getPreferencesByCategory)).Methods("GET")
|
||||
api.BaseRoutes.Preferences.Handle("/{category:[A-Za-z0-9_]+}/name/{preference_name:[A-Za-z0-9_]+}", api.APISessionRequired(getPreferenceByCategoryAndName)).Methods("GET")
|
||||
}
|
||||
|
||||
func getPreferences(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
|
||||
preferences, err := c.App.GetPreferencesForUser(c.Params.UserId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(preferences); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getPreferencesByCategory(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId().RequireCategory()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
|
||||
preferences, err := c.App.GetPreferenceByCategoryForUser(c.Params.UserId, c.Params.Category)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(preferences); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getPreferenceByCategoryAndName(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId().RequireCategory().RequirePreferenceName()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
|
||||
preferences, err := c.App.GetPreferenceByCategoryAndNameForUser(c.Params.UserId, c.Params.Category, c.Params.PreferenceName)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(preferences); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func updatePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("updatePreferences", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
|
||||
var preferences model.Preferences
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&preferences); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("preferences", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
var sanitizedPreferences model.Preferences
|
||||
|
||||
for _, pref := range preferences {
|
||||
if pref.Category == model.PreferenceCategoryFlaggedPost {
|
||||
post, err := c.App.GetSinglePost(pref.Name, false)
|
||||
if err != nil {
|
||||
c.SetInvalidParam("preference.name")
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), post.ChannelId, model.PermissionReadChannel) {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sanitizedPreferences = append(sanitizedPreferences, pref)
|
||||
}
|
||||
|
||||
if err := c.App.UpdatePreferences(c.Params.UserId, sanitizedPreferences); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func deletePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("deletePreferences", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
|
||||
var preferences model.Preferences
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&preferences); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("preferences", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.DeletePreferences(c.Params.UserId, preferences); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
141
api4/reaction.go
141
api4/reaction.go
|
|
@ -1,141 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitReaction() {
|
||||
api.BaseRoutes.Reactions.Handle("", api.APISessionRequired(saveReaction)).Methods("POST")
|
||||
api.BaseRoutes.Post.Handle("/reactions", api.APISessionRequired(getReactions)).Methods("GET")
|
||||
api.BaseRoutes.ReactionByNameForPostForUser.Handle("", api.APISessionRequired(deleteReaction)).Methods("DELETE")
|
||||
api.BaseRoutes.Posts.Handle("/ids/reactions", api.APISessionRequired(getBulkReactions)).Methods("POST")
|
||||
}
|
||||
|
||||
func saveReaction(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var reaction model.Reaction
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&reaction); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("reaction", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
if !model.IsValidId(reaction.UserId) || !model.IsValidId(reaction.PostId) || reaction.EmojiName == "" || len(reaction.EmojiName) > model.EmojiNameMaxLength {
|
||||
c.Err = model.NewAppError("saveReaction", "api.reaction.save_reaction.invalid.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if reaction.UserId != c.AppContext.Session().UserId {
|
||||
c.Err = model.NewAppError("saveReaction", "api.reaction.save_reaction.user_id.app_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), reaction.PostId, model.PermissionAddReaction) {
|
||||
c.SetPermissionError(model.PermissionAddReaction)
|
||||
return
|
||||
}
|
||||
|
||||
re, err := c.App.SaveReactionForPost(c.AppContext, &reaction)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(re); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getReactions(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequirePostId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
|
||||
reactions, appErr := c.App.GetReactionsForPost(c.Params.PostId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(reactions)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getReactions", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func deleteReaction(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.RequirePostId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireEmojiName()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionRemoveReaction) {
|
||||
c.SetPermissionError(model.PermissionRemoveReaction)
|
||||
return
|
||||
}
|
||||
|
||||
if c.Params.UserId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRemoveOthersReactions) {
|
||||
c.SetPermissionError(model.PermissionRemoveOthersReactions)
|
||||
return
|
||||
}
|
||||
|
||||
reaction := &model.Reaction{
|
||||
UserId: c.Params.UserId,
|
||||
PostId: c.Params.PostId,
|
||||
EmojiName: c.Params.EmojiName,
|
||||
}
|
||||
|
||||
err := c.App.DeleteReactionForPost(c.AppContext, reaction)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func getBulkReactions(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
postIds := model.ArrayFromJSON(r.Body)
|
||||
for _, postId := range postIds {
|
||||
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), postId, model.PermissionReadChannel) {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
}
|
||||
reactions, appErr := c.App.GetBulkReactionsForPosts(postIds)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(reactions)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getBulkReactions", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
w.Write(js)
|
||||
}
|
||||
|
|
@ -1,287 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app"
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/services/remotecluster"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitRemoteCluster() {
|
||||
api.BaseRoutes.RemoteCluster.Handle("/ping", api.RemoteClusterTokenRequired(remoteClusterPing)).Methods("POST")
|
||||
api.BaseRoutes.RemoteCluster.Handle("/msg", api.RemoteClusterTokenRequired(remoteClusterAcceptMessage)).Methods("POST")
|
||||
api.BaseRoutes.RemoteCluster.Handle("/confirm_invite", api.RemoteClusterTokenRequired(remoteClusterConfirmInvite)).Methods("POST")
|
||||
api.BaseRoutes.RemoteCluster.Handle("/upload/{upload_id:[A-Za-z0-9]+}", api.RemoteClusterTokenRequired(uploadRemoteData)).Methods("POST")
|
||||
api.BaseRoutes.RemoteCluster.Handle("/{user_id:[A-Za-z0-9]+}/image", api.RemoteClusterTokenRequired(remoteSetProfileImage)).Methods("POST")
|
||||
}
|
||||
|
||||
func remoteClusterPing(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// make sure remote cluster service is enabled.
|
||||
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
var frame model.RemoteClusterFrame
|
||||
if err := json.NewDecoder(r.Body).Decode(&frame); err != nil {
|
||||
c.Err = model.NewAppError("remoteClusterPing", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
if appErr := frame.IsValid(); appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
remoteId := c.GetRemoteID(r)
|
||||
if remoteId != frame.RemoteId {
|
||||
c.SetInvalidRemoteIdError(frame.RemoteId)
|
||||
return
|
||||
}
|
||||
|
||||
rc, appErr := c.App.GetRemoteCluster(frame.RemoteId)
|
||||
if appErr != nil {
|
||||
c.SetInvalidRemoteIdError(frame.RemoteId)
|
||||
return
|
||||
}
|
||||
|
||||
var ping model.RemoteClusterPing
|
||||
if err := json.Unmarshal(frame.Msg.Payload, &ping); err != nil {
|
||||
c.SetInvalidParamWithErr("msg.payload", err)
|
||||
return
|
||||
}
|
||||
ping.RecvAt = model.GetMillis()
|
||||
|
||||
if metrics := c.App.Metrics(); metrics != nil {
|
||||
metrics.IncrementRemoteClusterMsgReceivedCounter(rc.RemoteId)
|
||||
}
|
||||
|
||||
err := json.NewEncoder(w).Encode(ping)
|
||||
if err != nil {
|
||||
c.Logger.Warn("Error writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func remoteClusterAcceptMessage(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// make sure remote cluster service is running.
|
||||
service, appErr := c.App.GetRemoteClusterService()
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
var frame model.RemoteClusterFrame
|
||||
if err := json.NewDecoder(r.Body).Decode(&frame); err != nil {
|
||||
c.Err = model.NewAppError("remoteClusterAcceptMessage", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
appErr = frame.IsValid()
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("remoteClusterAcceptMessage", audit.Fail)
|
||||
audit.AddEventParameterAuditable(auditRec, "remote_cluster_frame", &frame)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
remoteId := c.GetRemoteID(r)
|
||||
if remoteId != frame.RemoteId {
|
||||
c.SetInvalidRemoteIdError(frame.RemoteId)
|
||||
return
|
||||
}
|
||||
|
||||
rc, appErr := c.App.GetRemoteCluster(frame.RemoteId)
|
||||
if appErr != nil {
|
||||
c.SetInvalidRemoteIdError(frame.RemoteId)
|
||||
return
|
||||
}
|
||||
audit.AddEventParameterAuditable(auditRec, "remote_cluster", rc)
|
||||
|
||||
// pass message to Remote Cluster Service and write response
|
||||
resp := service.ReceiveIncomingMsg(rc, frame.Msg)
|
||||
|
||||
b, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("remoteClusterAcceptMessage", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func remoteClusterConfirmInvite(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// make sure remote cluster service is running.
|
||||
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
var frame model.RemoteClusterFrame
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&frame); jsonErr != nil {
|
||||
c.Err = model.NewAppError("remoteClusterConfirmInvite", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
if appErr := frame.IsValid(); appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("remoteClusterAcceptInvite", audit.Fail)
|
||||
audit.AddEventParameterAuditable(auditRec, "remote_cluster_frame", &frame)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
remoteId := c.GetRemoteID(r)
|
||||
if remoteId != frame.RemoteId {
|
||||
c.SetInvalidRemoteIdError(frame.RemoteId)
|
||||
return
|
||||
}
|
||||
|
||||
rc, err := c.App.GetRemoteCluster(frame.RemoteId)
|
||||
if err != nil {
|
||||
c.SetInvalidRemoteIdError(frame.RemoteId)
|
||||
return
|
||||
}
|
||||
audit.AddEventParameterAuditable(auditRec, "remote_cluster", rc)
|
||||
|
||||
if time.Since(model.GetTimeForMillis(rc.CreateAt)) > remotecluster.InviteExpiresAfter {
|
||||
c.Err = model.NewAppError("remoteClusterAcceptMessage", "api.context.invitation_expired.error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var confirm model.RemoteClusterInvite
|
||||
if jsonErr := json.Unmarshal(frame.Msg.Payload, &confirm); jsonErr != nil {
|
||||
c.SetInvalidParam("msg.payload")
|
||||
return
|
||||
}
|
||||
|
||||
rc.RemoteTeamId = confirm.RemoteTeamId
|
||||
rc.SiteURL = confirm.SiteURL
|
||||
rc.RemoteToken = confirm.Token
|
||||
|
||||
if _, err := c.App.UpdateRemoteCluster(rc); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func uploadRemoteData(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !*c.App.Config().FileSettings.EnableFileAttachments {
|
||||
c.Err = model.NewAppError("uploadRemoteData", "api.file.attachments.disabled.app_error",
|
||||
nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireUploadId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("uploadRemoteData", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "upload_id", c.Params.UploadId)
|
||||
|
||||
c.AppContext.SetContext(app.WithMaster(c.AppContext.Context()))
|
||||
us, err := c.App.GetUploadSession(c.AppContext, c.Params.UploadId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if us.RemoteId != c.GetRemoteID(r) {
|
||||
c.Err = model.NewAppError("uploadRemoteData", "api.context.remote_id_mismatch.app_error",
|
||||
nil, "", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
info, err := doUploadData(c, us, r)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
if info == nil {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(info); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func remoteSetProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
defer io.Copy(io.Discard, r.Body)
|
||||
|
||||
c.RequireUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if *c.App.Config().FileSettings.DriverName == "" {
|
||||
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.storage.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
if r.ContentLength > *c.App.Config().FileSettings.MaxFileSize {
|
||||
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.too_large.app_error", nil, "", http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize); err != nil {
|
||||
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.parse.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
m := r.MultipartForm
|
||||
imageArray, ok := m.File["image"]
|
||||
if !ok {
|
||||
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.no_file.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(imageArray) == 0 {
|
||||
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.array.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("remoteUploadProfileImage", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
if imageArray[0] != nil {
|
||||
audit.AddEventParameter(auditRec, "filename", imageArray[0].Filename)
|
||||
}
|
||||
|
||||
user, err := c.App.GetUser(c.Params.UserId)
|
||||
if err != nil || !user.IsRemote() {
|
||||
c.SetInvalidURLParam("user_id")
|
||||
return
|
||||
}
|
||||
audit.AddEventParameterAuditable(auditRec, "user", user)
|
||||
|
||||
imageData := imageArray[0]
|
||||
if err := c.App.SetProfileImage(c.AppContext, c.Params.UserId, imageData); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
c.LogAudit("")
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
220
api4/role.go
220
api4/role.go
|
|
@ -1,220 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
var notAllowedPermissions = []string{
|
||||
model.PermissionSysconsoleWriteUserManagementSystemRoles.Id,
|
||||
model.PermissionSysconsoleReadUserManagementSystemRoles.Id,
|
||||
model.PermissionManageRoles.Id,
|
||||
}
|
||||
|
||||
func (api *API) InitRole() {
|
||||
api.BaseRoutes.Roles.Handle("", api.APISessionRequired(getAllRoles)).Methods("GET")
|
||||
api.BaseRoutes.Roles.Handle("/{role_id:[A-Za-z0-9]+}", api.APISessionRequiredTrustRequester(getRole)).Methods("GET")
|
||||
api.BaseRoutes.Roles.Handle("/name/{role_name:[a-z0-9_]+}", api.APISessionRequiredTrustRequester(getRoleByName)).Methods("GET")
|
||||
api.BaseRoutes.Roles.Handle("/names", api.APISessionRequiredTrustRequester(getRolesByNames)).Methods("POST")
|
||||
api.BaseRoutes.Roles.Handle("/{role_id:[A-Za-z0-9]+}/patch", api.APISessionRequired(patchRole)).Methods("PUT")
|
||||
}
|
||||
|
||||
func getAllRoles(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
roles, appErr := c.App.GetAllRoles()
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(roles)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getAllRoles", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func getRole(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireRoleId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
role, err := c.App.GetRole(c.Params.RoleId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(role); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getRoleByName(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireRoleName()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
role, err := c.App.GetRoleByName(r.Context(), c.Params.RoleName)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(role); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getRolesByNames(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
rolenames := model.ArrayFromJSON(r.Body)
|
||||
|
||||
if len(rolenames) == 0 {
|
||||
c.SetInvalidParam("rolenames")
|
||||
return
|
||||
}
|
||||
|
||||
cleanedRoleNames, valid := model.CleanRoleNames(rolenames)
|
||||
if !valid {
|
||||
c.SetInvalidParam("rolename")
|
||||
return
|
||||
}
|
||||
|
||||
roles, appErr := c.App.GetRolesByNames(cleanedRoleNames)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(roles)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getRolesByNames", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func patchRole(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireRoleId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var patch model.RolePatch
|
||||
if err := json.NewDecoder(r.Body).Decode(&patch); err != nil {
|
||||
c.SetInvalidParamWithErr("role", err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("patchRole", audit.Fail)
|
||||
audit.AddEventParameterAuditable(auditRec, "role_patch", &patch)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
oldRole, appErr := c.App.GetRole(c.Params.RoleId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
auditRec.AddEventPriorState(oldRole)
|
||||
auditRec.AddEventObjectType("role")
|
||||
|
||||
// manage_system permission is required to patch system_admin
|
||||
requiredPermission := model.PermissionSysconsoleWriteUserManagementPermissions
|
||||
specialProtectedSystemRoles := append(model.NewSystemRoleIDs, model.SystemAdminRoleId)
|
||||
for _, roleID := range specialProtectedSystemRoles {
|
||||
if oldRole.Name == roleID {
|
||||
requiredPermission = model.PermissionManageSystem
|
||||
}
|
||||
}
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), requiredPermission) {
|
||||
c.SetPermissionError(requiredPermission)
|
||||
return
|
||||
}
|
||||
|
||||
isGuest := oldRole.Name == model.SystemGuestRoleId || oldRole.Name == model.TeamGuestRoleId || oldRole.Name == model.ChannelGuestRoleId
|
||||
if c.App.Channels().License() == nil && patch.Permissions != nil {
|
||||
if isGuest {
|
||||
c.Err = model.NewAppError("Api4.PatchRoles", "api.roles.patch_roles.license.error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Licensed instances can not change permissions in the blacklist set.
|
||||
if patch.Permissions != nil {
|
||||
deltaPermissions := model.PermissionsChangedByPatch(oldRole, &patch)
|
||||
|
||||
for _, permission := range deltaPermissions {
|
||||
notAllowed := false
|
||||
for _, notAllowedPermission := range notAllowedPermissions {
|
||||
if permission == notAllowedPermission {
|
||||
notAllowed = true
|
||||
}
|
||||
}
|
||||
|
||||
if notAllowed {
|
||||
c.Err = model.NewAppError("Api4.PatchRoles", "api.roles.patch_roles.not_allowed_permission.error", nil, "Cannot add or remove permission: "+permission, http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
*patch.Permissions = model.RemoveDuplicateStrings(*patch.Permissions)
|
||||
}
|
||||
|
||||
if c.App.Channels().License() != nil && isGuest && !*c.App.Channels().License().Features.GuestAccountsPermissions {
|
||||
c.Err = model.NewAppError("Api4.PatchRoles", "api.roles.patch_roles.license.error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
if oldRole.Name == model.TeamAdminRoleId ||
|
||||
oldRole.Name == model.ChannelAdminRoleId ||
|
||||
oldRole.Name == model.SystemUserRoleId ||
|
||||
oldRole.Name == model.TeamUserRoleId ||
|
||||
oldRole.Name == model.ChannelUserRoleId ||
|
||||
oldRole.Name == model.SystemGuestRoleId ||
|
||||
oldRole.Name == model.TeamGuestRoleId ||
|
||||
oldRole.Name == model.ChannelGuestRoleId ||
|
||||
oldRole.Name == model.PlaybookAdminRoleId ||
|
||||
oldRole.Name == model.PlaybookMemberRoleId ||
|
||||
oldRole.Name == model.RunAdminRoleId ||
|
||||
oldRole.Name == model.RunMemberRoleId {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementPermissions) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementPermissions)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementSystemRoles) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementSystemRoles)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
role, appErr := c.App.PatchRole(oldRole, &patch)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventResultState(role)
|
||||
auditRec.Success()
|
||||
c.LogAudit("")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(role); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
294
api4/saml.go
294
api4/saml.go
|
|
@ -1,294 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitSaml() {
|
||||
api.BaseRoutes.SAML.Handle("/metadata", api.APIHandler(getSamlMetadata)).Methods("GET")
|
||||
|
||||
api.BaseRoutes.SAML.Handle("/certificate/public", api.APISessionRequired(addSamlPublicCertificate)).Methods("POST")
|
||||
api.BaseRoutes.SAML.Handle("/certificate/private", api.APISessionRequired(addSamlPrivateCertificate)).Methods("POST")
|
||||
api.BaseRoutes.SAML.Handle("/certificate/idp", api.APISessionRequired(addSamlIdpCertificate)).Methods("POST")
|
||||
|
||||
api.BaseRoutes.SAML.Handle("/certificate/public", api.APISessionRequired(removeSamlPublicCertificate)).Methods("DELETE")
|
||||
api.BaseRoutes.SAML.Handle("/certificate/private", api.APISessionRequired(removeSamlPrivateCertificate)).Methods("DELETE")
|
||||
api.BaseRoutes.SAML.Handle("/certificate/idp", api.APISessionRequired(removeSamlIdpCertificate)).Methods("DELETE")
|
||||
|
||||
api.BaseRoutes.SAML.Handle("/certificate/status", api.APISessionRequired(getSamlCertificateStatus)).Methods("GET")
|
||||
|
||||
api.BaseRoutes.SAML.Handle("/metadatafromidp", api.APIHandler(getSamlMetadataFromIdp)).Methods("POST")
|
||||
|
||||
api.BaseRoutes.SAML.Handle("/reset_auth_data", api.APISessionRequired(resetAuthDataToEmail)).Methods("POST")
|
||||
}
|
||||
|
||||
func (api *API) InitSamlLocal() {
|
||||
api.BaseRoutes.SAML.Handle("/reset_auth_data", api.APILocal(resetAuthDataToEmail)).Methods("POST")
|
||||
}
|
||||
|
||||
func getSamlMetadata(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
metadata, err := c.App.GetSamlMetadata()
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\"metadata.xml\"")
|
||||
w.Write([]byte(metadata))
|
||||
}
|
||||
|
||||
func parseSamlCertificateRequest(r *http.Request, maxFileSize int64) (*multipart.FileHeader, *model.AppError) {
|
||||
err := r.ParseMultipartForm(maxFileSize)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("addSamlCertificate", "api.admin.add_certificate.no_file.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
m := r.MultipartForm
|
||||
|
||||
fileArray, ok := m.File["certificate"]
|
||||
if !ok {
|
||||
return nil, model.NewAppError("addSamlCertificate", "api.admin.add_certificate.no_file.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if len(fileArray) <= 0 {
|
||||
return nil, model.NewAppError("addSamlCertificate", "api.admin.add_certificate.array.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return fileArray[0], nil
|
||||
}
|
||||
|
||||
func addSamlPublicCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionAddSamlPublicCert) {
|
||||
c.SetPermissionError(model.PermissionAddSamlPublicCert)
|
||||
return
|
||||
}
|
||||
|
||||
fileData, err := parseSamlCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("addSamlPublicCertificate", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
|
||||
|
||||
if err := c.App.AddSamlPublicCertificate(fileData); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func addSamlPrivateCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionAddSamlPrivateCert) {
|
||||
c.SetPermissionError(model.PermissionAddSamlPrivateCert)
|
||||
return
|
||||
}
|
||||
|
||||
fileData, err := parseSamlCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("addSamlPrivateCertificate", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
|
||||
|
||||
if err := c.App.AddSamlPrivateCertificate(fileData); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func addSamlIdpCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionAddSamlIdpCert) {
|
||||
c.SetPermissionError(model.PermissionAddSamlIdpCert)
|
||||
return
|
||||
}
|
||||
|
||||
v := r.Header.Get("Content-Type")
|
||||
if v == "" {
|
||||
c.Err = model.NewAppError("addSamlIdpCertificate", "api.admin.saml.set_certificate_from_metadata.missing_content_type.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
d, _, err := mime.ParseMediaType(v)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("addSamlIdpCertificate", "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("addSamlIdpCertificate", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
auditRec.AddMeta("type", d)
|
||||
|
||||
if d == "application/x-pem-file" {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("addSamlIdpCertificate", "api.admin.saml.set_certificate_from_metadata.invalid_body.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.SetSamlIdpCertificateFromMetadata(body); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
} else if d == "multipart/form-data" {
|
||||
fileData, err := parseSamlCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
|
||||
|
||||
if err := c.App.AddSamlIdpCertificate(fileData); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
} else {
|
||||
c.Err = model.NewAppError("addSamlIdpCertificate", "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func removeSamlPublicCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRemoveSamlPublicCert) {
|
||||
c.SetPermissionError(model.PermissionRemoveSamlPublicCert)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("removeSamlPublicCertificate", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if err := c.App.RemoveSamlPublicCertificate(); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func removeSamlPrivateCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRemoveSamlPrivateCert) {
|
||||
c.SetPermissionError(model.PermissionRemoveSamlPrivateCert)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("removeSamlPrivateCertificate", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if err := c.App.RemoveSamlPrivateCertificate(); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func removeSamlIdpCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRemoveSamlIdpCert) {
|
||||
c.SetPermissionError(model.PermissionRemoveSamlIdpCert)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("removeSamlIdpCertificate", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if err := c.App.RemoveSamlIdpCertificate(); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func getSamlCertificateStatus(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionGetSamlCertStatus) {
|
||||
c.SetPermissionError(model.PermissionGetSamlCertStatus)
|
||||
return
|
||||
}
|
||||
|
||||
status := c.App.GetSamlCertificateStatus()
|
||||
if err := json.NewEncoder(w).Encode(status); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getSamlMetadataFromIdp(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionGetSamlMetadataFromIdp) {
|
||||
c.SetPermissionError(model.PermissionGetSamlMetadataFromIdp)
|
||||
return
|
||||
}
|
||||
|
||||
props := model.MapFromJSON(r.Body)
|
||||
url := props["saml_metadata_url"]
|
||||
if url == "" {
|
||||
c.SetInvalidParam("saml_metadata_url")
|
||||
return
|
||||
}
|
||||
|
||||
metadata, err := c.App.GetSamlMetadataFromIdp(url)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getSamlMetadataFromIdp", "api.admin.saml.failure_get_metadata_from_idp.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(metadata); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func resetAuthDataToEmail(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
type ResetAuthDataParams struct {
|
||||
IncludeDeleted bool `json:"include_deleted"`
|
||||
DryRun bool `json:"dry_run"`
|
||||
SpecifiedUserIDs []string `json:"user_ids"`
|
||||
}
|
||||
var params *ResetAuthDataParams
|
||||
jsonErr := json.NewDecoder(r.Body).Decode(¶ms)
|
||||
if jsonErr != nil {
|
||||
c.Err = model.NewAppError("resetAuthDataToEmail", "model.utils.decode_json.app_error", nil, "", http.StatusBadRequest).Wrap(jsonErr)
|
||||
return
|
||||
}
|
||||
numAffected, appErr := c.App.ResetSamlAuthDataToEmail(params.IncludeDeleted, params.DryRun, params.SpecifiedUserIDs)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
n := struct {
|
||||
NumAffected int `json:"num_affected"`
|
||||
}{
|
||||
NumAffected: numAffected,
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(n); err != nil {
|
||||
c.Logger.Warn("Error writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
263
api4/scheme.go
263
api4/scheme.go
|
|
@ -1,263 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitScheme() {
|
||||
api.BaseRoutes.Schemes.Handle("", api.APISessionRequired(getSchemes)).Methods("GET")
|
||||
api.BaseRoutes.Schemes.Handle("", api.APISessionRequired(createScheme)).Methods("POST")
|
||||
api.BaseRoutes.Schemes.Handle("/{scheme_id:[A-Za-z0-9]+}", api.APISessionRequired(deleteScheme)).Methods("DELETE")
|
||||
api.BaseRoutes.Schemes.Handle("/{scheme_id:[A-Za-z0-9]+}", api.APISessionRequiredTrustRequester(getScheme)).Methods("GET")
|
||||
api.BaseRoutes.Schemes.Handle("/{scheme_id:[A-Za-z0-9]+}/patch", api.APISessionRequired(patchScheme)).Methods("PUT")
|
||||
api.BaseRoutes.Schemes.Handle("/{scheme_id:[A-Za-z0-9]+}/teams", api.APISessionRequiredTrustRequester(getTeamsForScheme)).Methods("GET")
|
||||
api.BaseRoutes.Schemes.Handle("/{scheme_id:[A-Za-z0-9]+}/channels", api.APISessionRequiredTrustRequester(getChannelsForScheme)).Methods("GET")
|
||||
}
|
||||
|
||||
func createScheme(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var scheme model.Scheme
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&scheme); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("scheme", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("createScheme", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameterAuditable(auditRec, "scheme", &scheme)
|
||||
|
||||
if c.App.Channels().License() == nil || (!*c.App.Channels().License().Features.CustomPermissionsSchemes && c.App.Channels().License().SkuShortName != model.LicenseShortSkuProfessional) {
|
||||
c.Err = model.NewAppError("Api4.CreateScheme", "api.scheme.create_scheme.license.error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementPermissions) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementPermissions)
|
||||
return
|
||||
}
|
||||
|
||||
returnedScheme, err := c.App.CreateScheme(&scheme)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(returnedScheme)
|
||||
auditRec.AddEventObjectType("scheme")
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(returnedScheme); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getScheme(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireSchemeId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementPermissions) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementPermissions)
|
||||
return
|
||||
}
|
||||
|
||||
scheme, err := c.App.GetScheme(c.Params.SchemeId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(scheme); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getSchemes(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementPermissions) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementPermissions)
|
||||
return
|
||||
}
|
||||
|
||||
scope := c.Params.Scope
|
||||
if scope != "" && scope != model.SchemeScopeTeam && scope != model.SchemeScopeChannel {
|
||||
c.SetInvalidParam("scope")
|
||||
return
|
||||
}
|
||||
|
||||
schemes, appErr := c.App.GetSchemesPage(c.Params.Scope, c.Params.Page, c.Params.PerPage)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(schemes)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getSchemes", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func getTeamsForScheme(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireSchemeId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementTeams) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementTeams)
|
||||
return
|
||||
}
|
||||
|
||||
scheme, appErr := c.App.GetScheme(c.Params.SchemeId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if scheme.Scope != model.SchemeScopeTeam {
|
||||
c.Err = model.NewAppError("Api4.GetTeamsForScheme", "api.scheme.get_teams_for_scheme.scope.error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
teams, appErr := c.App.GetTeamsForSchemePage(scheme, c.Params.Page, c.Params.PerPage)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(teams)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getTeamsForScheme", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func getChannelsForScheme(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireSchemeId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementChannels) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementChannels)
|
||||
return
|
||||
}
|
||||
|
||||
scheme, err := c.App.GetScheme(c.Params.SchemeId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if scheme.Scope != model.SchemeScopeChannel {
|
||||
c.Err = model.NewAppError("Api4.GetChannelsForScheme", "api.scheme.get_channels_for_scheme.scope.error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
channels, err := c.App.GetChannelsForSchemePage(scheme, c.Params.Page, c.Params.PerPage)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(channels); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func patchScheme(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireSchemeId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var patch model.SchemePatch
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&patch); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("scheme", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("patchScheme", audit.Fail)
|
||||
audit.AddEventParameterAuditable(auditRec, "scheme_patch", &patch)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if c.App.Channels().License() == nil || (!*c.App.Channels().License().Features.CustomPermissionsSchemes && c.App.Channels().License().SkuShortName != model.LicenseShortSkuProfessional) {
|
||||
c.Err = model.NewAppError("Api4.PatchScheme", "api.scheme.patch_scheme.license.error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
audit.AddEventParameter(auditRec, "scheme_id", c.Params.SchemeId)
|
||||
|
||||
scheme, err := c.App.GetScheme(c.Params.SchemeId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
auditRec.AddEventPriorState(scheme)
|
||||
auditRec.AddEventObjectType("scheme")
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementPermissions) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementPermissions)
|
||||
return
|
||||
}
|
||||
|
||||
scheme, err = c.App.PatchScheme(scheme, &patch)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
auditRec.AddEventResultState(scheme)
|
||||
|
||||
auditRec.Success()
|
||||
c.LogAudit("")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(scheme); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func deleteScheme(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireSchemeId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("deleteScheme", audit.Fail)
|
||||
audit.AddEventParameter(auditRec, "scheme_id", c.Params.SchemeId)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if c.App.Channels().License() == nil || (!*c.App.Channels().License().Features.CustomPermissionsSchemes && c.App.Channels().License().SkuShortName != model.LicenseShortSkuProfessional) {
|
||||
c.Err = model.NewAppError("Api4.DeleteScheme", "api.scheme.delete_scheme.license.error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementPermissions) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementPermissions)
|
||||
return
|
||||
}
|
||||
|
||||
scheme, err := c.App.DeleteScheme(c.Params.SchemeId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventResultState(scheme)
|
||||
auditRec.AddEventObjectType("scheme")
|
||||
auditRec.Success()
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
212
api4/status.go
212
api4/status.go
|
|
@ -1,212 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitStatus() {
|
||||
api.BaseRoutes.User.Handle("/status", api.APISessionRequired(getUserStatus)).Methods("GET")
|
||||
api.BaseRoutes.Users.Handle("/status/ids", api.APISessionRequired(getUserStatusesByIds)).Methods("POST")
|
||||
api.BaseRoutes.User.Handle("/status", api.APISessionRequired(updateUserStatus)).Methods("PUT")
|
||||
api.BaseRoutes.User.Handle("/status/custom", api.APISessionRequired(updateUserCustomStatus)).Methods("PUT")
|
||||
api.BaseRoutes.User.Handle("/status/custom", api.APISessionRequired(removeUserCustomStatus)).Methods("DELETE")
|
||||
|
||||
// Both these handlers are for removing the recent custom status but the one with the POST method should be preferred
|
||||
// as DELETE method doesn't support request body in the mobile app.
|
||||
api.BaseRoutes.User.Handle("/status/custom/recent", api.APISessionRequired(removeUserRecentCustomStatus)).Methods("DELETE")
|
||||
api.BaseRoutes.User.Handle("/status/custom/recent/delete", api.APISessionRequired(removeUserRecentCustomStatus)).Methods("POST")
|
||||
}
|
||||
|
||||
func getUserStatus(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// No permission check required
|
||||
|
||||
statusMap, err := c.App.GetUserStatusesByIds([]string{c.Params.UserId})
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if len(statusMap) == 0 {
|
||||
c.Err = model.NewAppError("UserStatus", "api.status.user_not_found.app_error", nil, "", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(statusMap[0]); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getUserStatusesByIds(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
userIds := model.ArrayFromJSON(r.Body)
|
||||
|
||||
if len(userIds) == 0 {
|
||||
c.SetInvalidParam("user_ids")
|
||||
return
|
||||
}
|
||||
|
||||
for _, userId := range userIds {
|
||||
if len(userId) != 26 {
|
||||
c.SetInvalidParam("user_ids")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// No permission check required
|
||||
statuses, appErr := c.App.GetUserStatusesByIds(userIds)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(statuses)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getUserStatusesByIds", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func updateUserStatus(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var status model.Status
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&status); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("status", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
// The user being updated in the payload must be the same one as indicated in the URL.
|
||||
if status.UserId != c.Params.UserId {
|
||||
c.SetInvalidParam("user_id")
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
|
||||
currentStatus, err := c.App.GetStatus(c.Params.UserId)
|
||||
if err == nil && currentStatus.Status == model.StatusOutOfOffice && status.Status != model.StatusOutOfOffice {
|
||||
c.App.DisableAutoResponder(c.AppContext, c.Params.UserId, c.IsSystemAdmin())
|
||||
}
|
||||
|
||||
switch status.Status {
|
||||
case "online":
|
||||
c.App.SetStatusOnline(c.Params.UserId, true)
|
||||
case "offline":
|
||||
c.App.SetStatusOffline(c.Params.UserId, true)
|
||||
case "away":
|
||||
c.App.SetStatusAwayIfNeeded(c.Params.UserId, true)
|
||||
case "dnd":
|
||||
c.App.SetStatusDoNotDisturbTimed(c.Params.UserId, status.DNDEndTime)
|
||||
default:
|
||||
c.SetInvalidParam("status")
|
||||
return
|
||||
}
|
||||
|
||||
getUserStatus(c, w, r)
|
||||
}
|
||||
|
||||
func updateUserCustomStatus(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !*c.App.Config().TeamSettings.EnableCustomUserStatuses {
|
||||
c.Err = model.NewAppError("updateUserCustomStatus", "api.custom_status.disabled", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
var customStatus model.CustomStatus
|
||||
jsonErr := json.NewDecoder(r.Body).Decode(&customStatus)
|
||||
if jsonErr != nil || (customStatus.Emoji == "" && customStatus.Text == "") || !customStatus.AreDurationAndExpirationTimeValid() {
|
||||
c.SetInvalidParamWithErr("custom_status", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
|
||||
customStatus.PreSave()
|
||||
err := c.App.SetCustomStatus(c.AppContext, c.Params.UserId, &customStatus)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func removeUserCustomStatus(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !*c.App.Config().TeamSettings.EnableCustomUserStatuses {
|
||||
c.Err = model.NewAppError("removeUserCustomStatus", "api.custom_status.disabled", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.RemoveCustomStatus(c.AppContext, c.Params.UserId); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func removeUserRecentCustomStatus(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !*c.App.Config().TeamSettings.EnableCustomUserStatuses {
|
||||
c.Err = model.NewAppError("removeUserRecentCustomStatus", "api.custom_status.disabled", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
var recentCustomStatus model.CustomStatus
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&recentCustomStatus); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("recent_custom_status", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.RemoveRecentCustomStatus(c.Params.UserId, &recentCustomStatus); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
1058
api4/system.go
1058
api4/system.go
File diff suppressed because it is too large
Load diff
1018
api4/system_test.go
1018
api4/system_test.go
File diff suppressed because it is too large
Load diff
1850
api4/team.go
1850
api4/team.go
File diff suppressed because it is too large
Load diff
3862
api4/team_test.go
3862
api4/team_test.go
File diff suppressed because it is too large
Load diff
|
|
@ -1,78 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app"
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitTermsOfService() {
|
||||
api.BaseRoutes.TermsOfService.Handle("", api.APISessionRequired(getLatestTermsOfService)).Methods("GET")
|
||||
api.BaseRoutes.TermsOfService.Handle("", api.APISessionRequired(createTermsOfService)).Methods("POST")
|
||||
}
|
||||
|
||||
func getLatestTermsOfService(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
termsOfService, err := c.App.GetLatestTermsOfService()
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(termsOfService); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func createTermsOfService(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
if license := c.App.Channels().License(); license == nil || !*license.Features.CustomTermsOfService {
|
||||
c.Err = model.NewAppError("createTermsOfService", "api.create_terms_of_service.custom_terms_of_service_disabled.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("createTermsOfService", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
props := model.MapFromJSON(r.Body)
|
||||
text := props["text"]
|
||||
userId := c.AppContext.Session().UserId
|
||||
|
||||
if text == "" {
|
||||
c.Err = model.NewAppError("Config.IsValid", "api.create_terms_of_service.empty_text.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
oldTermsOfService, err := c.App.GetLatestTermsOfService()
|
||||
if err != nil && err.Id != app.ErrorTermsOfServiceNoRowsFound {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if oldTermsOfService == nil || oldTermsOfService.Text != text {
|
||||
termsOfService, err := c.App.CreateTermsOfService(text, userId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(termsOfService); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
} else {
|
||||
if err := json.NewEncoder(w).Encode(oldTermsOfService); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
auditRec.Success()
|
||||
}
|
||||
193
api4/upload.go
193
api4/upload.go
|
|
@ -1,193 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app"
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitUpload() {
|
||||
api.BaseRoutes.Uploads.Handle("", api.APISessionRequired(createUpload)).Methods("POST")
|
||||
api.BaseRoutes.Upload.Handle("", api.APISessionRequired(getUpload)).Methods("GET")
|
||||
api.BaseRoutes.Upload.Handle("", api.APISessionRequired(uploadData)).Methods("POST")
|
||||
}
|
||||
|
||||
func createUpload(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !*c.App.Config().FileSettings.EnableFileAttachments {
|
||||
c.Err = model.NewAppError("createUpload",
|
||||
"api.file.attachments.disabled.app_error",
|
||||
nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
var us model.UploadSession
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&us); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("upload", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
// these are not supported for client uploads; shared channels only.
|
||||
us.RemoteId = ""
|
||||
us.ReqFileId = ""
|
||||
|
||||
auditRec := c.MakeAuditRecord("createUpload", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameterAuditable(auditRec, "upload", &us)
|
||||
|
||||
if us.Type == model.UploadTypeImport {
|
||||
if !c.IsSystemAdmin() {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
if c.App.Srv().License().IsCloud() {
|
||||
c.Err = model.NewAppError("createUpload", "api.file.cloud_upload.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), us.ChannelId, model.PermissionUploadFile) {
|
||||
c.SetPermissionError(model.PermissionUploadFile)
|
||||
return
|
||||
}
|
||||
us.Type = model.UploadTypeAttachment
|
||||
}
|
||||
|
||||
us.Id = model.NewId()
|
||||
if c.AppContext.Session().UserId != "" {
|
||||
us.UserId = c.AppContext.Session().UserId
|
||||
}
|
||||
|
||||
if us.FileSize > *c.App.Config().FileSettings.MaxFileSize {
|
||||
c.Err = model.NewAppError("createUpload", "api.upload.create.upload_too_large.app_error",
|
||||
map[string]any{"channelId": us.ChannelId}, "", http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
|
||||
rus, err := c.App.CreateUploadSession(c.AppContext, &us)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(rus); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getUpload(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUploadId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
us, err := c.App.GetUploadSession(c.AppContext, c.Params.UploadId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if us.UserId != c.AppContext.Session().UserId && !c.IsSystemAdmin() {
|
||||
c.Err = model.NewAppError("getUpload", "api.upload.get_upload.forbidden.app_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(us); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func uploadData(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !*c.App.Config().FileSettings.EnableFileAttachments {
|
||||
c.Err = model.NewAppError("uploadData", "api.file.attachments.disabled.app_error",
|
||||
nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireUploadId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("uploadData", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "upload_id", c.Params.UploadId)
|
||||
|
||||
c.AppContext.SetContext(app.WithMaster(c.AppContext.Context()))
|
||||
us, err := c.App.GetUploadSession(c.AppContext, c.Params.UploadId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if us.Type == model.UploadTypeImport {
|
||||
if !c.IsSystemAdmin() {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
if c.App.Srv().License().IsCloud() {
|
||||
c.Err = model.NewAppError("UploadData", "api.file.cloud_upload.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if us.UserId != c.AppContext.Session().UserId || !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), us.ChannelId, model.PermissionUploadFile) {
|
||||
c.SetPermissionError(model.PermissionUploadFile)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
info, err := doUploadData(c, us, r)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
if info == nil {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(info); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func doUploadData(c *Context, us *model.UploadSession, r *http.Request) (*model.FileInfo, *model.AppError) {
|
||||
boundary, parseErr := parseMultipartRequestHeader(r)
|
||||
if parseErr != nil && !errors.Is(parseErr, http.ErrNotMultipart) {
|
||||
return nil, model.NewAppError("uploadData", "api.upload.upload_data.invalid_content_type",
|
||||
nil, parseErr.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
var rd io.Reader
|
||||
if boundary != "" {
|
||||
mr := multipart.NewReader(r.Body, boundary)
|
||||
p, partErr := mr.NextPart()
|
||||
if partErr != nil {
|
||||
return nil, model.NewAppError("uploadData", "api.upload.upload_data.multipart_error",
|
||||
nil, partErr.Error(), http.StatusBadRequest)
|
||||
}
|
||||
rd = p
|
||||
} else {
|
||||
if r.ContentLength > (us.FileSize - us.FileOffset) {
|
||||
return nil, model.NewAppError("uploadData", "api.upload.upload_data.invalid_content_length",
|
||||
nil, "", http.StatusBadRequest)
|
||||
}
|
||||
rd = r.Body
|
||||
}
|
||||
|
||||
return c.App.UploadData(c.AppContext, us, rd)
|
||||
}
|
||||
|
|
@ -1,429 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/utils/fileutils"
|
||||
)
|
||||
|
||||
func TestCreateUpload(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
us := &model.UploadSession{
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
Filename: "upload",
|
||||
FileSize: 8 * 1024 * 1024,
|
||||
}
|
||||
|
||||
t.Run("file attachments disabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnableFileAttachments = false })
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnableFileAttachments = true })
|
||||
u, resp, err := th.Client.CreateUpload(us)
|
||||
require.Nil(t, u)
|
||||
CheckErrorID(t, err, "api.file.attachments.disabled.app_error")
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("no permissions", func(t *testing.T) {
|
||||
us.ChannelId = th.BasicPrivateChannel2.Id
|
||||
u, resp, err := th.Client.CreateUpload(us)
|
||||
require.Nil(t, u)
|
||||
CheckErrorID(t, err, "api.context.permissions.app_error")
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("FileSize over limit", func(t *testing.T) {
|
||||
maxFileSize := *th.App.Config().FileSettings.MaxFileSize
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.MaxFileSize = us.FileSize - 1 })
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.MaxFileSize = maxFileSize })
|
||||
us.ChannelId = th.BasicChannel.Id
|
||||
u, resp, err := th.Client.CreateUpload(us)
|
||||
require.Nil(t, u)
|
||||
CheckErrorID(t, err, "api.upload.create.upload_too_large.app_error")
|
||||
require.Equal(t, http.StatusRequestEntityTooLarge, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("not allowed in cloud", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
defer th.App.Srv().RemoveLicense()
|
||||
|
||||
u, resp, err := th.SystemAdminClient.CreateUpload(&model.UploadSession{
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
Filename: "upload",
|
||||
FileSize: 8 * 1024 * 1024,
|
||||
Type: model.UploadTypeImport,
|
||||
})
|
||||
require.Nil(t, u)
|
||||
CheckErrorID(t, err, "api.file.cloud_upload.app_error")
|
||||
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
us.ChannelId = th.BasicChannel.Id
|
||||
u, resp, err := th.Client.CreateUpload(us)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, u)
|
||||
require.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("import file", func(t *testing.T) {
|
||||
testsDir, _ := fileutils.FindDir("tests")
|
||||
|
||||
importFile, err := os.Open(testsDir + "/import_test.zip")
|
||||
require.NoError(t, err)
|
||||
defer importFile.Close()
|
||||
|
||||
info, err := importFile.Stat()
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("permissions error", func(t *testing.T) {
|
||||
us := &model.UploadSession{
|
||||
Filename: info.Name(),
|
||||
FileSize: info.Size(),
|
||||
Type: model.UploadTypeImport,
|
||||
}
|
||||
u, resp, err := th.Client.CreateUpload(us)
|
||||
require.Nil(t, u)
|
||||
CheckErrorID(t, err, "api.context.permissions.app_error")
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
us := &model.UploadSession{
|
||||
Filename: info.Name(),
|
||||
FileSize: info.Size(),
|
||||
Type: model.UploadTypeImport,
|
||||
}
|
||||
u, _, err := th.SystemAdminClient.CreateUpload(us)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, u)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetUpload(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
us := &model.UploadSession{
|
||||
Id: model.NewId(),
|
||||
Type: model.UploadTypeAttachment,
|
||||
CreateAt: model.GetMillis(),
|
||||
UserId: th.BasicUser2.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
Filename: "upload",
|
||||
FileSize: 8 * 1024 * 1024,
|
||||
}
|
||||
us, err := th.App.CreateUploadSession(th.Context, us)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, us)
|
||||
require.NotEmpty(t, us)
|
||||
|
||||
t.Run("upload not found", func(t *testing.T) {
|
||||
u, resp, err := th.Client.GetUpload(model.NewId())
|
||||
require.Nil(t, u)
|
||||
CheckErrorID(t, err, "app.upload.get.app_error")
|
||||
require.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("no permissions", func(t *testing.T) {
|
||||
u, _, err := th.Client.GetUpload(us.Id)
|
||||
require.Nil(t, u)
|
||||
CheckErrorID(t, err, "api.upload.get_upload.forbidden.app_error")
|
||||
})
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
expected, resp, err := th.Client.CreateUpload(us)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, expected)
|
||||
require.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
|
||||
u, _, err := th.Client.GetUpload(expected.Id)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, u)
|
||||
require.Equal(t, expected, u)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetUploadsForUser(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("no permissions", func(t *testing.T) {
|
||||
uss, _, err := th.Client.GetUploadsForUser(th.BasicUser2.Id)
|
||||
require.Error(t, err)
|
||||
CheckErrorID(t, err, "api.user.get_uploads_for_user.forbidden.app_error")
|
||||
require.Nil(t, uss)
|
||||
})
|
||||
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
uss, _, err := th.Client.GetUploadsForUser(th.BasicUser.Id)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, uss)
|
||||
})
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
uploads := make([]*model.UploadSession, 4)
|
||||
for i := 0; i < len(uploads); i++ {
|
||||
us := &model.UploadSession{
|
||||
Id: model.NewId(),
|
||||
Type: model.UploadTypeAttachment,
|
||||
CreateAt: model.GetMillis(),
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
Filename: "upload",
|
||||
FileSize: 8 * 1024 * 1024,
|
||||
}
|
||||
us, err := th.App.CreateUploadSession(th.Context, us)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, us)
|
||||
require.NotEmpty(t, us)
|
||||
us.Path = ""
|
||||
uploads[i] = us
|
||||
}
|
||||
|
||||
uss, _, err := th.Client.GetUploadsForUser(th.BasicUser.Id)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, uss)
|
||||
require.Len(t, uss, len(uploads))
|
||||
for i := range uploads {
|
||||
require.Contains(t, uss, uploads[i])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUploadData(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
if *th.App.Config().FileSettings.DriverName == "" {
|
||||
t.Skip("skipping because no file driver is enabled")
|
||||
}
|
||||
|
||||
us := &model.UploadSession{
|
||||
Id: model.NewId(),
|
||||
Type: model.UploadTypeAttachment,
|
||||
CreateAt: model.GetMillis(),
|
||||
UserId: th.BasicUser2.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
Filename: "upload.zip",
|
||||
FileSize: 8 * 1024 * 1024,
|
||||
}
|
||||
us, err := th.App.CreateUploadSession(th.Context, us)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, us)
|
||||
require.NotEmpty(t, us)
|
||||
|
||||
data := randomBytes(t, int(us.FileSize))
|
||||
|
||||
t.Run("file attachments disabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnableFileAttachments = false })
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnableFileAttachments = true })
|
||||
info, _, err := th.Client.UploadData(model.NewId(), bytes.NewReader(data))
|
||||
require.Nil(t, info)
|
||||
CheckErrorID(t, err, "api.file.attachments.disabled.app_error")
|
||||
})
|
||||
|
||||
t.Run("upload not found", func(t *testing.T) {
|
||||
info, resp, err := th.Client.UploadData(model.NewId(), bytes.NewReader(data))
|
||||
require.Nil(t, info)
|
||||
CheckErrorID(t, err, "app.upload.get.app_error")
|
||||
require.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("no permissions", func(t *testing.T) {
|
||||
info, _, err := th.Client.UploadData(us.Id, bytes.NewReader(data))
|
||||
require.Nil(t, info)
|
||||
CheckErrorID(t, err, "api.context.permissions.app_error")
|
||||
})
|
||||
|
||||
t.Run("not allowed in cloud", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
defer th.App.Srv().RemoveLicense()
|
||||
|
||||
us2 := &model.UploadSession{
|
||||
Id: model.NewId(),
|
||||
Type: model.UploadTypeImport,
|
||||
CreateAt: model.GetMillis(),
|
||||
UserId: th.BasicUser2.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
Filename: "upload",
|
||||
FileSize: 8 * 1024 * 1024,
|
||||
}
|
||||
_, appErr := th.App.CreateUploadSession(th.Context, us2)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
info, resp, err := th.SystemAdminClient.UploadData(us2.Id, bytes.NewReader(data))
|
||||
require.Nil(t, info)
|
||||
CheckErrorID(t, err, "api.file.cloud_upload.app_error")
|
||||
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("bad content-length", func(t *testing.T) {
|
||||
u, resp, err := th.Client.CreateUpload(us)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, u)
|
||||
require.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
|
||||
info, _, err := th.Client.UploadData(u.Id, bytes.NewReader(append(data, 0x00)))
|
||||
require.Nil(t, info)
|
||||
CheckErrorID(t, err, "api.upload.upload_data.invalid_content_length")
|
||||
})
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
u, resp, err := th.Client.CreateUpload(us)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, u)
|
||||
require.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
|
||||
info, _, err := th.Client.UploadData(u.Id, bytes.NewReader(data))
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, info)
|
||||
require.Equal(t, u.Filename, info.Name)
|
||||
require.Equal(t, u.FileSize, info.Size)
|
||||
require.Equal(t, "zip", info.Extension)
|
||||
require.Equal(t, "application/zip", info.MimeType)
|
||||
|
||||
file, _, err := th.Client.GetFile(info.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, file, data)
|
||||
})
|
||||
|
||||
t.Run("resume success", func(t *testing.T) {
|
||||
u, resp, err := th.Client.CreateUpload(us)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, u)
|
||||
require.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
|
||||
rd := &io.LimitedReader{
|
||||
R: bytes.NewReader(data),
|
||||
N: 5 * 1024 * 1024,
|
||||
}
|
||||
info, resp, err := th.Client.UploadData(u.Id, rd)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, info)
|
||||
require.Equal(t, http.StatusNoContent, resp.StatusCode)
|
||||
|
||||
info, _, err = th.Client.UploadData(u.Id, bytes.NewReader(data[5*1024*1024:]))
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, info)
|
||||
require.Equal(t, u.Filename, info.Name)
|
||||
|
||||
file, _, err := th.Client.GetFile(info.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, file, data)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUploadDataMultipart(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
if *th.App.Config().FileSettings.DriverName == "" {
|
||||
t.Skip("skipping because no file driver is enabled")
|
||||
}
|
||||
|
||||
us := &model.UploadSession{
|
||||
Id: model.NewId(),
|
||||
Type: model.UploadTypeAttachment,
|
||||
CreateAt: model.GetMillis(),
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
Filename: "upload",
|
||||
FileSize: 8 * 1024 * 1024,
|
||||
}
|
||||
us, _, err := th.Client.CreateUpload(us)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, us)
|
||||
require.NotEmpty(t, us)
|
||||
|
||||
data := randomBytes(t, int(us.FileSize))
|
||||
|
||||
genMultipartData := func(t *testing.T, data []byte) (io.Reader, string) {
|
||||
mpData := &bytes.Buffer{}
|
||||
mpWriter := multipart.NewWriter(mpData)
|
||||
part, err := mpWriter.CreateFormFile("data", us.Filename)
|
||||
require.NoError(t, err)
|
||||
n, err := part.Write(data)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(data), n)
|
||||
err = mpWriter.Close()
|
||||
require.NoError(t, err)
|
||||
return mpData, mpWriter.FormDataContentType()
|
||||
}
|
||||
|
||||
t.Run("bad content-type", func(t *testing.T) {
|
||||
info, _, err := th.Client.DoUploadFile("/uploads/"+us.Id, data, "multipart/form-data;")
|
||||
require.Nil(t, info)
|
||||
CheckErrorID(t, err, "api.upload.upload_data.invalid_content_type")
|
||||
})
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
mpData, contentType := genMultipartData(t, data)
|
||||
|
||||
req, err := http.NewRequest("POST", th.Client.APIURL+"/uploads/"+us.Id, mpData)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
req.Header.Set(model.HeaderAuth, th.Client.AuthType+" "+th.Client.AuthToken)
|
||||
res, err := th.Client.HTTPClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
var info model.FileInfo
|
||||
err = json.NewDecoder(res.Body).Decode(&info)
|
||||
res.Body.Close()
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, info)
|
||||
require.Equal(t, us.Filename, info.Name)
|
||||
|
||||
file, _, err := th.Client.GetFile(info.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, file, data)
|
||||
})
|
||||
|
||||
t.Run("resume success", func(t *testing.T) {
|
||||
mpData, contentType := genMultipartData(t, data[:5*1024*1024])
|
||||
|
||||
u, _, err := th.Client.CreateUpload(us)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, u)
|
||||
require.NotEmpty(t, u)
|
||||
|
||||
req, err := http.NewRequest("POST", th.Client.APIURL+"/uploads/"+u.Id, mpData)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
req.Header.Set(model.HeaderAuth, th.Client.AuthType+" "+th.Client.AuthToken)
|
||||
res, err := th.Client.HTTPClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusNoContent, res.StatusCode)
|
||||
require.Equal(t, int64(0), res.ContentLength)
|
||||
|
||||
mpData, contentType = genMultipartData(t, data[5*1024*1024:])
|
||||
|
||||
req, err = http.NewRequest("POST", th.Client.APIURL+"/uploads/"+u.Id, mpData)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
req.Header.Set(model.HeaderAuth, th.Client.AuthType+" "+th.Client.AuthToken)
|
||||
res, err = th.Client.HTTPClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
var info model.FileInfo
|
||||
err = json.NewDecoder(res.Body).Decode(&info)
|
||||
res.Body.Close()
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, info)
|
||||
require.Equal(t, u.Filename, info.Name)
|
||||
|
||||
file, _, err := th.Client.GetFile(info.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, file, data)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/utils"
|
||||
)
|
||||
|
||||
func (api *API) InitUsage() {
|
||||
// GET /api/v4/usage/posts
|
||||
api.BaseRoutes.Usage.Handle("/posts", api.APISessionRequired(getPostsUsage)).Methods("GET")
|
||||
// GET /api/v4/usage/storage
|
||||
api.BaseRoutes.Usage.Handle("/storage", api.APISessionRequired(getStorageUsage)).Methods("GET")
|
||||
// GET /api/v4/usage/teams
|
||||
api.BaseRoutes.Usage.Handle("/teams", api.APISessionRequired(getTeamsUsage)).Methods("GET")
|
||||
}
|
||||
|
||||
func getPostsUsage(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
count, appErr := c.App.GetPostsUsage()
|
||||
if appErr != nil {
|
||||
c.Err = model.NewAppError("Api4.getPostsUsage", "app.post.analytics_posts_count.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
||||
return
|
||||
}
|
||||
|
||||
json, err := json.Marshal(&model.PostsUsage{Count: count})
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getPostsUsage", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(json)
|
||||
}
|
||||
|
||||
func getStorageUsage(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
usage, appErr := c.App.GetStorageUsage()
|
||||
if appErr != nil {
|
||||
c.Err = model.NewAppError("Api4.getStorageUsage", "app.usage.get_storage_usage.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
||||
return
|
||||
}
|
||||
|
||||
usage = utils.RoundOffToZeroesResolution(float64(usage), 8)
|
||||
json, err := json.Marshal(&model.StorageUsage{Bytes: usage})
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getStorageUsage", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(json)
|
||||
}
|
||||
|
||||
func getTeamsUsage(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
teamsUsage, appErr := c.App.GetTeamsUsage()
|
||||
if appErr != nil {
|
||||
c.Err = model.NewAppError("Api4.getTeamsUsage", "app.teams.analytics_teams_count.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
||||
return
|
||||
}
|
||||
|
||||
if teamsUsage == nil {
|
||||
c.Err = model.NewAppError("Api4.getTeamsUsage", "app.teams.analytics_teams_count.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
||||
}
|
||||
|
||||
json, err := json.Marshal(teamsUsage)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getTeamsUsage", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(json)
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetPostsUsage(t *testing.T) {
|
||||
t.Run("unauthenticated users can not access", func(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Logout()
|
||||
|
||||
usage, r, err := th.Client.GetPostsUsage()
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, usage)
|
||||
assert.Equal(t, http.StatusUnauthorized, r.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("good request returns response", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
for i := 0; i < 14; i++ {
|
||||
th.CreatePost()
|
||||
}
|
||||
|
||||
total, err := th.Server.Store().Post().AnalyticsPostCount(&model.PostCountOptions{ExcludeDeleted: true})
|
||||
require.NoError(t, err)
|
||||
usersOnly, err := th.Server.Store().Post().AnalyticsPostCount(&model.PostCountOptions{ExcludeDeleted: true, UsersPostsOnly: true})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.GreaterOrEqual(t, usersOnly, int64(14))
|
||||
require.LessOrEqual(t, usersOnly, int64(20))
|
||||
require.GreaterOrEqual(t, total, usersOnly)
|
||||
|
||||
usage, r, err := th.Client.GetPostsUsage()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, r.StatusCode)
|
||||
assert.NotNil(t, usage)
|
||||
assert.Equal(t, int64(10), usage.Count)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetStorageUsage(t *testing.T) {
|
||||
t.Run("unauthenticated users cannot access", func(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Logout()
|
||||
|
||||
usage, r, err := th.Client.GetStorageUsage()
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, usage)
|
||||
assert.Equal(t, http.StatusUnauthorized, r.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetTeamsUsage(t *testing.T) {
|
||||
t.Run("unauthenticated users can not access", func(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Logout()
|
||||
|
||||
usage, r, err := th.Client.GetTeamsUsage()
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, usage)
|
||||
assert.Equal(t, http.StatusUnauthorized, r.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("good request returns response", func(t *testing.T) {
|
||||
// Following calls create a total of 3 teams
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
th.CreateTeam()
|
||||
th.CreateTeam()
|
||||
|
||||
usage, r, err := th.Client.GetTeamsUsage()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, r.StatusCode)
|
||||
assert.NotNil(t, usage)
|
||||
assert.Equal(t, int64(3), usage.Active)
|
||||
})
|
||||
}
|
||||
3386
api4/user.go
3386
api4/user.go
File diff suppressed because it is too large
Load diff
7355
api4/user_test.go
7355
api4/user_test.go
File diff suppressed because it is too large
Load diff
648
api4/webhook.go
648
api4/webhook.go
|
|
@ -1,648 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitWebhook() {
|
||||
api.BaseRoutes.IncomingHooks.Handle("", api.APISessionRequired(createIncomingHook)).Methods("POST")
|
||||
api.BaseRoutes.IncomingHooks.Handle("", api.APISessionRequired(getIncomingHooks)).Methods("GET")
|
||||
api.BaseRoutes.IncomingHook.Handle("", api.APISessionRequired(getIncomingHook)).Methods("GET")
|
||||
api.BaseRoutes.IncomingHook.Handle("", api.APISessionRequired(updateIncomingHook)).Methods("PUT")
|
||||
api.BaseRoutes.IncomingHook.Handle("", api.APISessionRequired(deleteIncomingHook)).Methods("DELETE")
|
||||
|
||||
api.BaseRoutes.OutgoingHooks.Handle("", api.APISessionRequired(createOutgoingHook)).Methods("POST")
|
||||
api.BaseRoutes.OutgoingHooks.Handle("", api.APISessionRequired(getOutgoingHooks)).Methods("GET")
|
||||
api.BaseRoutes.OutgoingHook.Handle("", api.APISessionRequired(getOutgoingHook)).Methods("GET")
|
||||
api.BaseRoutes.OutgoingHook.Handle("", api.APISessionRequired(updateOutgoingHook)).Methods("PUT")
|
||||
api.BaseRoutes.OutgoingHook.Handle("", api.APISessionRequired(deleteOutgoingHook)).Methods("DELETE")
|
||||
api.BaseRoutes.OutgoingHook.Handle("/regen_token", api.APISessionRequired(regenOutgoingHookToken)).Methods("POST")
|
||||
}
|
||||
|
||||
func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var hook model.IncomingWebhook
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&hook); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("incoming_webhook", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
channel, err := c.App.GetChannel(c.AppContext, hook.ChannelId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("createIncomingHook", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameterAuditable(auditRec, "incoming_webhook", &hook)
|
||||
audit.AddEventParameterAuditable(auditRec, "channel", channel)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionManageIncomingWebhooks) {
|
||||
c.SetPermissionError(model.PermissionManageIncomingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
if channel.Type != model.ChannelTypeOpen && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionReadChannel) {
|
||||
c.LogAudit("fail - bad channel permissions")
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
|
||||
userId := c.AppContext.Session().UserId
|
||||
if hook.UserId != "" && hook.UserId != userId {
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionManageOthersIncomingWebhooks) {
|
||||
c.LogAudit("fail - inappropriate permissions")
|
||||
c.SetPermissionError(model.PermissionManageOthersIncomingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = c.App.GetUser(hook.UserId); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
userId = hook.UserId
|
||||
}
|
||||
|
||||
incomingHook, err := c.App.CreateIncomingWebhookForChannel(userId, channel, &hook)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(incomingHook)
|
||||
auditRec.AddEventObjectType("hook")
|
||||
c.LogAudit("success")
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(incomingHook); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func updateIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireHookId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var updatedHook model.IncomingWebhook
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&updatedHook); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("incoming_webhook", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
// The hook being updated in the payload must be the same one as indicated in the URL.
|
||||
if updatedHook.Id != c.Params.HookId {
|
||||
c.SetInvalidParam("hook_id")
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("updateIncomingHook", audit.Fail)
|
||||
audit.AddEventParameter(auditRec, "hook_id", c.Params.HookId)
|
||||
audit.AddEventParameterAuditable(auditRec, "updated_hook", &updatedHook)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
oldHook, err := c.App.GetIncomingWebhook(c.Params.HookId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
auditRec.AddEventPriorState(oldHook)
|
||||
auditRec.AddEventObjectType("incoming_webhook")
|
||||
|
||||
if updatedHook.TeamId == "" {
|
||||
updatedHook.TeamId = oldHook.TeamId
|
||||
}
|
||||
|
||||
if updatedHook.TeamId != oldHook.TeamId {
|
||||
c.Err = model.NewAppError("updateIncomingHook", "api.webhook.team_mismatch.app_error", nil, "user_id="+c.AppContext.Session().UserId, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
channel, err := c.App.GetChannel(c.AppContext, updatedHook.ChannelId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
auditRec.AddMeta("channel_id", channel.Id)
|
||||
auditRec.AddMeta("channel_name", channel.Name)
|
||||
|
||||
if channel.TeamId != updatedHook.TeamId {
|
||||
c.SetInvalidParam("channel_id")
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionManageIncomingWebhooks) {
|
||||
c.SetPermissionError(model.PermissionManageIncomingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
if c.AppContext.Session().UserId != oldHook.UserId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionManageOthersIncomingWebhooks) {
|
||||
c.LogAudit("fail - inappropriate permissions")
|
||||
c.SetPermissionError(model.PermissionManageOthersIncomingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
if channel.Type != model.ChannelTypeOpen && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionReadChannel) {
|
||||
c.LogAudit("fail - bad channel permissions")
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
|
||||
incomingHook, err := c.App.UpdateIncomingWebhook(oldHook, &updatedHook)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventResultState(incomingHook)
|
||||
auditRec.Success()
|
||||
c.LogAudit("success")
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(incomingHook); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
teamID = r.URL.Query().Get("team_id")
|
||||
userID = c.AppContext.Session().UserId
|
||||
|
||||
hooks []*model.IncomingWebhook
|
||||
appErr *model.AppError
|
||||
)
|
||||
|
||||
if teamID != "" {
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionManageIncomingWebhooks) {
|
||||
c.SetPermissionError(model.PermissionManageIncomingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove userId as a filter if they have permission to manage others.
|
||||
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionManageOthersIncomingWebhooks) {
|
||||
userID = ""
|
||||
}
|
||||
|
||||
hooks, appErr = c.App.GetIncomingWebhooksForTeamPageByUser(teamID, userID, c.Params.Page, c.Params.PerPage)
|
||||
} else {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageIncomingWebhooks) {
|
||||
c.SetPermissionError(model.PermissionManageIncomingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove userId as a filter if they have permission to manage others.
|
||||
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOthersIncomingWebhooks) {
|
||||
userID = ""
|
||||
}
|
||||
|
||||
hooks, appErr = c.App.GetIncomingWebhooksPageByUser(userID, c.Params.Page, c.Params.PerPage)
|
||||
}
|
||||
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(hooks)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getIncomingHooks", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func getIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireHookId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hookId := c.Params.HookId
|
||||
|
||||
var err *model.AppError
|
||||
var hook *model.IncomingWebhook
|
||||
var channel *model.Channel
|
||||
|
||||
hook, err = c.App.GetIncomingWebhook(hookId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("getIncomingHook", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "hook_id", c.Params.HookId)
|
||||
auditRec.AddMeta("hook_id", hook.Id)
|
||||
auditRec.AddMeta("hook_display", hook.DisplayName)
|
||||
auditRec.AddMeta("channel_id", hook.ChannelId)
|
||||
auditRec.AddMeta("team_id", hook.TeamId)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
channel, err = c.App.GetChannel(c.AppContext, hook.ChannelId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageIncomingWebhooks) ||
|
||||
(channel.Type != model.ChannelTypeOpen && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), hook.ChannelId, model.PermissionReadChannel)) {
|
||||
c.LogAudit("fail - bad permissions")
|
||||
c.SetPermissionError(model.PermissionManageIncomingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
if c.AppContext.Session().UserId != hook.UserId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOthersIncomingWebhooks) {
|
||||
c.LogAudit("fail - inappropriate permissions")
|
||||
c.SetPermissionError(model.PermissionManageOthersIncomingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
c.LogAudit("success")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(hook); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireHookId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hookId := c.Params.HookId
|
||||
|
||||
var err *model.AppError
|
||||
var hook *model.IncomingWebhook
|
||||
var channel *model.Channel
|
||||
|
||||
hook, err = c.App.GetIncomingWebhook(hookId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
channel, err = c.App.GetChannel(c.AppContext, hook.ChannelId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("deleteIncomingHook", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "hook_id", c.Params.HookId)
|
||||
auditRec.AddMeta("hook_id", hook.Id)
|
||||
auditRec.AddMeta("hook_display", hook.DisplayName)
|
||||
auditRec.AddMeta("channel_id", channel.Id)
|
||||
auditRec.AddMeta("channel_name", channel.Name)
|
||||
auditRec.AddMeta("team_id", hook.TeamId)
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageIncomingWebhooks) ||
|
||||
(channel.Type != model.ChannelTypeOpen && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), hook.ChannelId, model.PermissionReadChannel)) {
|
||||
c.LogAudit("fail - bad permissions")
|
||||
c.SetPermissionError(model.PermissionManageIncomingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
if c.AppContext.Session().UserId != hook.UserId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOthersIncomingWebhooks) {
|
||||
c.LogAudit("fail - inappropriate permissions")
|
||||
c.SetPermissionError(model.PermissionManageOthersIncomingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
if err = c.App.DeleteIncomingWebhook(hookId); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventPriorState(hook)
|
||||
auditRec.AddEventObjectType("incoming_webhook")
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func updateOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireHookId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var updatedHook model.OutgoingWebhook
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&updatedHook); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("outgoing_webhook", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
// The hook being updated in the payload must be the same one as indicated in the URL.
|
||||
if updatedHook.Id != c.Params.HookId {
|
||||
c.SetInvalidParam("hook_id")
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("updateOutgoingHook", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameterAuditable(auditRec, "updated_hook", &updatedHook)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
oldHook, err := c.App.GetOutgoingWebhook(c.Params.HookId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if updatedHook.TeamId == "" {
|
||||
updatedHook.TeamId = oldHook.TeamId
|
||||
}
|
||||
|
||||
if updatedHook.TeamId != oldHook.TeamId {
|
||||
c.Err = model.NewAppError("updateOutgoingHook", "api.webhook.team_mismatch.app_error", nil, "user_id="+c.AppContext.Session().UserId, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), updatedHook.TeamId, model.PermissionManageOutgoingWebhooks) {
|
||||
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
if c.AppContext.Session().UserId != oldHook.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), updatedHook.TeamId, model.PermissionManageOthersOutgoingWebhooks) {
|
||||
c.LogAudit("fail - inappropriate permissions")
|
||||
c.SetPermissionError(model.PermissionManageOthersOutgoingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
updatedHook.CreatorId = c.AppContext.Session().UserId
|
||||
|
||||
rhook, err := c.App.UpdateOutgoingWebhook(c.AppContext, oldHook, &updatedHook)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
c.LogAudit("success")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(rhook); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var hook model.OutgoingWebhook
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&hook); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("outgoing_webhook", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("createOutgoingHook", audit.Fail)
|
||||
audit.AddEventParameterAuditable(auditRec, "hook", &hook)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOutgoingWebhooks) {
|
||||
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
if hook.CreatorId == "" {
|
||||
hook.CreatorId = c.AppContext.Session().UserId
|
||||
} else {
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOthersOutgoingWebhooks) {
|
||||
c.LogAudit("fail - inappropriate permissions")
|
||||
c.SetPermissionError(model.PermissionManageOthersOutgoingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := c.App.GetUser(hook.CreatorId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rhook, err := c.App.CreateOutgoingWebhook(&hook)
|
||||
if err != nil {
|
||||
c.LogAudit("fail")
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(rhook)
|
||||
auditRec.AddEventObjectType("outgoing_webhook")
|
||||
c.LogAudit("success")
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(rhook); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getOutgoingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
query = r.URL.Query()
|
||||
channelID = query.Get("channel_id")
|
||||
teamID = query.Get("team_id")
|
||||
userID = c.AppContext.Session().UserId
|
||||
|
||||
hooks []*model.OutgoingWebhook
|
||||
appErr *model.AppError
|
||||
)
|
||||
|
||||
if channelID != "" {
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelID, model.PermissionManageOutgoingWebhooks) {
|
||||
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove userId as a filter if they have permission to manage others.
|
||||
if c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelID, model.PermissionManageOthersOutgoingWebhooks) {
|
||||
userID = ""
|
||||
}
|
||||
|
||||
hooks, appErr = c.App.GetOutgoingWebhooksForChannelPageByUser(channelID, userID, c.Params.Page, c.Params.PerPage)
|
||||
} else if teamID != "" {
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionManageOutgoingWebhooks) {
|
||||
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove userId as a filter if they have permission to manage others.
|
||||
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionManageOthersOutgoingWebhooks) {
|
||||
userID = ""
|
||||
}
|
||||
|
||||
hooks, appErr = c.App.GetOutgoingWebhooksForTeamPageByUser(teamID, userID, c.Params.Page, c.Params.PerPage)
|
||||
} else {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOutgoingWebhooks) {
|
||||
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove userId as a filter if they have permission to manage others.
|
||||
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOthersOutgoingWebhooks) {
|
||||
userID = ""
|
||||
}
|
||||
|
||||
hooks, appErr = c.App.GetOutgoingWebhooksPageByUser(userID, c.Params.Page, c.Params.PerPage)
|
||||
}
|
||||
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(hooks)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getOutgoingHooks", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func getOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireHookId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hook, err := c.App.GetOutgoingWebhook(c.Params.HookId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("getOutgoingHook", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "hook_id", c.Params.HookId)
|
||||
auditRec.AddMeta("hook_id", hook.Id)
|
||||
auditRec.AddMeta("hook_display", hook.DisplayName)
|
||||
auditRec.AddMeta("channel_id", hook.ChannelId)
|
||||
auditRec.AddMeta("team_id", hook.TeamId)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOutgoingWebhooks) {
|
||||
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
if c.AppContext.Session().UserId != hook.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOthersOutgoingWebhooks) {
|
||||
c.LogAudit("fail - inappropriate permissions")
|
||||
c.SetPermissionError(model.PermissionManageOthersOutgoingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
c.LogAudit("success")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(hook); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func regenOutgoingHookToken(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireHookId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hook, err := c.App.GetOutgoingWebhook(c.Params.HookId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("regenOutgoingHookToken", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
auditRec.AddMeta("hook_id", hook.Id)
|
||||
auditRec.AddMeta("hook_display", hook.DisplayName)
|
||||
auditRec.AddMeta("channel_id", hook.ChannelId)
|
||||
auditRec.AddMeta("team_id", hook.TeamId)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOutgoingWebhooks) {
|
||||
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
if c.AppContext.Session().UserId != hook.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOthersOutgoingWebhooks) {
|
||||
c.LogAudit("fail - inappropriate permissions")
|
||||
c.SetPermissionError(model.PermissionManageOthersOutgoingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
rhook, err := c.App.RegenOutgoingWebhookToken(hook)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventResultState(rhook)
|
||||
auditRec.AddEventObjectType("outgoing_webhook")
|
||||
auditRec.Success()
|
||||
c.LogAudit("success")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(rhook); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func deleteOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireHookId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hook, err := c.App.GetOutgoingWebhook(c.Params.HookId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("deleteOutgoingHook", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "hook_id", c.Params.HookId)
|
||||
auditRec.AddMeta("hook_id", hook.Id)
|
||||
auditRec.AddMeta("hook_display", hook.DisplayName)
|
||||
auditRec.AddMeta("channel_id", hook.ChannelId)
|
||||
auditRec.AddMeta("team_id", hook.TeamId)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOutgoingWebhooks) {
|
||||
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
if c.AppContext.Session().UserId != hook.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOthersOutgoingWebhooks) {
|
||||
c.LogAudit("fail - inappropriate permissions")
|
||||
c.SetPermissionError(model.PermissionManageOthersOutgoingWebhooks)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.DeleteOutgoingWebhook(hook.Id); err != nil {
|
||||
c.LogAudit("fail")
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
c.LogAudit("success")
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/worktemplates"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
func (api *API) InitWorkTemplate() {
|
||||
api.BaseRoutes.WorkTemplates.Handle("/categories", api.APISessionRequired(getWorkTemplateCategories)).Methods("GET")
|
||||
api.BaseRoutes.WorkTemplates.Handle("/categories/{category}/templates", api.APISessionRequired(getWorkTemplates)).Methods("GET")
|
||||
api.BaseRoutes.WorkTemplates.Handle("/execute", api.APIHandler(executeWorkTemplate)).Methods("POST")
|
||||
}
|
||||
|
||||
func areWorkTemplatesEnabled(c *Context) *model.AppError {
|
||||
if !c.App.Config().FeatureFlags.WorkTemplate {
|
||||
return model.NewAppError("areWorkTemplatesEnabled", "api.work_templates.disabled", nil, "feature flag is off", http.StatusNotFound)
|
||||
}
|
||||
|
||||
// we have to make sure that playbooks plugin is enabled and board is a product
|
||||
pbActive, err := c.App.IsPluginActive(model.PluginIdPlaybooks)
|
||||
if err != nil {
|
||||
return model.NewAppError("areWorkTemplatesEnabled", "api.work_templates.disabled", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
if !pbActive {
|
||||
return model.NewAppError("areWorkTemplatesEnabled", "api.work_templates.disabled", nil, "playbook plugin not active", http.StatusNotFound)
|
||||
}
|
||||
|
||||
hasBoard, err := c.App.HasBoardProduct()
|
||||
if err != nil {
|
||||
return model.NewAppError("areWorkTemplatesEnabled", "api.work_templates.disabled", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
if !hasBoard {
|
||||
return model.NewAppError("areWorkTemplatesEnabled", "api.work_templates.disabled", nil, "board product not found", http.StatusNotFound)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getWorkTemplateCategories(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
appErr := areWorkTemplatesEnabled(c)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
t := c.AppContext.GetT()
|
||||
|
||||
categories, appErr := c.App.GetWorkTemplateCategories(t)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(categories)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getWorkTemplateCategories", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func getWorkTemplates(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
appErr := areWorkTemplatesEnabled(c)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireCategory()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
t := c.AppContext.GetT()
|
||||
|
||||
workTemplates, appErr := c.App.GetWorkTemplates(c.Params.Category, c.App.Config().FeatureFlags.ToMap(), t)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(workTemplates)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getWorkTemplates", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func executeWorkTemplate(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
appErr := areWorkTemplatesEnabled(c)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
wtcr := &worktemplates.ExecutionRequest{}
|
||||
err := json.NewDecoder(r.Body).Decode(wtcr)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("executeWorkTemplate", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
canCreatePublicChannel := c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), wtcr.TeamID, model.PermissionCreatePublicChannel)
|
||||
canCreatePrivateChannel := c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), wtcr.TeamID, model.PermissionCreatePrivateChannel)
|
||||
// focalboard uses channel permissions for board creation
|
||||
canCreatePublicBoard := canCreatePublicChannel
|
||||
canCreatePrivateBoard := canCreatePrivateChannel
|
||||
canCreatePublicPlaybook := c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), wtcr.TeamID, model.PermissionPublicPlaybookCreate)
|
||||
canCreatePrivatePlaybook := c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), wtcr.TeamID, model.PermissionPrivatePlaybookCreate)
|
||||
appErr = wtcr.CanBeExecuted(worktemplates.PermissionSet{
|
||||
License: c.App.License(),
|
||||
CanCreatePublicChannel: canCreatePublicChannel,
|
||||
CanCreatePrivateChannel: canCreatePrivateChannel,
|
||||
CanCreatePublicBoard: canCreatePublicBoard,
|
||||
CanCreatePrivateBoard: canCreatePrivateBoard,
|
||||
CanCreatePublicPlaybook: canCreatePublicPlaybook,
|
||||
CanCreatePrivatePlaybook: canCreatePrivatePlaybook,
|
||||
})
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
canInstallPlugin := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins)
|
||||
if !*c.App.Config().PluginSettings.Enable || !*c.App.Config().PluginSettings.EnableMarketplace || *c.App.Config().PluginSettings.MarketplaceURL != model.PluginSettingsDefaultMarketplaceURL {
|
||||
canInstallPlugin = false
|
||||
}
|
||||
|
||||
res, appErr := c.App.ExecuteWorkTemplate(c.AppContext, wtcr, canInstallPlugin)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(res)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("executeWorkTemplate", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
243
app/admin.go
243
app/admin.go
|
|
@ -1,243 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/services/cache"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/i18n"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mail"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
var latestVersionCache = cache.NewLRU(cache.LRUOptions{
|
||||
Size: 1,
|
||||
})
|
||||
|
||||
func (s *Server) GetLogs(page, perPage int) ([]string, *model.AppError) {
|
||||
var lines []string
|
||||
|
||||
license := s.License()
|
||||
if license != nil && *license.Features.Cluster && s.platform.Cluster() != nil && *s.platform.Config().ClusterSettings.Enable {
|
||||
if info := s.platform.Cluster().GetMyClusterInfo(); info != nil {
|
||||
lines = append(lines, "-----------------------------------------------------------------------------------------------------------")
|
||||
lines = append(lines, "-----------------------------------------------------------------------------------------------------------")
|
||||
lines = append(lines, info.Hostname)
|
||||
lines = append(lines, "-----------------------------------------------------------------------------------------------------------")
|
||||
lines = append(lines, "-----------------------------------------------------------------------------------------------------------")
|
||||
} else {
|
||||
mlog.Error("Could not get cluster info")
|
||||
}
|
||||
}
|
||||
|
||||
melines, err := s.GetLogsSkipSend(page, perPage, &model.LogFilter{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lines = append(lines, melines...)
|
||||
|
||||
if s.platform.Cluster() != nil && *s.platform.Config().ClusterSettings.Enable {
|
||||
clines, err := s.platform.Cluster().GetLogs(page, perPage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lines = append(lines, clines...)
|
||||
}
|
||||
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
func (s *Server) QueryLogs(page, perPage int, logFilter *model.LogFilter) (map[string][]string, *model.AppError) {
|
||||
logData := make(map[string][]string)
|
||||
|
||||
serverName := "default"
|
||||
|
||||
license := s.License()
|
||||
if license != nil && *license.Features.Cluster && s.platform.Cluster() != nil && *s.platform.Config().ClusterSettings.Enable {
|
||||
if info := s.platform.Cluster().GetMyClusterInfo(); info != nil {
|
||||
serverName = info.Hostname
|
||||
} else {
|
||||
mlog.Error("Could not get cluster info")
|
||||
}
|
||||
}
|
||||
|
||||
serverNames := logFilter.ServerNames
|
||||
if len(serverNames) > 0 {
|
||||
for _, nodeName := range serverNames {
|
||||
if nodeName == "default" {
|
||||
AddLocalLogs(logData, s, page, perPage, nodeName, logFilter)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AddLocalLogs(logData, s, page, perPage, serverName, logFilter)
|
||||
}
|
||||
|
||||
if s.platform.Cluster() != nil && *s.Config().ClusterSettings.Enable {
|
||||
clusterLogs, err := s.platform.Cluster().QueryLogs(page, perPage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if clusterLogs != nil && len(serverNames) > 0 {
|
||||
for _, filteredNodeName := range serverNames {
|
||||
logData[filteredNodeName] = clusterLogs[filteredNodeName]
|
||||
}
|
||||
} else {
|
||||
for nodeName, logs := range clusterLogs {
|
||||
logData[nodeName] = logs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return logData, nil
|
||||
}
|
||||
|
||||
func AddLocalLogs(logData map[string][]string, s *Server, page, perPage int, serverName string, logFilter *model.LogFilter) *model.AppError {
|
||||
currentServerLogs, err := s.GetLogsSkipSend(page, perPage, logFilter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logData[serverName] = currentServerLogs
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) QueryLogs(page, perPage int, logFilter *model.LogFilter) (map[string][]string, *model.AppError) {
|
||||
return a.Srv().QueryLogs(page, perPage, logFilter)
|
||||
}
|
||||
|
||||
func (a *App) GetLogs(page, perPage int) ([]string, *model.AppError) {
|
||||
return a.Srv().GetLogs(page, perPage)
|
||||
}
|
||||
|
||||
func (s *Server) GetLogsSkipSend(page, perPage int, logFilter *model.LogFilter) ([]string, *model.AppError) {
|
||||
return s.platform.GetLogsSkipSend(page, perPage, logFilter)
|
||||
}
|
||||
|
||||
func (a *App) GetLogsSkipSend(page, perPage int, logFilter *model.LogFilter) ([]string, *model.AppError) {
|
||||
return a.Srv().GetLogsSkipSend(page, perPage, logFilter)
|
||||
}
|
||||
|
||||
func (a *App) GetClusterStatus() []*model.ClusterInfo {
|
||||
infos := make([]*model.ClusterInfo, 0)
|
||||
|
||||
if a.Cluster() != nil {
|
||||
infos = a.Cluster().GetClusterInfos()
|
||||
}
|
||||
|
||||
return infos
|
||||
}
|
||||
|
||||
func (s *Server) InvalidateAllCaches() *model.AppError {
|
||||
return s.platform.InvalidateAllCaches()
|
||||
}
|
||||
|
||||
func (s *Server) InvalidateAllCachesSkipSend() {
|
||||
s.platform.InvalidateAllCachesSkipSend()
|
||||
|
||||
}
|
||||
|
||||
func (a *App) RecycleDatabaseConnection() {
|
||||
mlog.Info("Attempting to recycle database connections.")
|
||||
|
||||
// This works by setting 10 seconds as the max conn lifetime for all DB connections.
|
||||
// This allows in gradually closing connections as they expire. In future, we can think
|
||||
// of exposing this as a param from the REST api.
|
||||
a.Srv().Store().RecycleDBConnections(10 * time.Second)
|
||||
|
||||
mlog.Info("Finished recycling database connections.")
|
||||
}
|
||||
|
||||
func (a *App) TestSiteURL(siteURL string) *model.AppError {
|
||||
url := fmt.Sprintf("%s/api/v4/system/ping", siteURL)
|
||||
res, err := http.Get(url)
|
||||
if err != nil || res.StatusCode != 200 {
|
||||
return model.NewAppError("testSiteURL", "app.admin.test_site_url.failure", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, res.Body)
|
||||
_ = res.Body.Close()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) TestEmail(userID string, cfg *model.Config) *model.AppError {
|
||||
if *cfg.EmailSettings.SMTPServer == "" {
|
||||
return model.NewAppError("testEmail", "api.admin.test_email.missing_server", nil, i18n.T("api.context.invalid_param.app_error", map[string]any{"Name": "SMTPServer"}), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// if the user hasn't changed their email settings, fill in the actual SMTP password so that
|
||||
// the user can verify an existing SMTP connection
|
||||
if *cfg.EmailSettings.SMTPPassword == model.FakeSetting {
|
||||
if *cfg.EmailSettings.SMTPServer == *a.Config().EmailSettings.SMTPServer &&
|
||||
*cfg.EmailSettings.SMTPPort == *a.Config().EmailSettings.SMTPPort &&
|
||||
*cfg.EmailSettings.SMTPUsername == *a.Config().EmailSettings.SMTPUsername {
|
||||
*cfg.EmailSettings.SMTPPassword = *a.Config().EmailSettings.SMTPPassword
|
||||
} else {
|
||||
return model.NewAppError("testEmail", "api.admin.test_email.reenter_password", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
user, err := a.GetUser(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
T := i18n.GetUserTranslations(user.Locale)
|
||||
license := a.Srv().License()
|
||||
mailConfig := a.Srv().MailServiceConfig()
|
||||
if err := mail.SendMailUsingConfig(user.Email, T("api.admin.test_email.subject"), T("api.admin.test_email.body"), mailConfig, license != nil && *license.Features.Compliance, "", "", "", "", ""); err != nil {
|
||||
return model.NewAppError("testEmail", "app.admin.test_email.failure", map[string]any{"Error": err.Error()}, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) GetLatestVersion(latestVersionUrl string) (*model.GithubReleaseInfo, *model.AppError) {
|
||||
var cachedLatestVersion *model.GithubReleaseInfo
|
||||
if cacheErr := latestVersionCache.Get("latest_version_cache", &cachedLatestVersion); cacheErr == nil {
|
||||
return cachedLatestVersion, nil
|
||||
}
|
||||
|
||||
res, err := http.Get(latestVersionUrl)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetLatestVersion", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
responseData, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetLatestVersion", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
var releaseInfoResponse *model.GithubReleaseInfo
|
||||
err = json.Unmarshal(responseData, &releaseInfoResponse)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetLatestVersion", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
if validErr := releaseInfoResponse.IsValid(); validErr != nil {
|
||||
return nil, model.NewAppError("GetLatestVersion", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(validErr)
|
||||
}
|
||||
|
||||
err = latestVersionCache.Set("latest_version_cache", releaseInfoResponse)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetLatestVersion", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
return releaseInfoResponse, nil
|
||||
}
|
||||
|
||||
func (a *App) ClearLatestVersionCache() {
|
||||
latestVersionCache.Remove("latest_version_cache")
|
||||
}
|
||||
176
app/app.go
176
app/app.go
|
|
@ -1,176 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/product"
|
||||
"github.com/mattermost/mattermost-server/v6/services/httpservice"
|
||||
"github.com/mattermost/mattermost-server/v6/services/imageproxy"
|
||||
"github.com/mattermost/mattermost-server/v6/services/searchengine"
|
||||
"github.com/mattermost/mattermost-server/v6/services/timezones"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/templates"
|
||||
"github.com/mattermost/mattermost-server/v6/utils"
|
||||
)
|
||||
|
||||
// App is a pure functional component that does not have any fields, except Server.
|
||||
// It is a request-scoped struct constructed every time a request hits the server,
|
||||
// and its only purpose is to provide business logic to Server via its methods.
|
||||
type App struct {
|
||||
ch *Channels
|
||||
}
|
||||
|
||||
func New(options ...AppOption) *App {
|
||||
app := &App{}
|
||||
|
||||
for _, option := range options {
|
||||
option(app)
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func (a *App) TelemetryId() string {
|
||||
return a.Srv().TelemetryId()
|
||||
}
|
||||
|
||||
func (s *Server) TemplatesContainer() *templates.Container {
|
||||
return s.htmlTemplateWatcher
|
||||
}
|
||||
|
||||
func (a *App) Handle404(w http.ResponseWriter, r *http.Request) {
|
||||
ipAddress := utils.GetIPAddress(r, a.Config().ServiceSettings.TrustedProxyIPHeader)
|
||||
mlog.Debug("not found handler triggered", mlog.String("path", r.URL.Path), mlog.Int("code", 404), mlog.String("ip", ipAddress))
|
||||
|
||||
if *a.Config().ServiceSettings.WebserverMode == "disabled" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
utils.RenderWebAppError(a.Config(), w, r, model.NewAppError("Handle404", "api.context.404.app_error", nil, "", http.StatusNotFound), a.AsymmetricSigningKey())
|
||||
}
|
||||
|
||||
func (s *Server) getFirstServerRunTimestamp() (int64, *model.AppError) {
|
||||
systemData, err := s.Store().System().GetByName(model.SystemFirstServerRunTimestampKey)
|
||||
if err != nil {
|
||||
return 0, model.NewAppError("getFirstServerRunTimestamp", "app.system.get_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
value, err := strconv.ParseInt(systemData.Value, 10, 64)
|
||||
if err != nil {
|
||||
return 0, model.NewAppError("getFirstServerRunTimestamp", "app.system_install_date.parse_int.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (a *App) Channels() *Channels {
|
||||
return a.ch
|
||||
}
|
||||
func (a *App) Srv() *Server {
|
||||
return a.ch.srv
|
||||
}
|
||||
func (a *App) Log() *mlog.Logger {
|
||||
return a.ch.srv.Log()
|
||||
}
|
||||
func (a *App) NotificationsLog() *mlog.Logger {
|
||||
return a.ch.srv.NotificationsLog()
|
||||
}
|
||||
|
||||
func (a *App) AccountMigration() einterfaces.AccountMigrationInterface {
|
||||
return a.ch.AccountMigration
|
||||
}
|
||||
func (a *App) Cluster() einterfaces.ClusterInterface {
|
||||
return a.ch.srv.platform.Cluster()
|
||||
}
|
||||
func (a *App) Compliance() einterfaces.ComplianceInterface {
|
||||
return a.ch.Compliance
|
||||
}
|
||||
func (a *App) DataRetention() einterfaces.DataRetentionInterface {
|
||||
return a.ch.DataRetention
|
||||
}
|
||||
func (a *App) SearchEngine() *searchengine.Broker {
|
||||
return a.ch.srv.platform.SearchEngine
|
||||
}
|
||||
func (a *App) Ldap() einterfaces.LdapInterface {
|
||||
return a.ch.Ldap
|
||||
}
|
||||
func (a *App) MessageExport() einterfaces.MessageExportInterface {
|
||||
return a.ch.MessageExport
|
||||
}
|
||||
func (a *App) Metrics() einterfaces.MetricsInterface {
|
||||
return a.ch.srv.GetMetrics()
|
||||
}
|
||||
func (a *App) Notification() einterfaces.NotificationInterface {
|
||||
return a.ch.Notification
|
||||
}
|
||||
func (a *App) Saml() einterfaces.SamlInterface {
|
||||
return a.ch.Saml
|
||||
}
|
||||
func (a *App) Cloud() einterfaces.CloudInterface {
|
||||
return a.ch.srv.Cloud
|
||||
}
|
||||
func (a *App) HTTPService() httpservice.HTTPService {
|
||||
return a.ch.srv.httpService
|
||||
}
|
||||
func (a *App) ImageProxy() *imageproxy.ImageProxy {
|
||||
return a.ch.imageProxy
|
||||
}
|
||||
func (a *App) Timezones() *timezones.Timezones {
|
||||
return a.ch.srv.timezones
|
||||
}
|
||||
func (a *App) License() *model.License {
|
||||
return a.Srv().License()
|
||||
}
|
||||
|
||||
func (a *App) DBHealthCheckWrite() error {
|
||||
currentTime := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
|
||||
return a.Srv().Store().System().SaveOrUpdate(&model.System{
|
||||
Name: a.dbHealthCheckKey(),
|
||||
Value: currentTime,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) DBHealthCheckDelete() error {
|
||||
_, err := a.Srv().Store().System().PermanentDeleteByName(a.dbHealthCheckKey())
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *App) dbHealthCheckKey() string {
|
||||
return fmt.Sprintf("health_check_%s", a.GetClusterId())
|
||||
}
|
||||
|
||||
func (a *App) CheckIntegrity() <-chan model.IntegrityCheckResult {
|
||||
return a.Srv().Store().CheckIntegrity()
|
||||
}
|
||||
|
||||
func (a *App) SetChannels(ch *Channels) {
|
||||
a.ch = ch
|
||||
}
|
||||
|
||||
func (a *App) SetServer(srv *Server) {
|
||||
a.ch.srv = srv
|
||||
}
|
||||
|
||||
func (a *App) UpdateExpiredDNDStatuses() ([]*model.Status, error) {
|
||||
return a.Srv().Store().Status().UpdateExpiredDNDStatuses()
|
||||
}
|
||||
|
||||
// Ensure system service adapter implements `product.SystemService`
|
||||
var _ product.SystemService = (*systemServiceAdapter)(nil)
|
||||
|
||||
// systemServiceAdapter provides a collection of system APIs for use by products.
|
||||
type systemServiceAdapter struct {
|
||||
server *Server
|
||||
}
|
||||
|
||||
func (ssa *systemServiceAdapter) GetDiagnosticId() string {
|
||||
return ssa.server.TelemetryId()
|
||||
}
|
||||
274
app/app_test.go
274
app/app_test.go
|
|
@ -1,274 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/store/storetest/mocks"
|
||||
)
|
||||
|
||||
/* Temporarily comment out until MM-11108
|
||||
func TestAppRace(t *testing.T) {
|
||||
for i := 0; i < 10; i++ {
|
||||
a, err := New()
|
||||
require.NoError(t, err)
|
||||
a.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" })
|
||||
serverErr := a.StartServer()
|
||||
require.NoError(t, serverErr)
|
||||
a.Srv().Shutdown()
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
var allPermissionIDs []string
|
||||
|
||||
func init() {
|
||||
for _, perm := range model.AllPermissions {
|
||||
allPermissionIDs = append(allPermissionIDs, perm.Id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnitUpdateConfig(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
|
||||
mockStore := th.App.Srv().Store().(*mocks.Store)
|
||||
mockUserStore := mocks.UserStore{}
|
||||
mockUserStore.On("Count", mock.Anything).Return(int64(10), nil)
|
||||
mockPostStore := mocks.PostStore{}
|
||||
mockPostStore.On("GetMaxPostSize").Return(65535, nil)
|
||||
mockSystemStore := mocks.SystemStore{}
|
||||
mockSystemStore.On("GetByName", "UpgradedFromTE").Return(&model.System{Name: "UpgradedFromTE", Value: "false"}, nil)
|
||||
mockSystemStore.On("GetByName", "InstallationDate").Return(&model.System{Name: "InstallationDate", Value: "10"}, nil)
|
||||
mockSystemStore.On("GetByName", "FirstServerRunTimestamp").Return(&model.System{Name: "FirstServerRunTimestamp", Value: "10"}, nil)
|
||||
mockLicenseStore := mocks.LicenseStore{}
|
||||
mockLicenseStore.On("Get", "").Return(&model.LicenseRecord{}, nil)
|
||||
mockStore.On("User").Return(&mockUserStore)
|
||||
mockStore.On("Post").Return(&mockPostStore)
|
||||
mockStore.On("System").Return(&mockSystemStore)
|
||||
mockStore.On("License").Return(&mockLicenseStore)
|
||||
mockStore.On("GetDBSchemaVersion").Return(1, nil)
|
||||
|
||||
prev := *th.App.Config().ServiceSettings.SiteURL
|
||||
|
||||
th.App.AddConfigListener(func(old, current *model.Config) {
|
||||
assert.Equal(t, prev, *old.ServiceSettings.SiteURL)
|
||||
assert.Equal(t, "http://foo.com", *current.ServiceSettings.SiteURL)
|
||||
})
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.SiteURL = "http://foo.com"
|
||||
})
|
||||
}
|
||||
|
||||
func TestDoAdvancedPermissionsMigration(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
th.ResetRoleMigration()
|
||||
|
||||
th.App.DoAdvancedPermissionsMigration()
|
||||
|
||||
roleNames := []string{
|
||||
"system_user",
|
||||
"system_admin",
|
||||
"team_user",
|
||||
"team_admin",
|
||||
"channel_user",
|
||||
"channel_admin",
|
||||
"system_post_all",
|
||||
"system_post_all_public",
|
||||
"system_user_access_token",
|
||||
"team_post_all",
|
||||
"team_post_all_public",
|
||||
"playbook_admin",
|
||||
"playbook_member",
|
||||
"run_admin",
|
||||
"run_member",
|
||||
}
|
||||
|
||||
roles1, err1 := th.App.GetRolesByNames(roleNames)
|
||||
assert.Nil(t, err1)
|
||||
assert.Equal(t, len(roles1), len(roleNames))
|
||||
|
||||
expected1 := map[string][]string{
|
||||
"channel_user": {
|
||||
model.PermissionReadChannel.Id,
|
||||
model.PermissionAddReaction.Id,
|
||||
model.PermissionRemoveReaction.Id,
|
||||
model.PermissionManagePublicChannelMembers.Id,
|
||||
model.PermissionUploadFile.Id,
|
||||
model.PermissionGetPublicLink.Id,
|
||||
model.PermissionCreatePost.Id,
|
||||
model.PermissionUseChannelMentions.Id,
|
||||
model.PermissionUseSlashCommands.Id,
|
||||
model.PermissionManagePublicChannelProperties.Id,
|
||||
model.PermissionDeletePublicChannel.Id,
|
||||
model.PermissionManagePrivateChannelProperties.Id,
|
||||
model.PermissionDeletePrivateChannel.Id,
|
||||
model.PermissionManagePrivateChannelMembers.Id,
|
||||
model.PermissionDeletePost.Id,
|
||||
model.PermissionEditPost.Id,
|
||||
},
|
||||
"channel_admin": {
|
||||
model.PermissionManageChannelRoles.Id,
|
||||
model.PermissionUseGroupMentions.Id,
|
||||
},
|
||||
"team_user": {
|
||||
model.PermissionListTeamChannels.Id,
|
||||
model.PermissionJoinPublicChannels.Id,
|
||||
model.PermissionReadPublicChannel.Id,
|
||||
model.PermissionViewTeam.Id,
|
||||
model.PermissionCreatePublicChannel.Id,
|
||||
model.PermissionCreatePrivateChannel.Id,
|
||||
model.PermissionInviteUser.Id,
|
||||
model.PermissionAddUserToTeam.Id,
|
||||
},
|
||||
"team_post_all": {
|
||||
model.PermissionCreatePost.Id,
|
||||
model.PermissionUseChannelMentions.Id,
|
||||
},
|
||||
"team_post_all_public": {
|
||||
model.PermissionCreatePostPublic.Id,
|
||||
model.PermissionUseChannelMentions.Id,
|
||||
},
|
||||
"team_admin": {
|
||||
model.PermissionRemoveUserFromTeam.Id,
|
||||
model.PermissionManageTeam.Id,
|
||||
model.PermissionImportTeam.Id,
|
||||
model.PermissionManageTeamRoles.Id,
|
||||
model.PermissionManageChannelRoles.Id,
|
||||
model.PermissionManageOthersIncomingWebhooks.Id,
|
||||
model.PermissionManageOthersOutgoingWebhooks.Id,
|
||||
model.PermissionManageSlashCommands.Id,
|
||||
model.PermissionManageOthersSlashCommands.Id,
|
||||
model.PermissionManageIncomingWebhooks.Id,
|
||||
model.PermissionManageOutgoingWebhooks.Id,
|
||||
model.PermissionConvertPublicChannelToPrivate.Id,
|
||||
model.PermissionConvertPrivateChannelToPublic.Id,
|
||||
model.PermissionDeletePost.Id,
|
||||
model.PermissionDeleteOthersPosts.Id,
|
||||
},
|
||||
"system_user": {
|
||||
model.PermissionListPublicTeams.Id,
|
||||
model.PermissionJoinPublicTeams.Id,
|
||||
model.PermissionCreateDirectChannel.Id,
|
||||
model.PermissionCreateGroupChannel.Id,
|
||||
model.PermissionViewMembers.Id,
|
||||
model.PermissionCreateTeam.Id,
|
||||
model.PermissionCreateCustomGroup.Id,
|
||||
model.PermissionEditCustomGroup.Id,
|
||||
model.PermissionDeleteCustomGroup.Id,
|
||||
model.PermissionRestoreCustomGroup.Id,
|
||||
model.PermissionManageCustomGroupMembers.Id,
|
||||
},
|
||||
"system_post_all": {
|
||||
model.PermissionCreatePost.Id,
|
||||
model.PermissionUseChannelMentions.Id,
|
||||
},
|
||||
"system_post_all_public": {
|
||||
model.PermissionCreatePostPublic.Id,
|
||||
model.PermissionUseChannelMentions.Id,
|
||||
},
|
||||
"system_user_access_token": {
|
||||
model.PermissionCreateUserAccessToken.Id,
|
||||
model.PermissionReadUserAccessToken.Id,
|
||||
model.PermissionRevokeUserAccessToken.Id,
|
||||
},
|
||||
"system_admin": allPermissionIDs,
|
||||
}
|
||||
assert.Contains(t, allPermissionIDs, model.PermissionManageSharedChannels.Id, "manage_shared_channels permission not found")
|
||||
assert.Contains(t, allPermissionIDs, model.PermissionManageSecureConnections.Id, "manage_secure_connections permission not found")
|
||||
|
||||
// Check the migration matches what's expected.
|
||||
for name, permissions := range expected1 {
|
||||
role, err := th.App.GetRoleByName(context.Background(), name)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, role.Permissions, permissions, fmt.Sprintf("role %q didn't match", name))
|
||||
}
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense())
|
||||
|
||||
// Check the migration doesn't change anything if run again.
|
||||
th.App.DoAdvancedPermissionsMigration()
|
||||
|
||||
roles2, err2 := th.App.GetRolesByNames(roleNames)
|
||||
assert.Nil(t, err2)
|
||||
assert.Equal(t, len(roles2), len(roleNames))
|
||||
|
||||
for name, permissions := range expected1 {
|
||||
role, err := th.App.GetRoleByName(context.Background(), name)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, permissions, role.Permissions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoEmojisPermissionsMigration(t *testing.T) {
|
||||
th := SetupWithoutPreloadMigrations(t)
|
||||
defer th.TearDown()
|
||||
|
||||
expectedSystemAdmin := allPermissionIDs
|
||||
sort.Strings(expectedSystemAdmin)
|
||||
|
||||
th.ResetEmojisMigration()
|
||||
th.App.DoEmojisPermissionsMigration()
|
||||
|
||||
role3, err3 := th.App.GetRoleByName(context.Background(), model.SystemUserRoleId)
|
||||
assert.Nil(t, err3)
|
||||
expected3 := []string{
|
||||
model.PermissionCreateCustomGroup.Id,
|
||||
model.PermissionEditCustomGroup.Id,
|
||||
model.PermissionDeleteCustomGroup.Id,
|
||||
model.PermissionManageCustomGroupMembers.Id,
|
||||
model.PermissionRestoreCustomGroup.Id,
|
||||
model.PermissionListPublicTeams.Id,
|
||||
model.PermissionJoinPublicTeams.Id,
|
||||
model.PermissionCreateDirectChannel.Id,
|
||||
model.PermissionCreateGroupChannel.Id,
|
||||
model.PermissionCreateTeam.Id,
|
||||
model.PermissionCreateEmojis.Id,
|
||||
model.PermissionDeleteEmojis.Id,
|
||||
model.PermissionViewMembers.Id,
|
||||
}
|
||||
sort.Strings(expected3)
|
||||
sort.Strings(role3.Permissions)
|
||||
assert.Equal(t, expected3, role3.Permissions, fmt.Sprintf("'%v' did not have expected permissions", model.SystemUserRoleId))
|
||||
|
||||
systemAdmin2, systemAdminErr2 := th.App.GetRoleByName(context.Background(), model.SystemAdminRoleId)
|
||||
assert.Nil(t, systemAdminErr2)
|
||||
sort.Strings(systemAdmin2.Permissions)
|
||||
assert.Equal(t, expectedSystemAdmin, systemAdmin2.Permissions, fmt.Sprintf("'%v' did not have expected permissions", model.SystemAdminRoleId))
|
||||
}
|
||||
|
||||
func TestDBHealthCheckWriteAndDelete(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
expectedKey := "health_check_" + th.App.GetClusterId()
|
||||
assert.Equal(t, expectedKey, th.App.dbHealthCheckKey())
|
||||
|
||||
_, err := th.App.Srv().Store().System().GetByName(expectedKey)
|
||||
assert.Error(t, err)
|
||||
|
||||
err = th.App.DBHealthCheckWrite()
|
||||
assert.NoError(t, err)
|
||||
|
||||
systemVal, err := th.App.Srv().Store().System().GetByName(expectedKey)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, systemVal)
|
||||
|
||||
err = th.App.DBHealthCheckDelete()
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = th.App.Srv().Store().System().GetByName(expectedKey)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
138
app/audit.go
138
app/audit.go
|
|
@ -1,138 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os/user"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/config"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/store"
|
||||
)
|
||||
|
||||
var (
|
||||
LevelAPI = mlog.LvlAuditAPI
|
||||
LevelContent = mlog.LvlAuditContent
|
||||
LevelPerms = mlog.LvlAuditPerms
|
||||
LevelCLI = mlog.LvlAuditCLI
|
||||
)
|
||||
|
||||
func (a *App) GetAudits(userID string, limit int) (model.Audits, *model.AppError) {
|
||||
audits, err := a.Srv().Store().Audit().Get(userID, 0, limit)
|
||||
if err != nil {
|
||||
var outErr *store.ErrOutOfBounds
|
||||
switch {
|
||||
case errors.As(err, &outErr):
|
||||
return nil, model.NewAppError("GetAudits", "app.audit.get.limit.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
default:
|
||||
return nil, model.NewAppError("GetAudits", "app.audit.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
return audits, nil
|
||||
}
|
||||
|
||||
func (a *App) GetAuditsPage(userID string, page int, perPage int) (model.Audits, *model.AppError) {
|
||||
audits, err := a.Srv().Store().Audit().Get(userID, page*perPage, perPage)
|
||||
if err != nil {
|
||||
var outErr *store.ErrOutOfBounds
|
||||
switch {
|
||||
case errors.As(err, &outErr):
|
||||
return nil, model.NewAppError("GetAuditsPage", "app.audit.get.limit.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
default:
|
||||
return nil, model.NewAppError("GetAuditsPage", "app.audit.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
return audits, nil
|
||||
}
|
||||
|
||||
// LogAuditRec logs an audit record using default LvlAuditCLI.
|
||||
func (a *App) LogAuditRec(rec *audit.Record, err error) {
|
||||
a.LogAuditRecWithLevel(rec, mlog.LvlAuditCLI, err)
|
||||
}
|
||||
|
||||
// LogAuditRecWithLevel logs an audit record using specified Level.
|
||||
func (a *App) LogAuditRecWithLevel(rec *audit.Record, level mlog.Level, err error) {
|
||||
if rec == nil {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
appErr, ok := err.(*model.AppError)
|
||||
if ok {
|
||||
rec.AddErrorCode(appErr.StatusCode)
|
||||
}
|
||||
rec.AddErrorDesc(appErr.Error())
|
||||
rec.Fail()
|
||||
}
|
||||
a.Srv().Audit.LogRecord(level, *rec)
|
||||
}
|
||||
|
||||
// MakeAuditRecord creates a audit record pre-populated with defaults.
|
||||
func (a *App) MakeAuditRecord(event string, initialStatus string) *audit.Record {
|
||||
var userID string
|
||||
user, err := user.Current()
|
||||
if err == nil {
|
||||
userID = fmt.Sprintf("%s:%s", user.Uid, user.Username)
|
||||
}
|
||||
|
||||
rec := &audit.Record{
|
||||
EventName: event,
|
||||
Status: initialStatus,
|
||||
Meta: map[string]interface{}{
|
||||
audit.KeyAPIPath: "",
|
||||
audit.KeyClusterID: a.GetClusterId(),
|
||||
},
|
||||
Actor: audit.EventActor{
|
||||
UserId: userID,
|
||||
SessionId: "",
|
||||
Client: fmt.Sprintf("server %s-%s", model.BuildNumber, model.BuildHash),
|
||||
IpAddress: "",
|
||||
},
|
||||
EventData: audit.EventData{
|
||||
Parameters: map[string]interface{}{},
|
||||
PriorState: map[string]interface{}{},
|
||||
ResultState: map[string]interface{}{},
|
||||
ObjectType: "",
|
||||
},
|
||||
}
|
||||
|
||||
return rec
|
||||
}
|
||||
|
||||
func (s *Server) configureAudit(adt *audit.Audit, bAllowAdvancedLogging bool) error {
|
||||
adt.OnQueueFull = s.onAuditTargetQueueFull
|
||||
adt.OnError = s.onAuditError
|
||||
|
||||
var logConfigSrc config.LogConfigSrc
|
||||
dsn := *s.platform.Config().ExperimentalAuditSettings.AdvancedLoggingConfig
|
||||
if bAllowAdvancedLogging && dsn != "" {
|
||||
var err error
|
||||
logConfigSrc, err = config.NewLogConfigSrc(dsn, s.platform.GetConfigStore())
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid config source for audit, %w", err)
|
||||
}
|
||||
mlog.Debug("Loaded audit configuration", mlog.String("source", dsn))
|
||||
}
|
||||
|
||||
// ExperimentalAuditSettings provides basic file audit (E0, E10); logConfigSrc provides advanced config (E20).
|
||||
cfg, err := config.MloggerConfigFromAuditConfig(s.platform.Config().ExperimentalAuditSettings, logConfigSrc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid config for audit, %w", err)
|
||||
}
|
||||
|
||||
return adt.Configure(cfg)
|
||||
}
|
||||
|
||||
func (s *Server) onAuditTargetQueueFull(qname string, maxQSize int) bool {
|
||||
mlog.Error("Audit queue full, dropping record.", mlog.String("qname", qname), mlog.Int("queueSize", maxQSize))
|
||||
return true // drop it
|
||||
}
|
||||
|
||||
func (s *Server) onAuditError(err error) {
|
||||
mlog.Error("Audit Error", mlog.Err(err))
|
||||
}
|
||||
658
app/bot.go
658
app/bot.go
|
|
@ -1,658 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/request"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/product"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/i18n"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/store"
|
||||
)
|
||||
|
||||
const (
|
||||
internalKeyPrefix = "mmi_"
|
||||
botUserKey = internalKeyPrefix + "botid"
|
||||
)
|
||||
|
||||
// Ensure bot service wrapper implements `product.BotService`
|
||||
var _ product.BotService = (*botServiceWrapper)(nil)
|
||||
|
||||
// botServiceWrapper provides an implementation of `product.BotService` for use by products.
|
||||
type botServiceWrapper struct {
|
||||
app AppIface
|
||||
}
|
||||
|
||||
func (w *botServiceWrapper) EnsureBot(c *request.Context, productID string, bot *model.Bot) (string, error) {
|
||||
return w.app.EnsureBot(c, productID, bot)
|
||||
}
|
||||
|
||||
// EnsureBot provides similar functionality with the plugin-api BotService. It doesn't accept
|
||||
// any ensureBotOptions hence it is not required for now.
|
||||
// TODO: Once the focalboard migration completed, we should add this logic to the app and
|
||||
// let plugin-api use the same code
|
||||
func (a *App) EnsureBot(c request.CTX, productID string, bot *model.Bot) (string, error) {
|
||||
if bot == nil {
|
||||
return "", errors.New("passed a nil bot")
|
||||
}
|
||||
|
||||
if bot.Username == "" {
|
||||
return "", errors.New("passed a bot with no username")
|
||||
}
|
||||
|
||||
botIDBytes, err := a.GetPluginKey(productID, botUserKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// If the bot has already been created, use it
|
||||
if botIDBytes != nil {
|
||||
botID := string(botIDBytes)
|
||||
|
||||
// ensure existing bot is synced with what is being created
|
||||
botPatch := &model.BotPatch{
|
||||
Username: &bot.Username,
|
||||
DisplayName: &bot.DisplayName,
|
||||
Description: &bot.Description,
|
||||
}
|
||||
|
||||
if _, err = a.PatchBot(botID, botPatch); err != nil {
|
||||
return "", fmt.Errorf("failed to patch bot: %w", err)
|
||||
}
|
||||
|
||||
return botID, nil
|
||||
}
|
||||
|
||||
// Check for an existing bot user with that username. If one exists, then use that.
|
||||
if user, appErr := a.GetUserByUsername(bot.Username); appErr == nil && user != nil {
|
||||
if user.IsBot {
|
||||
if appErr := a.SetPluginKey(productID, botUserKey, []byte(user.Id)); appErr != nil {
|
||||
return "", fmt.Errorf("failed to set plugin key: %w", err)
|
||||
}
|
||||
} else {
|
||||
c.Logger().Error("Product attempted to use an account that already exists. Convert user to a bot "+
|
||||
"account in the CLI by running 'mattermost user convert <username> --bot'. If the user is an "+
|
||||
"existing user account you want to preserve, change its username and restart the Mattermost server, "+
|
||||
"after which the plugin will create a bot account with that name. For more information about bot "+
|
||||
"accounts, see https://mattermost.com/pl/default-bot-accounts", mlog.String("username",
|
||||
bot.Username),
|
||||
mlog.String("user_id",
|
||||
user.Id),
|
||||
)
|
||||
}
|
||||
return user.Id, nil
|
||||
}
|
||||
|
||||
createdBot, err := a.CreateBot(c, bot)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create bot: %w", err)
|
||||
}
|
||||
|
||||
if appErr := a.SetPluginKey(productID, botUserKey, []byte(createdBot.UserId)); appErr != nil {
|
||||
return "", fmt.Errorf("failed to set plugin key: %w", err)
|
||||
}
|
||||
|
||||
return createdBot.UserId, nil
|
||||
}
|
||||
|
||||
// CreateBot creates the given bot and corresponding user.
|
||||
func (a *App) CreateBot(c request.CTX, bot *model.Bot) (*model.Bot, *model.AppError) {
|
||||
vErr := bot.IsValidCreate()
|
||||
if vErr != nil {
|
||||
return nil, vErr
|
||||
}
|
||||
|
||||
user, nErr := a.Srv().Store().User().Save(model.UserFromBot(bot))
|
||||
if nErr != nil {
|
||||
var appErr *model.AppError
|
||||
var invErr *store.ErrInvalidInput
|
||||
switch {
|
||||
case errors.As(nErr, &appErr):
|
||||
return nil, appErr
|
||||
case errors.As(nErr, &invErr):
|
||||
code := ""
|
||||
switch invErr.Field {
|
||||
case "email":
|
||||
code = "app.user.save.email_exists.app_error"
|
||||
case "username":
|
||||
code = "app.user.save.username_exists.app_error"
|
||||
default:
|
||||
code = "app.user.save.existing.app_error"
|
||||
}
|
||||
return nil, model.NewAppError("CreateBot", code, nil, "", http.StatusBadRequest).Wrap(nErr)
|
||||
default:
|
||||
return nil, model.NewAppError("CreateBot", "app.user.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
}
|
||||
bot.UserId = user.Id
|
||||
|
||||
savedBot, nErr := a.Srv().Store().Bot().Save(bot)
|
||||
if nErr != nil {
|
||||
a.Srv().Store().User().PermanentDelete(bot.UserId)
|
||||
var appErr *model.AppError
|
||||
switch {
|
||||
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
|
||||
return nil, appErr
|
||||
default: // last fallback in case it doesn't map to an existing app error.
|
||||
return nil, model.NewAppError("CreateBot", "app.bot.createbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
}
|
||||
|
||||
// Get the owner of the bot, if one exists. If not, don't send a message
|
||||
ownerUser, err := a.Srv().Store().User().Get(context.Background(), bot.OwnerId)
|
||||
var nfErr *store.ErrNotFound
|
||||
if err != nil && !errors.As(err, &nfErr) {
|
||||
return nil, model.NewAppError("CreateBot", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
} else if ownerUser != nil {
|
||||
// Send a message to the bot's creator to inform them that the bot needs to be added
|
||||
// to a team and channel after it's created
|
||||
botOwner, err := a.GetUser(bot.OwnerId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
channel, err := a.getOrCreateDirectChannelWithUser(c, user, botOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
T := i18n.GetUserTranslations(ownerUser.Locale)
|
||||
botAddPost := &model.Post{
|
||||
Type: model.PostTypeAddBotTeamsChannels,
|
||||
UserId: savedBot.UserId,
|
||||
ChannelId: channel.Id,
|
||||
Message: T("api.bot.teams_channels.add_message_mobile"),
|
||||
}
|
||||
|
||||
if _, err := a.CreatePostAsUser(c, botAddPost, c.Session().Id, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return savedBot, nil
|
||||
}
|
||||
|
||||
func (a *App) GetWarnMetricsBot() (*model.Bot, *model.AppError) {
|
||||
perPage := 1
|
||||
userOptions := &model.UserGetOptions{
|
||||
Page: 0,
|
||||
PerPage: perPage,
|
||||
Role: model.SystemAdminRoleId,
|
||||
Inactive: false,
|
||||
}
|
||||
|
||||
sysAdminList, err := a.GetUsersFromProfiles(userOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(sysAdminList) == 0 {
|
||||
return nil, model.NewAppError("GetWarnMetricsBot", "app.bot.get_warn_metrics_bot.empty_admin_list.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
T := i18n.GetUserTranslations(sysAdminList[0].Locale)
|
||||
warnMetricsBot := &model.Bot{
|
||||
Username: model.BotWarnMetricBotUsername,
|
||||
DisplayName: T("app.system.warn_metric.bot_displayname"),
|
||||
Description: "",
|
||||
OwnerId: sysAdminList[0].Id,
|
||||
}
|
||||
|
||||
return a.getOrCreateBot(warnMetricsBot)
|
||||
}
|
||||
|
||||
func (a *App) GetSystemBot() (*model.Bot, *model.AppError) {
|
||||
perPage := 1
|
||||
userOptions := &model.UserGetOptions{
|
||||
Page: 0,
|
||||
PerPage: perPage,
|
||||
Role: model.SystemAdminRoleId,
|
||||
Inactive: false,
|
||||
}
|
||||
|
||||
sysAdminList, err := a.GetUsersFromProfiles(userOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(sysAdminList) == 0 {
|
||||
return nil, model.NewAppError("GetSystemBot", "app.bot.get_system_bot.empty_admin_list.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
T := i18n.GetUserTranslations(sysAdminList[0].Locale)
|
||||
systemBot := &model.Bot{
|
||||
Username: model.BotSystemBotUsername,
|
||||
DisplayName: T("app.system.system_bot.bot_displayname"),
|
||||
Description: "",
|
||||
OwnerId: sysAdminList[0].Id,
|
||||
}
|
||||
|
||||
return a.getOrCreateBot(systemBot)
|
||||
}
|
||||
|
||||
func (a *App) getOrCreateBot(botDef *model.Bot) (*model.Bot, *model.AppError) {
|
||||
botUser, appErr := a.GetUserByUsername(botDef.Username)
|
||||
if appErr != nil {
|
||||
if appErr.StatusCode != http.StatusNotFound {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
// cannot find this bot user, save the user
|
||||
user, nErr := a.Srv().Store().User().Save(model.UserFromBot(botDef))
|
||||
if nErr != nil {
|
||||
var appError *model.AppError
|
||||
var invErr *store.ErrInvalidInput
|
||||
switch {
|
||||
case errors.As(nErr, &appError):
|
||||
return nil, appError
|
||||
case errors.As(nErr, &invErr):
|
||||
code := ""
|
||||
switch invErr.Field {
|
||||
case "email":
|
||||
code = "app.user.save.email_exists.app_error"
|
||||
case "username":
|
||||
code = "app.user.save.username_exists.app_error"
|
||||
default:
|
||||
code = "app.user.save.existing.app_error"
|
||||
}
|
||||
return nil, model.NewAppError("getOrCreateBot", code, nil, "", http.StatusBadRequest).Wrap(nErr)
|
||||
default:
|
||||
return nil, model.NewAppError("getOrCreateBot", "app.user.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
}
|
||||
botDef.UserId = user.Id
|
||||
|
||||
//save the bot
|
||||
savedBot, nErr := a.Srv().Store().Bot().Save(botDef)
|
||||
if nErr != nil {
|
||||
a.Srv().Store().User().PermanentDelete(savedBot.UserId)
|
||||
var nAppErr *model.AppError
|
||||
switch {
|
||||
case errors.As(nErr, &nAppErr): // in case we haven't converted to plain error.
|
||||
return nil, nAppErr
|
||||
default: // last fallback in case it doesn't map to an existing app error.
|
||||
return nil, model.NewAppError("getOrCreateBot", "app.bot.createbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
}
|
||||
return savedBot, nil
|
||||
}
|
||||
|
||||
if botUser == nil {
|
||||
return nil, model.NewAppError("getOrCreateBot", "app.bot.createbot.internal_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
//return the bot for this user
|
||||
savedBot, appErr := a.GetBot(botUser.Id, false)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
return savedBot, nil
|
||||
}
|
||||
|
||||
// PatchBot applies the given patch to the bot and corresponding user.
|
||||
func (a *App) PatchBot(botUserId string, botPatch *model.BotPatch) (*model.Bot, *model.AppError) {
|
||||
bot, err := a.GetBot(botUserId, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !bot.WouldPatch(botPatch) {
|
||||
return bot, nil
|
||||
}
|
||||
|
||||
bot.Patch(botPatch)
|
||||
|
||||
user, nErr := a.Srv().Store().User().Get(context.Background(), botUserId)
|
||||
if nErr != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(nErr, &nfErr):
|
||||
return nil, model.NewAppError("PatchBot", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
|
||||
default:
|
||||
return nil, model.NewAppError("PatchBot", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
}
|
||||
|
||||
patchedUser := model.UserFromBot(bot)
|
||||
user.Id = patchedUser.Id
|
||||
user.Username = patchedUser.Username
|
||||
user.Email = patchedUser.Email
|
||||
user.FirstName = patchedUser.FirstName
|
||||
|
||||
userUpdate, nErr := a.Srv().Store().User().Update(user, true)
|
||||
if nErr != nil {
|
||||
var appErr *model.AppError
|
||||
var invErr *store.ErrInvalidInput
|
||||
var conErr *store.ErrConflict
|
||||
switch {
|
||||
case errors.As(nErr, &appErr):
|
||||
return nil, appErr
|
||||
case errors.As(nErr, &invErr):
|
||||
return nil, model.NewAppError("PatchBot", "app.user.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
||||
case errors.As(nErr, &conErr):
|
||||
if conErr.Resource == "Username" {
|
||||
return nil, model.NewAppError("PatchBot", "app.user.save.username_exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
||||
}
|
||||
return nil, model.NewAppError("PatchBot", "app.user.save.email_exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
||||
default:
|
||||
return nil, model.NewAppError("PatchBot", "app.user.update.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
}
|
||||
a.InvalidateCacheForUser(user.Id)
|
||||
|
||||
ruser := userUpdate.New
|
||||
a.sendUpdatedUserEvent(*ruser)
|
||||
|
||||
bot, nErr = a.Srv().Store().Bot().Update(bot)
|
||||
if nErr != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
var appErr *model.AppError
|
||||
switch {
|
||||
case errors.As(nErr, &nfErr):
|
||||
return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(nErr)
|
||||
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
|
||||
return nil, appErr
|
||||
default: // last fallback in case it doesn't map to an existing app error.
|
||||
return nil, model.NewAppError("PatchBot", "app.bot.patchbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
}
|
||||
return bot, nil
|
||||
}
|
||||
|
||||
// GetBot returns the given bot.
|
||||
func (a *App) GetBot(botUserId string, includeDeleted bool) (*model.Bot, *model.AppError) {
|
||||
bot, err := a.Srv().Store().Bot().Get(botUserId, includeDeleted)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(err)
|
||||
default: // last fallback in case it doesn't map to an existing app error.
|
||||
return nil, model.NewAppError("GetBot", "app.bot.getbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
return bot, nil
|
||||
}
|
||||
|
||||
// GetBots returns the requested page of bots.
|
||||
func (a *App) GetBots(options *model.BotGetOptions) (model.BotList, *model.AppError) {
|
||||
bots, err := a.Srv().Store().Bot().GetAll(options)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetBots", "app.bot.getbots.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
return bots, nil
|
||||
}
|
||||
|
||||
// UpdateBotActive marks a bot as active or inactive, along with its corresponding user.
|
||||
func (a *App) UpdateBotActive(c request.CTX, botUserId string, active bool) (*model.Bot, *model.AppError) {
|
||||
user, nErr := a.Srv().Store().User().Get(context.Background(), botUserId)
|
||||
if nErr != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(nErr, &nfErr):
|
||||
return nil, model.NewAppError("PatchBot", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
|
||||
default:
|
||||
return nil, model.NewAppError("PatchBot", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := a.UpdateActive(c, user, active); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bot, nErr := a.Srv().Store().Bot().Get(botUserId, true)
|
||||
if nErr != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(nErr, &nfErr):
|
||||
return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(nErr)
|
||||
default: // last fallback in case it doesn't map to an existing app error.
|
||||
return nil, model.NewAppError("UpdateBotActive", "app.bot.getbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
}
|
||||
|
||||
changed := true
|
||||
if active && bot.DeleteAt != 0 {
|
||||
bot.DeleteAt = 0
|
||||
} else if !active && bot.DeleteAt == 0 {
|
||||
bot.DeleteAt = model.GetMillis()
|
||||
} else {
|
||||
changed = false
|
||||
}
|
||||
|
||||
if changed {
|
||||
bot, nErr = a.Srv().Store().Bot().Update(bot)
|
||||
if nErr != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
var appErr *model.AppError
|
||||
switch {
|
||||
case errors.As(nErr, &nfErr):
|
||||
return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(nErr)
|
||||
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
|
||||
return nil, appErr
|
||||
default: // last fallback in case it doesn't map to an existing app error.
|
||||
return nil, model.NewAppError("PatchBot", "app.bot.patchbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bot, nil
|
||||
}
|
||||
|
||||
// PermanentDeleteBot permanently deletes a bot and its corresponding user.
|
||||
func (a *App) PermanentDeleteBot(botUserId string) *model.AppError {
|
||||
if err := a.Srv().Store().Bot().PermanentDelete(botUserId); err != nil {
|
||||
var invErr *store.ErrInvalidInput
|
||||
switch {
|
||||
case errors.As(err, &invErr):
|
||||
return model.NewAppError("PermanentDeleteBot", "app.bot.permenent_delete.bad_id", map[string]any{"user_id": invErr.Value}, "", http.StatusBadRequest).Wrap(err)
|
||||
default: // last fallback in case it doesn't map to an existing app error.
|
||||
return model.NewAppError("PatchBot", "app.bot.permanent_delete.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := a.Srv().Store().User().PermanentDelete(botUserId); err != nil {
|
||||
return model.NewAppError("PermanentDeleteBot", "app.user.permanent_delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateBotOwner changes a bot's owner to the given value.
|
||||
func (a *App) UpdateBotOwner(botUserId, newOwnerId string) (*model.Bot, *model.AppError) {
|
||||
bot, err := a.Srv().Store().Bot().Get(botUserId, true)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(err)
|
||||
default: // last fallback in case it doesn't map to an existing app error.
|
||||
return nil, model.NewAppError("UpdateBotOwner", "app.bot.getbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
bot.OwnerId = newOwnerId
|
||||
|
||||
bot, err = a.Srv().Store().Bot().Update(bot)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
var appErr *model.AppError
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(err)
|
||||
case errors.As(err, &appErr): // in case we haven't converted to plain error.
|
||||
return nil, appErr
|
||||
default: // last fallback in case it doesn't map to an existing app error.
|
||||
return nil, model.NewAppError("PatchBot", "app.bot.patchbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return bot, nil
|
||||
}
|
||||
|
||||
// disableUserBots disables all bots owned by the given user.
|
||||
func (a *App) disableUserBots(c request.CTX, userID string) *model.AppError {
|
||||
perPage := 20
|
||||
for {
|
||||
options := &model.BotGetOptions{
|
||||
OwnerId: userID,
|
||||
IncludeDeleted: false,
|
||||
OnlyOrphaned: false,
|
||||
Page: 0,
|
||||
PerPage: perPage,
|
||||
}
|
||||
userBots, err := a.GetBots(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, bot := range userBots {
|
||||
_, err := a.UpdateBotActive(c, bot.UserId, false)
|
||||
if err != nil {
|
||||
c.Logger().Warn("Unable to deactivate bot.", mlog.String("bot_user_id", bot.UserId), mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Get next set of bots if we got the max number of bots
|
||||
if len(userBots) == perPage {
|
||||
options.Page += 1
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) notifySysadminsBotOwnerDeactivated(c request.CTX, userID string) *model.AppError {
|
||||
perPage := 25
|
||||
botOptions := &model.BotGetOptions{
|
||||
OwnerId: userID,
|
||||
IncludeDeleted: false,
|
||||
OnlyOrphaned: false,
|
||||
Page: 0,
|
||||
PerPage: perPage,
|
||||
}
|
||||
// get owner bots
|
||||
var userBots []*model.Bot
|
||||
for {
|
||||
bots, err := a.GetBots(botOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userBots = append(userBots, bots...)
|
||||
|
||||
if len(bots) < perPage {
|
||||
break
|
||||
}
|
||||
|
||||
botOptions.Page += 1
|
||||
}
|
||||
|
||||
// user does not own bots
|
||||
if len(userBots) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
userOptions := &model.UserGetOptions{
|
||||
Page: 0,
|
||||
PerPage: perPage,
|
||||
Role: model.SystemAdminRoleId,
|
||||
Inactive: false,
|
||||
}
|
||||
// get sysadmins
|
||||
var sysAdmins []*model.User
|
||||
for {
|
||||
sysAdminsList, err := a.GetUsersFromProfiles(userOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sysAdmins = append(sysAdmins, sysAdminsList...)
|
||||
|
||||
if len(sysAdminsList) < perPage {
|
||||
break
|
||||
}
|
||||
|
||||
userOptions.Page += 1
|
||||
}
|
||||
|
||||
// user being disabled
|
||||
user, err := a.GetUser(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// for each sysadmin, notify user that owns bots was disabled
|
||||
for _, sysAdmin := range sysAdmins {
|
||||
channel, appErr := a.GetOrCreateDirectChannel(c, sysAdmin.Id, sysAdmin.Id)
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
post := &model.Post{
|
||||
UserId: sysAdmin.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: a.getDisableBotSysadminMessage(user, userBots),
|
||||
Type: model.PostTypeSystemGeneric,
|
||||
}
|
||||
|
||||
_, appErr = a.CreatePost(c, post, channel, false, true)
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) getDisableBotSysadminMessage(user *model.User, userBots model.BotList) string {
|
||||
disableBotsSetting := *a.Config().ServiceSettings.DisableBotsWhenOwnerIsDeactivated
|
||||
|
||||
var printAllBots = true
|
||||
numBotsToPrint := len(userBots)
|
||||
|
||||
if numBotsToPrint > 10 {
|
||||
numBotsToPrint = 10
|
||||
printAllBots = false
|
||||
}
|
||||
|
||||
var message, botList string
|
||||
for _, bot := range userBots[:numBotsToPrint] {
|
||||
botList += fmt.Sprintf("* %v\n", bot.Username)
|
||||
}
|
||||
|
||||
T := i18n.GetUserTranslations(user.Locale)
|
||||
message = T("app.bot.get_disable_bot_sysadmin_message",
|
||||
map[string]any{
|
||||
"UserName": user.Username,
|
||||
"NumBots": len(userBots),
|
||||
"BotNames": botList,
|
||||
"disableBotsSetting": disableBotsSetting,
|
||||
"printAllBots": printAllBots,
|
||||
})
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
// ConvertUserToBot converts a user to bot.
|
||||
func (a *App) ConvertUserToBot(user *model.User) (*model.Bot, *model.AppError) {
|
||||
bot, err := a.Srv().Store().Bot().Save(model.BotFromUser(user))
|
||||
if err != nil {
|
||||
var appErr *model.AppError
|
||||
switch {
|
||||
case errors.As(err, &appErr): // in case we haven't converted to plain error.
|
||||
return nil, appErr
|
||||
default: // last fallback in case it doesn't map to an existing app error.
|
||||
return nil, model.NewAppError("CreateBot", "app.bot.createbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
return bot, nil
|
||||
}
|
||||
155
app/busy.go
155
app/busy.go
|
|
@ -1,155 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
const (
|
||||
TimestampFormat = "Mon Jan 2 15:04:05 -0700 MST 2006"
|
||||
)
|
||||
|
||||
// Busy represents the busy state of the server. A server marked busy
|
||||
// will have non-critical services disabled. If a Cluster is provided
|
||||
// any changes will be propagated to each node.
|
||||
type Busy struct {
|
||||
busy int32 // protected via atomic for fast IsBusy calls
|
||||
mux sync.RWMutex
|
||||
timer *time.Timer
|
||||
expires time.Time
|
||||
|
||||
cluster einterfaces.ClusterInterface
|
||||
}
|
||||
|
||||
// NewBusy creates a new Busy instance with optional cluster which will
|
||||
// be notified of busy state changes.
|
||||
func NewBusy(cluster einterfaces.ClusterInterface) *Busy {
|
||||
return &Busy{cluster: cluster}
|
||||
}
|
||||
|
||||
// IsBusy returns true if the server has been marked as busy.
|
||||
func (b *Busy) IsBusy() bool {
|
||||
if b == nil {
|
||||
return false
|
||||
}
|
||||
return atomic.LoadInt32(&b.busy) != 0
|
||||
}
|
||||
|
||||
// Set marks the server as busy for dur duration and notifies cluster nodes.
|
||||
func (b *Busy) Set(dur time.Duration) {
|
||||
b.mux.Lock()
|
||||
defer b.mux.Unlock()
|
||||
|
||||
// minimum 1 second
|
||||
if dur < (time.Second * 1) {
|
||||
dur = time.Second * 1
|
||||
}
|
||||
|
||||
b.setWithoutNotify(dur)
|
||||
|
||||
if b.cluster != nil {
|
||||
sbs := &model.ServerBusyState{Busy: true, Expires: b.expires.Unix(), ExpiresTS: b.expires.UTC().Format(TimestampFormat)}
|
||||
b.notifyServerBusyChange(sbs)
|
||||
}
|
||||
}
|
||||
|
||||
// must hold mutex
|
||||
func (b *Busy) setWithoutNotify(dur time.Duration) {
|
||||
b.clearWithoutNotify()
|
||||
atomic.StoreInt32(&b.busy, 1)
|
||||
b.expires = time.Now().Add(dur)
|
||||
b.timer = time.AfterFunc(dur, func() {
|
||||
b.mux.Lock()
|
||||
b.clearWithoutNotify()
|
||||
b.mux.Unlock()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearBusy marks the server as not busy and notifies cluster nodes.
|
||||
func (b *Busy) Clear() {
|
||||
b.mux.Lock()
|
||||
defer b.mux.Unlock()
|
||||
|
||||
b.clearWithoutNotify()
|
||||
|
||||
if b.cluster != nil {
|
||||
sbs := &model.ServerBusyState{Busy: false, Expires: time.Time{}.Unix(), ExpiresTS: ""}
|
||||
b.notifyServerBusyChange(sbs)
|
||||
}
|
||||
}
|
||||
|
||||
// must hold mutex
|
||||
func (b *Busy) clearWithoutNotify() {
|
||||
if b.timer != nil {
|
||||
b.timer.Stop() // don't drain timer.C channel for AfterFunc timers.
|
||||
}
|
||||
b.timer = nil
|
||||
b.expires = time.Time{}
|
||||
atomic.StoreInt32(&b.busy, 0)
|
||||
}
|
||||
|
||||
// Expires returns the expected time that the server
|
||||
// will be marked not busy. This expiry can be extended
|
||||
// via additional calls to SetBusy.
|
||||
func (b *Busy) Expires() time.Time {
|
||||
b.mux.RLock()
|
||||
defer b.mux.RUnlock()
|
||||
return b.expires
|
||||
}
|
||||
|
||||
// notifyServerBusyChange informs all cluster members of a server busy state change.
|
||||
func (b *Busy) notifyServerBusyChange(sbs *model.ServerBusyState) {
|
||||
if b.cluster == nil {
|
||||
return
|
||||
}
|
||||
buf, _ := json.Marshal(sbs)
|
||||
msg := &model.ClusterMessage{
|
||||
Event: model.ClusterEventBusyStateChanged,
|
||||
SendType: model.ClusterSendReliable,
|
||||
WaitForAllToSend: true,
|
||||
Data: buf,
|
||||
}
|
||||
b.cluster.SendClusterMessage(msg)
|
||||
}
|
||||
|
||||
// ClusterEventChanged is called when a CLUSTER_EVENT_BUSY_STATE_CHANGED is received.
|
||||
func (b *Busy) ClusterEventChanged(sbs *model.ServerBusyState) {
|
||||
b.mux.Lock()
|
||||
defer b.mux.Unlock()
|
||||
|
||||
if sbs.Busy {
|
||||
expires := time.Unix(sbs.Expires, 0)
|
||||
dur := time.Until(expires)
|
||||
if dur > 0 {
|
||||
b.setWithoutNotify(dur)
|
||||
}
|
||||
} else {
|
||||
b.clearWithoutNotify()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Busy) ToJSON() ([]byte, error) {
|
||||
b.mux.RLock()
|
||||
defer b.mux.RUnlock()
|
||||
|
||||
sbs := &model.ServerBusyState{
|
||||
Busy: atomic.LoadInt32(&b.busy) != 0,
|
||||
Expires: b.expires.Unix(),
|
||||
ExpiresTS: b.expires.UTC().Format(TimestampFormat),
|
||||
}
|
||||
sbsJSON, jsonErr := json.Marshal(sbs)
|
||||
if jsonErr != nil {
|
||||
return []byte{}, fmt.Errorf("failed to encode server busy state to JSON: %w", jsonErr)
|
||||
}
|
||||
|
||||
return sbsJSON, nil
|
||||
}
|
||||
153
app/busy_test.go
153
app/busy_test.go
|
|
@ -1,153 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
func TestBusySet(t *testing.T) {
|
||||
cluster := &ClusterMock{Busy: &Busy{}}
|
||||
busy := NewBusy(cluster)
|
||||
|
||||
isNotBusy := func() bool {
|
||||
return !busy.IsBusy()
|
||||
}
|
||||
|
||||
require.False(t, busy.IsBusy())
|
||||
|
||||
busy.Set(time.Millisecond * 500)
|
||||
require.True(t, busy.IsBusy())
|
||||
require.True(t, compareBusyState(t, busy, cluster.Busy))
|
||||
|
||||
// should automatically expire after 500ms.
|
||||
require.Eventually(t, isNotBusy, time.Second*15, time.Millisecond*100)
|
||||
// allow a moment for cluster to sync.
|
||||
require.Eventually(t, func() bool { return compareBusyState(t, busy, cluster.Busy) }, time.Second*15, time.Millisecond*20)
|
||||
|
||||
// test set after auto expiry.
|
||||
busy.Set(time.Second * 30)
|
||||
require.True(t, busy.IsBusy())
|
||||
require.True(t, compareBusyState(t, busy, cluster.Busy))
|
||||
expire := busy.Expires()
|
||||
require.Greater(t, expire.Unix(), time.Now().Add(time.Second*10).Unix())
|
||||
|
||||
// test extending existing expiry
|
||||
busy.Set(time.Minute * 5)
|
||||
require.True(t, busy.IsBusy())
|
||||
require.True(t, compareBusyState(t, busy, cluster.Busy))
|
||||
expire = busy.Expires()
|
||||
require.Greater(t, expire.Unix(), time.Now().Add(time.Minute*2).Unix())
|
||||
|
||||
busy.Clear()
|
||||
require.False(t, busy.IsBusy())
|
||||
require.True(t, compareBusyState(t, busy, cluster.Busy))
|
||||
}
|
||||
|
||||
func TestBusyExpires(t *testing.T) {
|
||||
cluster := &ClusterMock{Busy: &Busy{}}
|
||||
busy := NewBusy(cluster)
|
||||
|
||||
isNotBusy := func() bool {
|
||||
return !busy.IsBusy()
|
||||
}
|
||||
|
||||
// get expiry before it is set
|
||||
expire := busy.Expires()
|
||||
// should be time.Time zero value
|
||||
require.Equal(t, time.Time{}.Unix(), expire.Unix())
|
||||
|
||||
// get expiry after it is set
|
||||
busy.Set(time.Minute * 5)
|
||||
expire = busy.Expires()
|
||||
require.Greater(t, expire.Unix(), time.Now().Add(time.Minute*2).Unix())
|
||||
require.True(t, compareBusyState(t, busy, cluster.Busy))
|
||||
|
||||
// get expiry after clear
|
||||
busy.Clear()
|
||||
expire = busy.Expires()
|
||||
// should be time.Time zero value
|
||||
require.Equal(t, time.Time{}.Unix(), expire.Unix())
|
||||
require.True(t, compareBusyState(t, busy, cluster.Busy))
|
||||
|
||||
// get expiry after auto-expire
|
||||
busy.Set(time.Millisecond * 100)
|
||||
require.Eventually(t, isNotBusy, time.Second*5, time.Millisecond*20)
|
||||
expire = busy.Expires()
|
||||
// should be time.Time zero value
|
||||
require.Equal(t, time.Time{}.Unix(), expire.Unix())
|
||||
// allow a moment for cluster to sync
|
||||
require.Eventually(t, func() bool { return compareBusyState(t, busy, cluster.Busy) }, time.Second*15, time.Millisecond*20)
|
||||
}
|
||||
|
||||
func TestBusyRace(t *testing.T) {
|
||||
cluster := &ClusterMock{Busy: &Busy{}}
|
||||
busy := NewBusy(cluster)
|
||||
|
||||
busy.Set(500 * time.Millisecond)
|
||||
|
||||
// We are sleeping in order to let the race trigger.
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
func compareBusyState(t *testing.T, busy1 *Busy, busy2 *Busy) bool {
|
||||
t.Helper()
|
||||
if busy1.IsBusy() != busy2.IsBusy() {
|
||||
busy1JSON, _ := busy1.ToJSON()
|
||||
busy2JSON, _ := busy2.ToJSON()
|
||||
t.Logf("busy1:%s; busy2:%s\n", busy1JSON, busy2JSON)
|
||||
return false
|
||||
}
|
||||
if busy1.Expires().Unix() != busy2.Expires().Unix() {
|
||||
busy1JSON, _ := busy1.ToJSON()
|
||||
busy2JSON, _ := busy2.ToJSON()
|
||||
t.Logf("busy1:%s; busy2:%s\n", busy1JSON, busy2JSON)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ClusterMock simulates the busy state of a cluster.
|
||||
type ClusterMock struct {
|
||||
Busy *Busy
|
||||
}
|
||||
|
||||
func (c *ClusterMock) SendClusterMessage(msg *model.ClusterMessage) {
|
||||
var sbs model.ServerBusyState
|
||||
json.Unmarshal(msg.Data, &sbs)
|
||||
c.Busy.ClusterEventChanged(&sbs)
|
||||
}
|
||||
|
||||
func (c *ClusterMock) SendClusterMessageToNode(nodeID string, msg *model.ClusterMessage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClusterMock) StartInterNodeCommunication() {}
|
||||
func (c *ClusterMock) StopInterNodeCommunication() {}
|
||||
func (c *ClusterMock) RegisterClusterMessageHandler(event model.ClusterEvent, crm einterfaces.ClusterMessageHandler) {
|
||||
}
|
||||
func (c *ClusterMock) GetClusterId() string { return "cluster_mock" }
|
||||
func (c *ClusterMock) IsLeader() bool { return false }
|
||||
func (c *ClusterMock) GetMyClusterInfo() *model.ClusterInfo { return nil }
|
||||
func (c *ClusterMock) GetClusterInfos() []*model.ClusterInfo { return nil }
|
||||
func (c *ClusterMock) NotifyMsg(buf []byte) {}
|
||||
func (c *ClusterMock) GetClusterStats() ([]*model.ClusterStats, *model.AppError) { return nil, nil }
|
||||
func (c *ClusterMock) GetLogs(page, perPage int) ([]string, *model.AppError) {
|
||||
return nil, nil
|
||||
}
|
||||
func (c *ClusterMock) QueryLogs(page, perPage int) (map[string][]string, *model.AppError) {
|
||||
return nil, nil
|
||||
}
|
||||
func (c *ClusterMock) GetPluginStatuses() (model.PluginStatuses, *model.AppError) { return nil, nil }
|
||||
func (c *ClusterMock) ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError {
|
||||
return nil
|
||||
}
|
||||
func (c *ClusterMock) HealthScore() int { return 0 }
|
||||
3574
app/channel.go
3574
app/channel.go
File diff suppressed because it is too large
Load diff
|
|
@ -1,288 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/request"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/store"
|
||||
)
|
||||
|
||||
func (a *App) createInitialSidebarCategories(userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, *model.AppError) {
|
||||
categories, nErr := a.Srv().Store().Channel().CreateInitialSidebarCategories(userID, opts)
|
||||
if nErr != nil {
|
||||
return nil, model.NewAppError("createInitialSidebarCategories", "app.channel.create_initial_sidebar_categories.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
|
||||
return categories, nil
|
||||
}
|
||||
|
||||
func (a *App) GetSidebarCategoriesForTeamForUser(c request.CTX, userID, teamID string) (*model.OrderedSidebarCategories, *model.AppError) {
|
||||
var appErr *model.AppError
|
||||
categories, err := a.Srv().Store().Channel().GetSidebarCategoriesForTeamForUser(userID, teamID)
|
||||
if err == nil && len(categories.Categories) == 0 {
|
||||
// A user must always have categories, so migration must not have happened yet, and we should run it ourselves
|
||||
categories, appErr = a.createInitialSidebarCategories(userID, &store.SidebarCategorySearchOpts{
|
||||
TeamID: teamID,
|
||||
ExcludeTeam: false,
|
||||
})
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return nil, model.NewAppError("GetSidebarCategoriesForTeamForUser", "app.channel.sidebar_categories.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
||||
default:
|
||||
return nil, model.NewAppError("GetSidebarCategoriesForTeamForUser", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return categories, nil
|
||||
}
|
||||
|
||||
func (a *App) GetSidebarCategories(c request.CTX, userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, *model.AppError) {
|
||||
var appErr *model.AppError
|
||||
categories, err := a.Srv().Store().Channel().GetSidebarCategories(userID, opts)
|
||||
if err == nil && len(categories.Categories) == 0 {
|
||||
// A user must always have categories, so migration must not have happened yet, and we should run it ourselves
|
||||
categories, appErr = a.createInitialSidebarCategories(userID, opts)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return nil, model.NewAppError("GetSidebarCategories", "app.channel.sidebar_categories.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
||||
default:
|
||||
return nil, model.NewAppError("GetSidebarCategories", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return categories, nil
|
||||
}
|
||||
|
||||
func (a *App) GetSidebarCategoryOrder(c request.CTX, userID, teamID string) ([]string, *model.AppError) {
|
||||
categories, err := a.Srv().Store().Channel().GetSidebarCategoryOrder(userID, teamID)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return nil, model.NewAppError("GetSidebarCategoryOrder", "app.channel.sidebar_categories.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
||||
default:
|
||||
return nil, model.NewAppError("GetSidebarCategoryOrder", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return categories, nil
|
||||
}
|
||||
|
||||
func (a *App) GetSidebarCategory(c request.CTX, categoryId string) (*model.SidebarCategoryWithChannels, *model.AppError) {
|
||||
category, err := a.Srv().Store().Channel().GetSidebarCategory(categoryId)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return nil, model.NewAppError("GetSidebarCategory", "app.channel.sidebar_categories.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
||||
default:
|
||||
return nil, model.NewAppError("GetSidebarCategory", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return category, nil
|
||||
}
|
||||
|
||||
func (a *App) CreateSidebarCategory(c request.CTX, userID, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) {
|
||||
category, err := a.Srv().Store().Channel().CreateSidebarCategory(userID, teamID, newCategory)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return nil, model.NewAppError("CreateSidebarCategory", "app.channel.sidebar_categories.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
||||
default:
|
||||
return nil, model.NewAppError("CreateSidebarCategory", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventSidebarCategoryCreated, teamID, "", userID, nil, "")
|
||||
message.Add("category_id", category.Id)
|
||||
a.Publish(message)
|
||||
return category, nil
|
||||
}
|
||||
|
||||
func (a *App) UpdateSidebarCategoryOrder(c request.CTX, userID, teamID string, categoryOrder []string) *model.AppError {
|
||||
err := a.Srv().Store().Channel().UpdateSidebarCategoryOrder(userID, teamID, categoryOrder)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
var invErr *store.ErrInvalidInput
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return model.NewAppError("UpdateSidebarCategoryOrder", "app.channel.sidebar_categories.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
||||
case errors.As(err, &invErr):
|
||||
return model.NewAppError("UpdateSidebarCategoryOrder", "app.channel.sidebar_categories.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
default:
|
||||
return model.NewAppError("UpdateSidebarCategoryOrder", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventSidebarCategoryOrderUpdated, teamID, "", userID, nil, "")
|
||||
message.Add("order", categoryOrder)
|
||||
a.Publish(message)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) UpdateSidebarCategories(c request.CTX, userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) {
|
||||
updatedCategories, originalCategories, err := a.Srv().Store().Channel().UpdateSidebarCategories(userID, teamID, categories)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("UpdateSidebarCategories", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventSidebarCategoryUpdated, teamID, "", userID, nil, "")
|
||||
|
||||
updatedCategoriesJSON, jsonErr := json.Marshal(updatedCategories)
|
||||
if jsonErr != nil {
|
||||
return nil, model.NewAppError("UpdateSidebarCategories", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
|
||||
}
|
||||
|
||||
message.Add("updatedCategories", string(updatedCategoriesJSON))
|
||||
|
||||
a.Publish(message)
|
||||
|
||||
a.muteChannelsForUpdatedCategories(c, userID, updatedCategories, originalCategories)
|
||||
|
||||
return updatedCategories, nil
|
||||
}
|
||||
|
||||
func (a *App) muteChannelsForUpdatedCategories(c request.CTX, userID string, updatedCategories []*model.SidebarCategoryWithChannels, originalCategories []*model.SidebarCategoryWithChannels) {
|
||||
var channelsToMute []string
|
||||
var channelsToUnmute []string
|
||||
|
||||
// Mute or unmute all channels in categories that were muted or unmuted
|
||||
for i, updatedCategory := range updatedCategories {
|
||||
if i > len(originalCategories)-1 {
|
||||
// The two slices should be the same length, but double check that to be safe
|
||||
continue
|
||||
}
|
||||
|
||||
originalCategory := originalCategories[i]
|
||||
|
||||
if updatedCategory.Muted && !originalCategory.Muted {
|
||||
channelsToMute = append(channelsToMute, updatedCategory.Channels...)
|
||||
} else if !updatedCategory.Muted && originalCategory.Muted {
|
||||
channelsToUnmute = append(channelsToUnmute, updatedCategory.Channels...)
|
||||
}
|
||||
}
|
||||
|
||||
// Mute any channels moved from an unmuted category into a muted one and vice versa
|
||||
channelsDiff := diffChannelsBetweenCategories(updatedCategories, originalCategories)
|
||||
if len(channelsDiff) != 0 {
|
||||
makeCategoryMap := func(categories []*model.SidebarCategoryWithChannels) map[string]*model.SidebarCategoryWithChannels {
|
||||
result := make(map[string]*model.SidebarCategoryWithChannels)
|
||||
for _, category := range categories {
|
||||
result[category.Id] = category
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
updatedCategoriesById := makeCategoryMap(updatedCategories)
|
||||
originalCategoriesById := makeCategoryMap(originalCategories)
|
||||
|
||||
for channelID, diff := range channelsDiff {
|
||||
fromCategory := originalCategoriesById[diff.fromCategoryId]
|
||||
toCategory := updatedCategoriesById[diff.toCategoryId]
|
||||
|
||||
if toCategory.Muted && !fromCategory.Muted {
|
||||
channelsToMute = append(channelsToMute, channelID)
|
||||
} else if !toCategory.Muted && fromCategory.Muted {
|
||||
channelsToUnmute = append(channelsToUnmute, channelID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(channelsToMute) > 0 {
|
||||
_, err := a.setChannelsMuted(c, channelsToMute, userID, true)
|
||||
if err != nil {
|
||||
c.Logger().Error(
|
||||
"Failed to mute channels to match category",
|
||||
mlog.String("user_id", userID),
|
||||
mlog.Err(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if len(channelsToUnmute) > 0 {
|
||||
_, err := a.setChannelsMuted(c, channelsToUnmute, userID, false)
|
||||
if err != nil {
|
||||
c.Logger().Error(
|
||||
"Failed to unmute channels to match category",
|
||||
mlog.String("user_id", userID),
|
||||
mlog.Err(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type categoryChannelDiff struct {
|
||||
fromCategoryId string
|
||||
toCategoryId string
|
||||
}
|
||||
|
||||
func diffChannelsBetweenCategories(updatedCategories []*model.SidebarCategoryWithChannels, originalCategories []*model.SidebarCategoryWithChannels) map[string]*categoryChannelDiff {
|
||||
// mapChannelIdsToCategories returns a map of channel IDs to the IDs of the categories that they're a member of.
|
||||
mapChannelIdsToCategories := func(categories []*model.SidebarCategoryWithChannels) map[string]string {
|
||||
result := make(map[string]string)
|
||||
for _, category := range categories {
|
||||
for _, channelID := range category.Channels {
|
||||
result[channelID] = category.Id
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
updatedChannelIdsMap := mapChannelIdsToCategories(updatedCategories)
|
||||
originalChannelIdsMap := mapChannelIdsToCategories(originalCategories)
|
||||
|
||||
// Check for any channels that have changed categories. Note that we don't worry about any channels that have moved
|
||||
// outside of these categories since that heavily complicates things and doesn't currently happen in our apps.
|
||||
channelsDiff := make(map[string]*categoryChannelDiff)
|
||||
for channelID, originalCategoryId := range originalChannelIdsMap {
|
||||
updatedCategoryId := updatedChannelIdsMap[channelID]
|
||||
|
||||
if originalCategoryId != updatedCategoryId && updatedCategoryId != "" {
|
||||
channelsDiff[channelID] = &categoryChannelDiff{originalCategoryId, updatedCategoryId}
|
||||
}
|
||||
}
|
||||
|
||||
return channelsDiff
|
||||
}
|
||||
|
||||
func (a *App) DeleteSidebarCategory(c request.CTX, userID, teamID, categoryId string) *model.AppError {
|
||||
err := a.Srv().Store().Channel().DeleteSidebarCategory(categoryId)
|
||||
if err != nil {
|
||||
var invErr *store.ErrInvalidInput
|
||||
switch {
|
||||
case errors.As(err, &invErr):
|
||||
return model.NewAppError("DeleteSidebarCategory", "app.channel.sidebar_categories.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
default:
|
||||
return model.NewAppError("DeleteSidebarCategory", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventSidebarCategoryDeleted, teamID, "", userID, nil, "")
|
||||
message.Add("category_id", categoryId)
|
||||
a.Publish(message)
|
||||
|
||||
return nil
|
||||
}
|
||||
2900
app/channel_test.go
2900
app/channel_test.go
File diff suppressed because it is too large
Load diff
387
app/channels.go
387
app/channels.go
|
|
@ -1,387 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/imaging"
|
||||
"github.com/mattermost/mattermost-server/v6/app/request"
|
||||
"github.com/mattermost/mattermost-server/v6/config"
|
||||
"github.com/mattermost/mattermost-server/v6/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/plugin"
|
||||
"github.com/mattermost/mattermost-server/v6/product"
|
||||
"github.com/mattermost/mattermost-server/v6/services/imageproxy"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/filestore"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
const ServerKey product.ServiceKey = "server"
|
||||
|
||||
// licenseSvc is added to act as a starting point for future integrated products.
|
||||
// It has the same signature and functionality with the license related APIs of the plugin-api.
|
||||
type licenseSvc interface {
|
||||
GetLicense() *model.License
|
||||
RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError
|
||||
}
|
||||
|
||||
// Channels contains all channels related state.
|
||||
type Channels struct {
|
||||
srv *Server
|
||||
cfgSvc product.ConfigService
|
||||
filestore filestore.FileBackend
|
||||
licenseSvc licenseSvc
|
||||
routerSvc *routerService
|
||||
|
||||
postActionCookieSecret []byte
|
||||
|
||||
pluginCommandsLock sync.RWMutex
|
||||
pluginCommands []*PluginCommand
|
||||
pluginsLock sync.RWMutex
|
||||
pluginsEnvironment *plugin.Environment
|
||||
pluginConfigListenerID string
|
||||
|
||||
imageProxy *imageproxy.ImageProxy
|
||||
|
||||
// cached counts that are used during notice condition validation
|
||||
cachedPostCount int64
|
||||
cachedUserCount int64
|
||||
cachedDBMSVersion string
|
||||
// previously fetched notices
|
||||
cachedNotices model.ProductNotices
|
||||
|
||||
AccountMigration einterfaces.AccountMigrationInterface
|
||||
Compliance einterfaces.ComplianceInterface
|
||||
DataRetention einterfaces.DataRetentionInterface
|
||||
MessageExport einterfaces.MessageExportInterface
|
||||
Saml einterfaces.SamlInterface
|
||||
Notification einterfaces.NotificationInterface
|
||||
Ldap einterfaces.LdapInterface
|
||||
|
||||
// These are used to prevent concurrent upload requests
|
||||
// for a given upload session which could cause inconsistencies
|
||||
// and data corruption.
|
||||
uploadLockMapMut sync.Mutex
|
||||
uploadLockMap map[string]bool
|
||||
|
||||
imgDecoder *imaging.Decoder
|
||||
imgEncoder *imaging.Encoder
|
||||
|
||||
dndTaskMut sync.Mutex
|
||||
dndTask *model.ScheduledTask
|
||||
|
||||
postReminderMut sync.Mutex
|
||||
postReminderTask *model.ScheduledTask
|
||||
|
||||
// collectionTypes maps from collection types to the registering plugin id
|
||||
collectionTypes map[string]string
|
||||
// topicTypes maps from topic types to collection types
|
||||
topicTypes map[string]string
|
||||
collectionAndTopicTypesMut sync.Mutex
|
||||
}
|
||||
|
||||
func init() {
|
||||
product.RegisterProduct("channels", product.Manifest{
|
||||
Initializer: func(services map[product.ServiceKey]any) (product.Product, error) {
|
||||
return NewChannels(services)
|
||||
},
|
||||
Dependencies: map[product.ServiceKey]struct{}{
|
||||
ServerKey: {},
|
||||
product.ConfigKey: {},
|
||||
product.LicenseKey: {},
|
||||
product.FilestoreKey: {},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func NewChannels(services map[product.ServiceKey]any) (*Channels, error) {
|
||||
s, ok := services[ServerKey].(*Server)
|
||||
if !ok {
|
||||
return nil, errors.New("server not passed")
|
||||
}
|
||||
ch := &Channels{
|
||||
srv: s,
|
||||
imageProxy: imageproxy.MakeImageProxy(s.platform, s.httpService, s.Log()),
|
||||
uploadLockMap: map[string]bool{},
|
||||
collectionTypes: map[string]string{},
|
||||
topicTypes: map[string]string{},
|
||||
}
|
||||
|
||||
// To get another service:
|
||||
// 1. Prepare the service interface
|
||||
// 2. Add the field to *Channels
|
||||
// 3. Add the service key to the slice.
|
||||
// 4. Add a new case in the switch statement.
|
||||
requiredServices := []product.ServiceKey{
|
||||
product.ConfigKey,
|
||||
product.LicenseKey,
|
||||
product.FilestoreKey,
|
||||
}
|
||||
for _, svcKey := range requiredServices {
|
||||
svc, ok := services[svcKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Service %s not passed", svcKey)
|
||||
}
|
||||
switch svcKey {
|
||||
// Keep adding more services here
|
||||
case product.ConfigKey:
|
||||
cfgSvc, ok := svc.(product.ConfigService)
|
||||
if !ok {
|
||||
return nil, errors.New("Config service did not satisfy ConfigSvc interface")
|
||||
}
|
||||
ch.cfgSvc = cfgSvc
|
||||
case product.FilestoreKey:
|
||||
filestore, ok := svc.(filestore.FileBackend)
|
||||
if !ok {
|
||||
return nil, errors.New("Filestore service did not satisfy FileBackend interface")
|
||||
}
|
||||
ch.filestore = filestore
|
||||
case product.LicenseKey:
|
||||
svc, ok := svc.(licenseSvc)
|
||||
if !ok {
|
||||
return nil, errors.New("License service did not satisfy licenseSvc interface")
|
||||
}
|
||||
ch.licenseSvc = svc
|
||||
}
|
||||
}
|
||||
// We are passing a partially filled Channels struct so that the enterprise
|
||||
// methods can have access to app methods.
|
||||
// Otherwise, passing server would mean it has to call s.Channels(),
|
||||
// which would be nil at this point.
|
||||
if complianceInterface != nil {
|
||||
ch.Compliance = complianceInterface(New(ServerConnector(ch)))
|
||||
}
|
||||
if messageExportInterface != nil {
|
||||
ch.MessageExport = messageExportInterface(New(ServerConnector(ch)))
|
||||
}
|
||||
if dataRetentionInterface != nil {
|
||||
ch.DataRetention = dataRetentionInterface(New(ServerConnector(ch)))
|
||||
}
|
||||
if accountMigrationInterface != nil {
|
||||
ch.AccountMigration = accountMigrationInterface(New(ServerConnector(ch)))
|
||||
}
|
||||
if ldapInterface != nil {
|
||||
ch.Ldap = ldapInterface(New(ServerConnector(ch)))
|
||||
}
|
||||
if notificationInterface != nil {
|
||||
ch.Notification = notificationInterface(New(ServerConnector(ch)))
|
||||
}
|
||||
if samlInterfaceNew != nil {
|
||||
ch.Saml = samlInterfaceNew(New(ServerConnector(ch)))
|
||||
if err := ch.Saml.ConfigureSP(); err != nil {
|
||||
s.Log().Error("An error occurred while configuring SAML Service Provider", mlog.Err(err))
|
||||
}
|
||||
|
||||
ch.AddConfigListener(func(_, _ *model.Config) {
|
||||
if err := ch.Saml.ConfigureSP(); err != nil {
|
||||
s.Log().Error("An error occurred while configuring SAML Service Provider", mlog.Err(err))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var imgErr error
|
||||
decoderConcurrency := int(*ch.cfgSvc.Config().FileSettings.MaxImageDecoderConcurrency)
|
||||
if decoderConcurrency == -1 {
|
||||
decoderConcurrency = runtime.NumCPU()
|
||||
}
|
||||
ch.imgDecoder, imgErr = imaging.NewDecoder(imaging.DecoderOptions{
|
||||
ConcurrencyLevel: decoderConcurrency,
|
||||
})
|
||||
if imgErr != nil {
|
||||
return nil, errors.Wrap(imgErr, "failed to create image decoder")
|
||||
}
|
||||
ch.imgEncoder, imgErr = imaging.NewEncoder(imaging.EncoderOptions{
|
||||
ConcurrencyLevel: runtime.NumCPU(),
|
||||
})
|
||||
if imgErr != nil {
|
||||
return nil, errors.Wrap(imgErr, "failed to create image encoder")
|
||||
}
|
||||
|
||||
ch.routerSvc = newRouterService()
|
||||
services[product.RouterKey] = ch.routerSvc
|
||||
|
||||
// Setup routes.
|
||||
pluginsRoute := ch.srv.Router.PathPrefix("/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}").Subrouter()
|
||||
pluginsRoute.HandleFunc("", ch.ServePluginRequest)
|
||||
pluginsRoute.HandleFunc("/public/{public_file:.*}", ch.ServePluginPublicRequest)
|
||||
pluginsRoute.HandleFunc("/{anything:.*}", ch.ServePluginRequest)
|
||||
|
||||
services[product.ChannelKey] = &channelsWrapper{
|
||||
app: &App{ch: ch},
|
||||
}
|
||||
|
||||
services[product.PostKey] = &postServiceWrapper{
|
||||
app: &App{ch: ch},
|
||||
}
|
||||
|
||||
services[product.PermissionsKey] = &permissionsServiceWrapper{
|
||||
app: &App{ch: ch},
|
||||
}
|
||||
|
||||
services[product.TeamKey] = &teamServiceWrapper{
|
||||
app: &App{ch: ch},
|
||||
}
|
||||
|
||||
services[product.BotKey] = &botServiceWrapper{
|
||||
app: &App{ch: ch},
|
||||
}
|
||||
|
||||
services[product.HooksKey] = &hooksService{
|
||||
ch: ch,
|
||||
}
|
||||
|
||||
services[product.UserKey] = &App{ch: ch}
|
||||
|
||||
services[product.PreferencesKey] = &preferencesServiceWrapper{
|
||||
app: &App{ch: ch},
|
||||
}
|
||||
|
||||
services[product.CommandKey] = &App{ch: ch}
|
||||
|
||||
services[product.ThreadsKey] = &App{ch: ch}
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func (ch *Channels) Start() error {
|
||||
// Start plugins
|
||||
ctx := request.EmptyContext(ch.srv.Log())
|
||||
ch.initPlugins(ctx, *ch.cfgSvc.Config().PluginSettings.Directory, *ch.cfgSvc.Config().PluginSettings.ClientDirectory)
|
||||
|
||||
ch.AddConfigListener(func(prevCfg, cfg *model.Config) {
|
||||
// We compute the difference between configs
|
||||
// to ensure we don't re-init plugins unnecessarily.
|
||||
diffs, err := config.Diff(prevCfg, cfg)
|
||||
if err != nil {
|
||||
ch.srv.Log().Warn("Error in comparing configs", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
hasDiff := false
|
||||
// TODO: This could be a method on ConfigDiffs itself
|
||||
for _, diff := range diffs {
|
||||
if strings.HasPrefix(diff.Path, "PluginSettings.") {
|
||||
hasDiff = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Do only if some plugin related settings has changed.
|
||||
if hasDiff {
|
||||
if *cfg.PluginSettings.Enable {
|
||||
ch.initPlugins(ctx, *cfg.PluginSettings.Directory, *ch.cfgSvc.Config().PluginSettings.ClientDirectory)
|
||||
} else {
|
||||
ch.ShutDownPlugins()
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
// This needs to be done after initPlugins has completed,
|
||||
// because we want the full plugin processing to be complete before disabling it.
|
||||
ch.disableBoardsIfNeeded()
|
||||
ch.srv.AddClusterLeaderChangedListener(ch.disableBoardsIfNeeded)
|
||||
|
||||
// TODO: This should be moved to the platform service.
|
||||
if err := ch.srv.platform.EnsureAsymmetricSigningKey(); err != nil {
|
||||
return errors.Wrapf(err, "unable to ensure asymmetric signing key")
|
||||
}
|
||||
|
||||
if err := ch.ensurePostActionCookieSecret(); err != nil {
|
||||
return errors.Wrapf(err, "unable to ensure PostAction cookie secret")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ch *Channels) Stop() error {
|
||||
ch.ShutDownPlugins()
|
||||
|
||||
ch.dndTaskMut.Lock()
|
||||
if ch.dndTask != nil {
|
||||
ch.dndTask.Cancel()
|
||||
}
|
||||
ch.dndTaskMut.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ch *Channels) AddConfigListener(listener func(*model.Config, *model.Config)) string {
|
||||
return ch.cfgSvc.AddConfigListener(listener)
|
||||
}
|
||||
|
||||
func (ch *Channels) RemoveConfigListener(id string) {
|
||||
ch.cfgSvc.RemoveConfigListener(id)
|
||||
}
|
||||
|
||||
func (ch *Channels) License() *model.License {
|
||||
return ch.licenseSvc.GetLicense()
|
||||
}
|
||||
|
||||
func (ch *Channels) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError {
|
||||
return ch.licenseSvc.RequestTrialLicense(requesterID, users, termsAccepted,
|
||||
receiveEmailsAccepted)
|
||||
}
|
||||
|
||||
func (a *App) HooksManager() *product.HooksManager {
|
||||
return a.Srv().hooksManager
|
||||
}
|
||||
|
||||
// Ensure hooksService implements `product.HooksService`
|
||||
var _ product.HooksService = (*hooksService)(nil)
|
||||
|
||||
type hooksService struct {
|
||||
ch *Channels
|
||||
}
|
||||
|
||||
func (s *hooksService) RegisterHooks(productID string, hooks any) error {
|
||||
return s.ch.srv.hooksManager.AddProduct(productID, hooks)
|
||||
}
|
||||
|
||||
func (ch *Channels) RunMultiHook(hookRunnerFunc func(hooks plugin.Hooks) bool, hookId int) {
|
||||
if env := ch.GetPluginsEnvironment(); env != nil {
|
||||
env.RunMultiPluginHook(hookRunnerFunc, hookId)
|
||||
}
|
||||
|
||||
// run hook for the products
|
||||
ch.srv.hooksManager.RunMultiHook(hookRunnerFunc, hookId)
|
||||
}
|
||||
|
||||
func (ch *Channels) HooksForPluginOrProduct(id string) (plugin.Hooks, error) {
|
||||
var hooks plugin.Hooks
|
||||
if env := ch.GetPluginsEnvironment(); env != nil {
|
||||
// we intentionally ignore the error here, because the id can be a product id
|
||||
// we are going to check if we have the hooks or not
|
||||
hooks, _ = env.HooksForPlugin(id)
|
||||
if hooks != nil {
|
||||
return hooks, nil
|
||||
}
|
||||
}
|
||||
|
||||
hooks = ch.srv.hooksManager.HooksForProduct(id)
|
||||
if hooks != nil {
|
||||
return hooks, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("could not find hooks for id %s", id)
|
||||
}
|
||||
|
||||
func (ch *Channels) disableBoardsIfNeeded() {
|
||||
// Disable focalboard in product mode.
|
||||
if ch.srv.Config().FeatureFlags.BoardsProduct {
|
||||
// disablePlugin automatically checks if the plugin is running or not,
|
||||
// and if it isn't, it returns an error. Therefore we ignore those errors.
|
||||
// We don't want to check here again if the plugin is enabled or not.
|
||||
appErr := ch.disablePlugin(model.PluginIdFocalboard)
|
||||
if appErr != nil && appErr.Id != "app.plugin.not_installed.app_error" && appErr.Id != "app.plugin.disabled.app_error" {
|
||||
ch.srv.Log().Error("Error disabling plugin in product mode", mlog.Err(appErr))
|
||||
}
|
||||
}
|
||||
}
|
||||
263
app/cloud.go
263
app/cloud.go
|
|
@ -1,263 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/product"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
// Ensure cloud service wrapper implements `product.CloudService`
|
||||
var _ product.CloudService = (*cloudWrapper)(nil)
|
||||
|
||||
// cloudWrapper provides an implementation of `product.CloudService` for use by products.
|
||||
type cloudWrapper struct {
|
||||
cloud einterfaces.CloudInterface
|
||||
}
|
||||
|
||||
func (c *cloudWrapper) GetCloudLimits() (*model.ProductLimits, error) {
|
||||
if c.cloud != nil {
|
||||
return c.cloud.GetCloudLimits("")
|
||||
}
|
||||
|
||||
return &model.ProductLimits{}, nil
|
||||
}
|
||||
|
||||
func (a *App) getSysAdminsEmailRecipients() ([]*model.User, *model.AppError) {
|
||||
userOptions := &model.UserGetOptions{
|
||||
Page: 0,
|
||||
PerPage: 100,
|
||||
Role: model.SystemAdminRoleId,
|
||||
Inactive: false,
|
||||
}
|
||||
return a.GetUsersFromProfiles(userOptions)
|
||||
}
|
||||
|
||||
func getCurrentPlanName(a *App) (string, *model.AppError) {
|
||||
subscription, err := a.Cloud().GetSubscription("")
|
||||
if err != nil {
|
||||
return "", model.NewAppError("getCurrentPlanName", "app.cloud.get_subscription.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
if subscription == nil {
|
||||
return "", model.NewAppError("getCurrentPlanName", "app.cloud.get_subscription.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
products, err := a.Cloud().GetCloudProducts("", false)
|
||||
if err != nil {
|
||||
return "", model.NewAppError("getCurrentPlanName", "app.cloud.get_cloud_products.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
if products == nil {
|
||||
return "", model.NewAppError("getCurrentPlanName", "app.cloud.get_cloud_products.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
planName := getCurrentProduct(subscription.ProductID, products).Name
|
||||
return planName, nil
|
||||
}
|
||||
|
||||
func (a *App) SendPaymentFailedEmail(failedPayment *model.FailedPayment) *model.AppError {
|
||||
sysAdmins, err := a.getSysAdminsEmailRecipients()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
planName, err := getCurrentPlanName(a)
|
||||
if err != nil {
|
||||
return model.NewAppError("SendPaymentFailedEmail", "app.cloud.get_current_plan_name.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
for _, admin := range sysAdmins {
|
||||
_, err := a.Srv().EmailService.SendPaymentFailedEmail(admin.Email, admin.Locale, failedPayment, planName, *a.Config().ServiceSettings.SiteURL)
|
||||
if err != nil {
|
||||
a.Log().Error("Error sending payment failed email", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getCurrentProduct(subscriptionProductID string, products []*model.Product) *model.Product {
|
||||
for _, product := range products {
|
||||
if product.ID == subscriptionProductID {
|
||||
return product
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) SendDelinquencyEmail(emailToSend model.DelinquencyEmail) *model.AppError {
|
||||
sysAdmins, aErr := a.getSysAdminsEmailRecipients()
|
||||
if aErr != nil {
|
||||
return aErr
|
||||
}
|
||||
planName, aErr := getCurrentPlanName(a)
|
||||
if aErr != nil {
|
||||
return model.NewAppError("SendDelinquencyEmail", "app.cloud.get_current_plan_name.app_error", nil, aErr.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
subscription, err := a.Cloud().GetSubscription("")
|
||||
if err != nil {
|
||||
return model.NewAppError("SendDelinquencyEmail", "app.cloud.get_subscription.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
if subscription == nil {
|
||||
return model.NewAppError("SendDelinquencyEmail", "app.cloud.get_subscription.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if subscription.DelinquentSince == nil {
|
||||
return model.NewAppError("SendDelinquencyEmail", "app.cloud.get_subscription_delinquency_date.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
delinquentSince := time.Unix(*subscription.DelinquentSince, 0)
|
||||
|
||||
delinquencyDate := delinquentSince.Format("01/02/2006")
|
||||
for _, admin := range sysAdmins {
|
||||
switch emailToSend {
|
||||
case model.DelinquencyEmail7:
|
||||
err := a.Srv().EmailService.SendDelinquencyEmail7(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName)
|
||||
if err != nil {
|
||||
a.Log().Error("Error sending delinquency email 7", mlog.Err(err))
|
||||
}
|
||||
case model.DelinquencyEmail14:
|
||||
err := a.Srv().EmailService.SendDelinquencyEmail14(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName)
|
||||
if err != nil {
|
||||
a.Log().Error("Error sending delinquency email 14", mlog.Err(err))
|
||||
}
|
||||
case model.DelinquencyEmail30:
|
||||
err := a.Srv().EmailService.SendDelinquencyEmail30(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName)
|
||||
if err != nil {
|
||||
a.Log().Error("Error sending delinquency email 30", mlog.Err(err))
|
||||
}
|
||||
case model.DelinquencyEmail45:
|
||||
err := a.Srv().EmailService.SendDelinquencyEmail45(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName, delinquencyDate)
|
||||
if err != nil {
|
||||
a.Log().Error("Error sending delinquency email 45", mlog.Err(err))
|
||||
}
|
||||
case model.DelinquencyEmail60:
|
||||
err := a.Srv().EmailService.SendDelinquencyEmail60(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL)
|
||||
if err != nil {
|
||||
a.Log().Error("Error sending delinquency email 60", mlog.Err(err))
|
||||
}
|
||||
case model.DelinquencyEmail75:
|
||||
err := a.Srv().EmailService.SendDelinquencyEmail75(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName, delinquencyDate)
|
||||
if err != nil {
|
||||
a.Log().Error("Error sending delinquency email 75", mlog.Err(err))
|
||||
}
|
||||
case model.DelinquencyEmail90:
|
||||
err := a.Srv().EmailService.SendDelinquencyEmail90(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL)
|
||||
if err != nil {
|
||||
a.Log().Error("Error sending delinquency email 90", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) AdjustInProductLimits(limits *model.ProductLimits, subscription *model.Subscription) *model.AppError {
|
||||
if limits.Teams != nil && limits.Teams.Active != nil && *limits.Teams.Active > 0 {
|
||||
err := a.AdjustTeamsFromProductLimits(limits.Teams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getNextBillingDateString() string {
|
||||
now := time.Now()
|
||||
t := time.Date(now.Year(), now.Month()+1, 1, 0, 0, 0, 0, time.UTC)
|
||||
return fmt.Sprintf("%s %d, %d", t.Month(), t.Day(), t.Year())
|
||||
}
|
||||
|
||||
func (a *App) SendUpgradeConfirmationEmail(isYearly bool) *model.AppError {
|
||||
sysAdmins, e := a.getSysAdminsEmailRecipients()
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
if len(sysAdmins) == 0 {
|
||||
return model.NewAppError("app.SendCloudUpgradeConfirmationEmail", "app.user.send_emails.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
subscription, err := a.Cloud().GetSubscription("")
|
||||
if err != nil {
|
||||
return model.NewAppError("app.SendCloudUpgradeConfirmationEmail", "app.user.send_emails.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
billingDate := getNextBillingDateString()
|
||||
|
||||
// we want to at least have one email sent out to an admin
|
||||
countNotOks := 0
|
||||
|
||||
embeddedFiles := make(map[string]io.Reader)
|
||||
if isYearly {
|
||||
pdf, filename, pdfErr := a.Cloud().GetInvoicePDF("", subscription.LastInvoice.ID)
|
||||
if pdfErr != nil {
|
||||
a.Log().Error("Error retrieving the invoice for subscription id", mlog.String("subscription", subscription.ID), mlog.Err(pdfErr))
|
||||
} else {
|
||||
embeddedFiles = map[string]io.Reader{
|
||||
filename: bytes.NewReader(pdf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, admin := range sysAdmins {
|
||||
name := admin.FirstName
|
||||
if name == "" {
|
||||
name = admin.Username
|
||||
}
|
||||
|
||||
err := a.Srv().EmailService.SendCloudUpgradeConfirmationEmail(admin.Email, name, billingDate, admin.Locale, *a.Config().ServiceSettings.SiteURL, subscription.GetWorkSpaceNameFromDNS(), isYearly, embeddedFiles)
|
||||
if err != nil {
|
||||
a.Log().Error("Error sending trial ended email to", mlog.String("email", admin.Email), mlog.Err(err))
|
||||
countNotOks++
|
||||
}
|
||||
}
|
||||
|
||||
// if not even one admin got an email, we consider that this operation errored
|
||||
if countNotOks == len(sysAdmins) {
|
||||
return model.NewAppError("app.SendCloudUpgradeConfirmationEmail", "app.user.send_emails.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendNoCardPaymentFailedEmail
|
||||
func (a *App) SendNoCardPaymentFailedEmail() *model.AppError {
|
||||
sysAdmins, err := a.getSysAdminsEmailRecipients()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, admin := range sysAdmins {
|
||||
err := a.Srv().EmailService.SendNoCardPaymentFailedEmail(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL)
|
||||
if err != nil {
|
||||
a.Log().Error("Error sending payment failed email", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create/ Update a subscription history event
|
||||
func (a *App) SendSubscriptionHistoryEvent(userID string) (*model.SubscriptionHistory, error) {
|
||||
license := a.Srv().License()
|
||||
|
||||
// No need to create a Subscription History Event if the license isn't cloud
|
||||
if !license.IsCloud() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Get user count
|
||||
userCount, err := a.Srv().Store().User().Count(model.UserCountOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.Cloud().CreateOrUpdateSubscriptionHistoryEvent(userID, int(userCount))
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/plugin"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (s *Server) clusterInstallPluginHandler(msg *model.ClusterMessage) {
|
||||
var data model.PluginEventData
|
||||
if jsonErr := json.Unmarshal(msg.Data, &data); jsonErr != nil {
|
||||
mlog.Warn("Failed to decode from JSON", mlog.Err(jsonErr))
|
||||
}
|
||||
s.Channels().installPluginFromData(data)
|
||||
}
|
||||
|
||||
func (s *Server) clusterRemovePluginHandler(msg *model.ClusterMessage) {
|
||||
var data model.PluginEventData
|
||||
if jsonErr := json.Unmarshal(msg.Data, &data); jsonErr != nil {
|
||||
mlog.Warn("Failed to decode from JSON", mlog.Err(jsonErr))
|
||||
}
|
||||
s.Channels().removePluginFromData(data)
|
||||
}
|
||||
|
||||
func (s *Server) clusterPluginEventHandler(msg *model.ClusterMessage) {
|
||||
if msg.Props == nil {
|
||||
mlog.Warn("ClusterMessage.Props for plugin event should not be nil")
|
||||
return
|
||||
}
|
||||
pluginID := msg.Props["PluginID"]
|
||||
// if the plugin key is empty, the message might be coming from a product.
|
||||
if pluginID == "" {
|
||||
pluginID = msg.Props["ProductID"]
|
||||
}
|
||||
eventID := msg.Props["EventID"]
|
||||
if pluginID == "" || eventID == "" {
|
||||
mlog.Warn("Invalid ClusterMessage.Props values for plugin event",
|
||||
mlog.String("plugin_id", pluginID), mlog.String("event_id", eventID))
|
||||
return
|
||||
}
|
||||
|
||||
channels, ok := s.products["channels"].(*Channels)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
hooks, err := channels.HooksForPluginOrProduct(pluginID)
|
||||
if err != nil {
|
||||
mlog.Warn("Getting hooks for plugin failed", mlog.String("plugin_id", pluginID), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
hooks.OnPluginClusterEvent(&plugin.Context{}, model.PluginClusterEvent{
|
||||
Id: eventID,
|
||||
Data: msg.Data,
|
||||
})
|
||||
}
|
||||
|
||||
// registerClusterHandlers registers the cluster message handlers that are handled by the server.
|
||||
//
|
||||
// The cluster event handlers are spread across this function and NewLocalCacheLayer.
|
||||
// Be careful to not have duplicated handlers here and there.
|
||||
func (s *Server) registerClusterHandlers() {
|
||||
|
||||
s.platform.RegisterClusterMessageHandler(model.ClusterEventInstallPlugin, s.clusterInstallPluginHandler)
|
||||
s.platform.RegisterClusterMessageHandler(model.ClusterEventRemovePlugin, s.clusterRemovePluginHandler)
|
||||
s.platform.RegisterClusterMessageHandler(model.ClusterEventPluginEvent, s.clusterPluginEventHandler)
|
||||
|
||||
s.platform.RegisterClusterHandlers()
|
||||
}
|
||||
812
app/command.go
812
app/command.go
|
|
@ -1,812 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/request"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/i18n"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/store"
|
||||
)
|
||||
|
||||
const (
|
||||
CmdCustomStatusTrigger = "status"
|
||||
usernameSpecialChars = ".-_"
|
||||
maxTriggerLen = 512
|
||||
)
|
||||
|
||||
var atMentionRegexp = regexp.MustCompile(`\B@[[:alnum:]][[:alnum:]\.\-_:]*`)
|
||||
|
||||
type CommandProvider interface {
|
||||
GetTrigger() string
|
||||
GetCommand(a *App, T i18n.TranslateFunc) *model.Command
|
||||
DoCommand(a *App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse
|
||||
}
|
||||
|
||||
var commandProviders = make(map[string]CommandProvider)
|
||||
|
||||
func RegisterCommandProvider(newProvider CommandProvider) {
|
||||
commandProviders[newProvider.GetTrigger()] = newProvider
|
||||
}
|
||||
|
||||
func GetCommandProvider(name string) CommandProvider {
|
||||
provider, ok := commandProviders[name]
|
||||
if ok {
|
||||
return provider
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// @openTracingParams teamID, skipSlackParsing
|
||||
func (a *App) CreateCommandPost(c request.CTX, post *model.Post, teamID string, response *model.CommandResponse, skipSlackParsing bool) (*model.Post, *model.AppError) {
|
||||
if skipSlackParsing {
|
||||
post.Message = response.Text
|
||||
} else {
|
||||
post.Message = model.ParseSlackLinksToMarkdown(response.Text)
|
||||
}
|
||||
|
||||
post.CreateAt = model.GetMillis()
|
||||
|
||||
if strings.HasPrefix(post.Type, model.PostSystemMessagePrefix) {
|
||||
err := model.NewAppError("CreateCommandPost", "api.context.invalid_param.app_error", map[string]any{"Name": "post.type"}, "", http.StatusBadRequest)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.Attachments != nil {
|
||||
model.ParseSlackAttachment(post, response.Attachments)
|
||||
}
|
||||
|
||||
if response.ResponseType == model.CommandResponseTypeInChannel {
|
||||
return a.CreatePostMissingChannel(c, post, true, true)
|
||||
}
|
||||
|
||||
if (response.ResponseType == "" || response.ResponseType == model.CommandResponseTypeEphemeral) && (response.Text != "" || response.Attachments != nil) {
|
||||
a.SendEphemeralPost(c, post.UserId, post)
|
||||
}
|
||||
|
||||
return post, nil
|
||||
}
|
||||
|
||||
// @openTracingParams teamID
|
||||
// previous ListCommands now ListAutocompleteCommands
|
||||
func (a *App) ListAutocompleteCommands(teamID string, T i18n.TranslateFunc) ([]*model.Command, *model.AppError) {
|
||||
commands := make([]*model.Command, 0, 32)
|
||||
seen := make(map[string]bool)
|
||||
|
||||
// Disable custom status slash command if the feature or the setting is off
|
||||
if !*a.Config().TeamSettings.EnableCustomUserStatuses {
|
||||
seen[CmdCustomStatusTrigger] = true
|
||||
}
|
||||
|
||||
for _, cmd := range a.PluginCommandsForTeam(teamID) {
|
||||
if cmd.AutoComplete && !seen[cmd.Trigger] {
|
||||
seen[cmd.Trigger] = true
|
||||
commands = append(commands, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
if *a.Config().ServiceSettings.EnableCommands {
|
||||
teamCmds, err := a.Srv().Store().Command().GetByTeam(teamID)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("ListAutocompleteCommands", "app.command.listautocompletecommands.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
for _, cmd := range teamCmds {
|
||||
if cmd.AutoComplete && !seen[cmd.Trigger] {
|
||||
cmd.Sanitize()
|
||||
seen[cmd.Trigger] = true
|
||||
commands = append(commands, cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, value := range commandProviders {
|
||||
if cmd := value.GetCommand(a, T); cmd != nil {
|
||||
cpy := *cmd
|
||||
if cpy.AutoComplete && !seen[cpy.Trigger] {
|
||||
cpy.Sanitize()
|
||||
seen[cpy.Trigger] = true
|
||||
commands = append(commands, &cpy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
func (a *App) ListTeamCommands(teamID string) ([]*model.Command, *model.AppError) {
|
||||
if !*a.Config().ServiceSettings.EnableCommands {
|
||||
return nil, model.NewAppError("ListTeamCommands", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
teamCmds, err := a.Srv().Store().Command().GetByTeam(teamID)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("ListTeamCommands", "app.command.listteamcommands.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
return teamCmds, nil
|
||||
}
|
||||
|
||||
func (a *App) ListAllCommands(teamID string, T i18n.TranslateFunc) ([]*model.Command, *model.AppError) {
|
||||
commands := make([]*model.Command, 0, 32)
|
||||
seen := make(map[string]bool)
|
||||
for _, value := range commandProviders {
|
||||
if cmd := value.GetCommand(a, T); cmd != nil {
|
||||
cpy := *cmd
|
||||
if cpy.AutoComplete && !seen[cpy.Trigger] {
|
||||
cpy.Sanitize()
|
||||
seen[cpy.Trigger] = true
|
||||
commands = append(commands, &cpy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, cmd := range a.PluginCommandsForTeam(teamID) {
|
||||
if !seen[cmd.Trigger] {
|
||||
seen[cmd.Trigger] = true
|
||||
commands = append(commands, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
if *a.Config().ServiceSettings.EnableCommands {
|
||||
teamCmds, err := a.Srv().Store().Command().GetByTeam(teamID)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("ListAllCommands", "app.command.listallcommands.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
for _, cmd := range teamCmds {
|
||||
if !seen[cmd.Trigger] {
|
||||
cmd.Sanitize()
|
||||
seen[cmd.Trigger] = true
|
||||
commands = append(commands, cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
// @openTracingParams args
|
||||
func (a *App) ExecuteCommand(c request.CTX, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
||||
trigger := ""
|
||||
message := ""
|
||||
index := strings.IndexFunc(args.Command, unicode.IsSpace)
|
||||
if index != -1 {
|
||||
trigger = args.Command[:index]
|
||||
message = args.Command[index+1:]
|
||||
} else {
|
||||
trigger = args.Command
|
||||
}
|
||||
trigger = strings.ToLower(trigger)
|
||||
if !strings.HasPrefix(trigger, "/") {
|
||||
return nil, model.NewAppError("command", "api.command.execute_command.format.app_error", map[string]any{"Trigger": trigger}, "", http.StatusBadRequest)
|
||||
}
|
||||
trigger = strings.TrimPrefix(trigger, "/")
|
||||
|
||||
clientTriggerId, triggerId, appErr := model.GenerateTriggerId(args.UserId, a.AsymmetricSigningKey())
|
||||
if appErr != nil {
|
||||
c.Logger().Warn("error occurred in generating trigger Id for a user ", mlog.Err(appErr))
|
||||
}
|
||||
|
||||
args.TriggerId = triggerId
|
||||
|
||||
// Plugins can override built in and custom commands
|
||||
cmd, response, appErr := a.tryExecutePluginCommand(c, args)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
} else if cmd != nil && response != nil {
|
||||
response.TriggerId = clientTriggerId
|
||||
return a.HandleCommandResponse(c, cmd, args, response, true)
|
||||
}
|
||||
|
||||
// Custom commands can override built ins
|
||||
cmd, response, appErr = a.tryExecuteCustomCommand(c, args, trigger, message)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
} else if cmd != nil && response != nil {
|
||||
response.TriggerId = clientTriggerId
|
||||
return a.HandleCommandResponse(c, cmd, args, response, false)
|
||||
}
|
||||
|
||||
cmd, response = a.tryExecuteBuiltInCommand(c, args, trigger, message)
|
||||
if cmd != nil && response != nil {
|
||||
return a.HandleCommandResponse(c, cmd, args, response, true)
|
||||
}
|
||||
|
||||
if len(trigger) > maxTriggerLen {
|
||||
trigger = trigger[:maxTriggerLen]
|
||||
trigger += "..."
|
||||
}
|
||||
return nil, model.NewAppError("command", "api.command.execute_command.not_found.app_error", map[string]any{"Trigger": trigger}, "", http.StatusNotFound)
|
||||
}
|
||||
|
||||
// MentionsToTeamMembers returns all the @ mentions found in message that
|
||||
// belong to users in the specified team, linking them to their users
|
||||
func (a *App) MentionsToTeamMembers(c request.CTX, message, teamID string) model.UserMentionMap {
|
||||
type mentionMapItem struct {
|
||||
Name string
|
||||
Id string
|
||||
}
|
||||
|
||||
possibleMentions := possibleAtMentions(message)
|
||||
mentionChan := make(chan *mentionMapItem, len(possibleMentions))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, mention := range possibleMentions {
|
||||
wg.Add(1)
|
||||
go func(mention string) {
|
||||
defer wg.Done()
|
||||
user, nErr := a.Srv().Store().User().GetByUsername(mention)
|
||||
|
||||
var nfErr *store.ErrNotFound
|
||||
if nErr != nil && !errors.As(nErr, &nfErr) {
|
||||
c.Logger().Warn("Failed to retrieve user @"+mention, mlog.Err(nErr))
|
||||
return
|
||||
}
|
||||
|
||||
// If it's a http.StatusNotFound error, check for usernames in substrings
|
||||
// without trailing punctuation
|
||||
if nErr != nil {
|
||||
trimmed, ok := trimUsernameSpecialChar(mention)
|
||||
for ; ok; trimmed, ok = trimUsernameSpecialChar(trimmed) {
|
||||
userFromTrimmed, nErr := a.Srv().Store().User().GetByUsername(trimmed)
|
||||
if nErr != nil && !errors.As(nErr, &nfErr) {
|
||||
return
|
||||
}
|
||||
|
||||
if nErr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := a.GetTeamMember(teamID, userFromTrimmed.Id)
|
||||
if err != nil {
|
||||
// The user is not in the team, so we should ignore it
|
||||
return
|
||||
}
|
||||
|
||||
mentionChan <- &mentionMapItem{trimmed, userFromTrimmed.Id}
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_, err := a.GetTeamMember(teamID, user.Id)
|
||||
if err != nil {
|
||||
// The user is not in the team, so we should ignore it
|
||||
return
|
||||
}
|
||||
|
||||
mentionChan <- &mentionMapItem{mention, user.Id}
|
||||
}(mention)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(mentionChan)
|
||||
|
||||
atMentionMap := make(model.UserMentionMap)
|
||||
for mention := range mentionChan {
|
||||
atMentionMap[mention.Name] = mention.Id
|
||||
}
|
||||
|
||||
return atMentionMap
|
||||
}
|
||||
|
||||
// MentionsToPublicChannels returns all the mentions to public channels,
|
||||
// linking them to their channels
|
||||
func (a *App) MentionsToPublicChannels(c request.CTX, message, teamID string) model.ChannelMentionMap {
|
||||
type mentionMapItem struct {
|
||||
Name string
|
||||
Id string
|
||||
}
|
||||
|
||||
channelMentions := model.ChannelMentions(message)
|
||||
mentionChan := make(chan *mentionMapItem, len(channelMentions))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, channelName := range channelMentions {
|
||||
wg.Add(1)
|
||||
go func(channelName string) {
|
||||
defer wg.Done()
|
||||
channel, err := a.GetChannelByName(c, channelName, teamID, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !channel.IsOpen() {
|
||||
return
|
||||
}
|
||||
|
||||
mentionChan <- &mentionMapItem{channelName, channel.Id}
|
||||
}(channelName)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(mentionChan)
|
||||
|
||||
channelMentionMap := make(model.ChannelMentionMap)
|
||||
for mention := range mentionChan {
|
||||
channelMentionMap[mention.Name] = mention.Id
|
||||
}
|
||||
|
||||
return channelMentionMap
|
||||
}
|
||||
|
||||
// tryExecuteBuiltInCommand attempts to run a built in command based on the given arguments. If no such command can be
|
||||
// found, returns nil for all arguments.
|
||||
func (a *App) tryExecuteBuiltInCommand(c request.CTX, args *model.CommandArgs, trigger string, message string) (*model.Command, *model.CommandResponse) {
|
||||
provider := GetCommandProvider(trigger)
|
||||
if provider == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cmd := provider.GetCommand(a, args.T)
|
||||
if cmd == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return cmd, provider.DoCommand(a, c, args, message)
|
||||
}
|
||||
|
||||
// tryExecuteCustomCommand attempts to run a custom command based on the given arguments. If no such command can be
|
||||
// found, returns nil for all arguments.
|
||||
func (a *App) tryExecuteCustomCommand(c request.CTX, args *model.CommandArgs, trigger string, message string) (*model.Command, *model.CommandResponse, *model.AppError) {
|
||||
// Handle custom commands
|
||||
if !*a.Config().ServiceSettings.EnableCommands {
|
||||
return nil, nil, model.NewAppError("ExecuteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
chanChan := make(chan store.StoreResult, 1)
|
||||
go func() {
|
||||
channel, err := a.Srv().Store().Channel().Get(args.ChannelId, true)
|
||||
chanChan <- store.StoreResult{Data: channel, NErr: err}
|
||||
close(chanChan)
|
||||
}()
|
||||
|
||||
teamChan := make(chan store.StoreResult, 1)
|
||||
go func() {
|
||||
team, err := a.Srv().Store().Team().Get(args.TeamId)
|
||||
teamChan <- store.StoreResult{Data: team, NErr: err}
|
||||
close(teamChan)
|
||||
}()
|
||||
|
||||
userChan := make(chan store.StoreResult, 1)
|
||||
go func() {
|
||||
user, err := a.Srv().Store().User().Get(context.Background(), args.UserId)
|
||||
userChan <- store.StoreResult{Data: user, NErr: err}
|
||||
close(userChan)
|
||||
}()
|
||||
|
||||
teamCmds, err := a.Srv().Store().Command().GetByTeam(args.TeamId)
|
||||
if err != nil {
|
||||
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.command.tryexecutecustomcommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
tr := <-teamChan
|
||||
if tr.NErr != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(tr.NErr, &nfErr):
|
||||
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(tr.NErr)
|
||||
default:
|
||||
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(tr.NErr)
|
||||
}
|
||||
}
|
||||
team := tr.Data.(*model.Team)
|
||||
|
||||
ur := <-userChan
|
||||
if ur.NErr != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(ur.NErr, &nfErr):
|
||||
return nil, nil, model.NewAppError("tryExecuteCustomCommand", MissingAccountError, nil, "", http.StatusNotFound).Wrap(ur.NErr)
|
||||
default:
|
||||
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(ur.NErr)
|
||||
}
|
||||
}
|
||||
user := ur.Data.(*model.User)
|
||||
|
||||
cr := <-chanChan
|
||||
if cr.NErr != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(cr.NErr, &nfErr):
|
||||
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.channel.get.existing.app_error", nil, "", http.StatusNotFound).Wrap(cr.NErr)
|
||||
default:
|
||||
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.channel.get.find.app_error", nil, "", http.StatusInternalServerError).Wrap(cr.NErr)
|
||||
}
|
||||
}
|
||||
channel := cr.Data.(*model.Channel)
|
||||
|
||||
var cmd *model.Command
|
||||
|
||||
for _, teamCmd := range teamCmds {
|
||||
if trigger == teamCmd.Trigger {
|
||||
cmd = teamCmd
|
||||
}
|
||||
}
|
||||
|
||||
if cmd == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
c.Logger().Debug("Executing command", mlog.String("command", trigger), mlog.String("user_id", args.UserId))
|
||||
|
||||
p := url.Values{}
|
||||
p.Set("token", cmd.Token)
|
||||
|
||||
p.Set("team_id", cmd.TeamId)
|
||||
p.Set("team_domain", team.Name)
|
||||
|
||||
p.Set("channel_id", args.ChannelId)
|
||||
p.Set("channel_name", channel.Name)
|
||||
|
||||
p.Set("user_id", args.UserId)
|
||||
p.Set("user_name", user.Username)
|
||||
|
||||
p.Set("command", "/"+trigger)
|
||||
p.Set("text", message)
|
||||
|
||||
p.Set("trigger_id", args.TriggerId)
|
||||
|
||||
userMentionMap := a.MentionsToTeamMembers(c, message, team.Id)
|
||||
for key, values := range userMentionMap.ToURLValues() {
|
||||
p[key] = values
|
||||
}
|
||||
|
||||
channelMentionMap := a.MentionsToPublicChannels(c, message, team.Id)
|
||||
for key, values := range channelMentionMap.ToURLValues() {
|
||||
p[key] = values
|
||||
}
|
||||
|
||||
hook, appErr := a.CreateCommandWebhook(cmd.Id, args)
|
||||
if appErr != nil {
|
||||
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]any{"Trigger": trigger}, "", http.StatusInternalServerError).Wrap(appErr)
|
||||
}
|
||||
p.Set("response_url", args.SiteURL+"/hooks/commands/"+hook.Id)
|
||||
|
||||
return a.DoCommandRequest(cmd, p)
|
||||
}
|
||||
|
||||
func (a *App) DoCommandRequest(cmd *model.Command, p url.Values) (*model.Command, *model.CommandResponse, *model.AppError) {
|
||||
// Prepare the request
|
||||
var req *http.Request
|
||||
var err error
|
||||
if cmd.Method == model.CommandMethodGet {
|
||||
req, err = http.NewRequest(http.MethodGet, cmd.URL, nil)
|
||||
} else {
|
||||
req, err = http.NewRequest(http.MethodPost, cmd.URL, strings.NewReader(p.Encode()))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]any{"Trigger": cmd.Trigger}, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
if cmd.Method == model.CommandMethodGet {
|
||||
if req.URL.RawQuery != "" {
|
||||
req.URL.RawQuery += "&"
|
||||
}
|
||||
req.URL.RawQuery += p.Encode()
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Authorization", "Token "+cmd.Token)
|
||||
if cmd.Method == model.CommandMethodPost {
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
|
||||
// Send the request
|
||||
resp, err := a.HTTPService().MakeClient(false).Do(req)
|
||||
if err != nil {
|
||||
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]any{"Trigger": cmd.Trigger}, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Handle the response
|
||||
body := io.LimitReader(resp.Body, MaxIntegrationResponseSize)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
// Ignore the error below because the resulting string will just be the empty string if bodyBytes is nil
|
||||
bodyBytes, _ := io.ReadAll(body)
|
||||
|
||||
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed_resp.app_error", map[string]any{"Trigger": cmd.Trigger, "Status": resp.Status}, string(bodyBytes), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
response, err := model.CommandResponseFromHTTPBody(resp.Header.Get("Content-Type"), body)
|
||||
if err != nil {
|
||||
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]any{"Trigger": cmd.Trigger}, "", http.StatusInternalServerError).Wrap(err)
|
||||
} else if response == nil {
|
||||
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed_empty.app_error", map[string]any{"Trigger": cmd.Trigger}, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return cmd, response, nil
|
||||
}
|
||||
|
||||
func (a *App) HandleCommandResponse(c request.CTX, command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.CommandResponse, *model.AppError) {
|
||||
trigger := ""
|
||||
if args.Command != "" {
|
||||
parts := strings.Split(args.Command, " ")
|
||||
trigger = parts[0][1:]
|
||||
trigger = strings.ToLower(trigger)
|
||||
}
|
||||
|
||||
var lastError *model.AppError
|
||||
_, err := a.HandleCommandResponsePost(c, command, args, response, builtIn)
|
||||
|
||||
if err != nil {
|
||||
mlog.Debug("Error occurred in handling command response post", mlog.Err(err))
|
||||
lastError = err
|
||||
}
|
||||
|
||||
if response.ExtraResponses != nil {
|
||||
for _, resp := range response.ExtraResponses {
|
||||
_, err := a.HandleCommandResponsePost(c, command, args, resp, builtIn)
|
||||
|
||||
if err != nil {
|
||||
mlog.Debug("Error occurred in handling command response post", mlog.Err(err))
|
||||
lastError = err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if lastError != nil {
|
||||
return response, model.NewAppError("command", "api.command.execute_command.create_post_failed.app_error", map[string]any{"Trigger": trigger}, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (a *App) HandleCommandResponsePost(c request.CTX, command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.Post, *model.AppError) {
|
||||
post := &model.Post{}
|
||||
post.ChannelId = args.ChannelId
|
||||
post.RootId = args.RootId
|
||||
post.UserId = args.UserId
|
||||
post.Type = response.Type
|
||||
post.SetProps(response.Props)
|
||||
|
||||
if response.ChannelId != "" {
|
||||
_, err := a.GetChannelMember(c, response.ChannelId, args.UserId)
|
||||
if err != nil {
|
||||
err = model.NewAppError("HandleCommandResponsePost", "api.command.command_post.forbidden.app_error", nil, "", http.StatusForbidden).Wrap(err)
|
||||
return nil, err
|
||||
}
|
||||
post.ChannelId = response.ChannelId
|
||||
}
|
||||
|
||||
isBotPost := !builtIn
|
||||
|
||||
if *a.Config().ServiceSettings.EnablePostUsernameOverride {
|
||||
if command.Username != "" {
|
||||
post.AddProp("override_username", command.Username)
|
||||
isBotPost = true
|
||||
} else if response.Username != "" {
|
||||
post.AddProp("override_username", response.Username)
|
||||
isBotPost = true
|
||||
}
|
||||
}
|
||||
|
||||
if *a.Config().ServiceSettings.EnablePostIconOverride {
|
||||
if command.IconURL != "" {
|
||||
post.AddProp("override_icon_url", command.IconURL)
|
||||
isBotPost = true
|
||||
} else if response.IconURL != "" {
|
||||
post.AddProp("override_icon_url", response.IconURL)
|
||||
isBotPost = true
|
||||
} else {
|
||||
post.AddProp("override_icon_url", "")
|
||||
}
|
||||
}
|
||||
|
||||
if isBotPost {
|
||||
post.AddProp("from_webhook", "true")
|
||||
}
|
||||
|
||||
// Process Slack text replacements if the response does not contain "skip_slack_parsing": true.
|
||||
if !response.SkipSlackParsing {
|
||||
response.Text = a.ProcessSlackText(response.Text)
|
||||
response.Attachments = a.ProcessSlackAttachments(response.Attachments)
|
||||
}
|
||||
|
||||
if _, err := a.CreateCommandPost(c, post, args.TeamId, response, response.SkipSlackParsing); err != nil {
|
||||
return post, err
|
||||
}
|
||||
|
||||
return post, nil
|
||||
}
|
||||
|
||||
func (a *App) CreateCommand(cmd *model.Command) (*model.Command, *model.AppError) {
|
||||
if !*a.Config().ServiceSettings.EnableCommands {
|
||||
return nil, model.NewAppError("CreateCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
return a.createCommand(cmd)
|
||||
}
|
||||
|
||||
func (a *App) createCommand(cmd *model.Command) (*model.Command, *model.AppError) {
|
||||
cmd.Trigger = strings.ToLower(cmd.Trigger)
|
||||
|
||||
teamCmds, err := a.Srv().Store().Command().GetByTeam(cmd.TeamId)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("CreateCommand", "app.command.createcommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
for _, existingCommand := range teamCmds {
|
||||
if cmd.Trigger == existingCommand.Trigger {
|
||||
return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
for _, builtInProvider := range commandProviders {
|
||||
builtInCommand := builtInProvider.GetCommand(a, i18n.T)
|
||||
if builtInCommand != nil && cmd.Trigger == builtInCommand.Trigger {
|
||||
return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
command, nErr := a.Srv().Store().Command().Save(cmd)
|
||||
if nErr != nil {
|
||||
var appErr *model.AppError
|
||||
switch {
|
||||
case errors.As(nErr, &appErr):
|
||||
return nil, appErr
|
||||
default:
|
||||
return nil, model.NewAppError("CreateCommand", "app.command.createcommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
}
|
||||
|
||||
return command, nil
|
||||
}
|
||||
|
||||
func (a *App) GetCommand(commandID string) (*model.Command, *model.AppError) {
|
||||
if !*a.Config().ServiceSettings.EnableCommands {
|
||||
return nil, model.NewAppError("GetCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
command, err := a.Srv().Store().Command().Get(commandID)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return nil, model.NewAppError("SqlCommandStore.Get", "store.sql_command.get.missing.app_error", map[string]any{"command_id": commandID}, "", http.StatusNotFound).Wrap(err)
|
||||
default:
|
||||
return nil, model.NewAppError("GetCommand", "app.command.getcommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
return command, nil
|
||||
}
|
||||
|
||||
func (a *App) UpdateCommand(oldCmd, updatedCmd *model.Command) (*model.Command, *model.AppError) {
|
||||
if !*a.Config().ServiceSettings.EnableCommands {
|
||||
return nil, model.NewAppError("UpdateCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
updatedCmd.Trigger = strings.ToLower(updatedCmd.Trigger)
|
||||
updatedCmd.Id = oldCmd.Id
|
||||
updatedCmd.Token = oldCmd.Token
|
||||
updatedCmd.CreateAt = oldCmd.CreateAt
|
||||
updatedCmd.UpdateAt = model.GetMillis()
|
||||
updatedCmd.DeleteAt = oldCmd.DeleteAt
|
||||
updatedCmd.CreatorId = oldCmd.CreatorId
|
||||
updatedCmd.PluginId = oldCmd.PluginId
|
||||
updatedCmd.TeamId = oldCmd.TeamId
|
||||
|
||||
command, err := a.Srv().Store().Command().Update(updatedCmd)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
var appErr *model.AppError
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return nil, model.NewAppError("SqlCommandStore.Update", "store.sql_command.update.missing.app_error", map[string]any{"command_id": updatedCmd.Id}, "", http.StatusNotFound).Wrap(err)
|
||||
case errors.As(err, &appErr):
|
||||
return nil, appErr
|
||||
default:
|
||||
return nil, model.NewAppError("UpdateCommand", "app.command.updatecommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return command, nil
|
||||
}
|
||||
|
||||
func (a *App) MoveCommand(team *model.Team, command *model.Command) *model.AppError {
|
||||
command.TeamId = team.Id
|
||||
|
||||
_, err := a.Srv().Store().Command().Update(command)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
var appErr *model.AppError
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return model.NewAppError("SqlCommandStore.Update", "store.sql_command.update.missing.app_error", map[string]any{"command_id": command.Id}, "", http.StatusNotFound).Wrap(err)
|
||||
case errors.As(err, &appErr):
|
||||
return appErr
|
||||
default:
|
||||
return model.NewAppError("MoveCommand", "app.command.movecommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) RegenCommandToken(cmd *model.Command) (*model.Command, *model.AppError) {
|
||||
if !*a.Config().ServiceSettings.EnableCommands {
|
||||
return nil, model.NewAppError("RegenCommandToken", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
cmd.Token = model.NewId()
|
||||
|
||||
command, err := a.Srv().Store().Command().Update(cmd)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
var appErr *model.AppError
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return nil, model.NewAppError("SqlCommandStore.Update", "store.sql_command.update.missing.app_error", map[string]any{"command_id": cmd.Id}, "", http.StatusNotFound).Wrap(err)
|
||||
case errors.As(err, &appErr):
|
||||
return nil, appErr
|
||||
default:
|
||||
return nil, model.NewAppError("RegenCommandToken", "app.command.regencommandtoken.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return command, nil
|
||||
}
|
||||
|
||||
func (a *App) DeleteCommand(commandID string) *model.AppError {
|
||||
if !*a.Config().ServiceSettings.EnableCommands {
|
||||
return model.NewAppError("DeleteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
err := a.Srv().Store().Command().Delete(commandID, model.GetMillis())
|
||||
if err != nil {
|
||||
return model.NewAppError("DeleteCommand", "app.command.deletecommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// possibleAtMentions returns all substrings in message that look like valid @
|
||||
// mentions.
|
||||
func possibleAtMentions(message string) []string {
|
||||
var names []string
|
||||
|
||||
if !strings.Contains(message, "@") {
|
||||
return names
|
||||
}
|
||||
|
||||
alreadyMentioned := make(map[string]bool)
|
||||
for _, match := range atMentionRegexp.FindAllString(message, -1) {
|
||||
name := model.NormalizeUsername(match[1:])
|
||||
if !alreadyMentioned[name] && model.IsValidUsernameAllowRemote(name) {
|
||||
names = append(names, name)
|
||||
alreadyMentioned[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
// trimUsernameSpecialChar tries to remove the last character from word if it
|
||||
// is a special character for usernames (dot, dash or underscore). If not, it
|
||||
// returns the same string.
|
||||
func trimUsernameSpecialChar(word string) (string, bool) {
|
||||
len := len(word)
|
||||
|
||||
if len > 0 && strings.LastIndexAny(word, usernameSpecialChars) == (len-1) {
|
||||
return word[:len-1], true
|
||||
}
|
||||
|
||||
return word, false
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/store"
|
||||
)
|
||||
|
||||
func (a *App) GetComplianceReports(page, perPage int) (model.Compliances, *model.AppError) {
|
||||
if license := a.Srv().License(); !*a.Config().ComplianceSettings.Enable || license == nil || !*license.Features.Compliance {
|
||||
return nil, model.NewAppError("GetComplianceReports", "ent.compliance.licence_disable.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
compliances, err := a.Srv().Store().Compliance().GetAll(page*perPage, perPage)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetComplianceReports", "app.compliance.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
return compliances, nil
|
||||
}
|
||||
|
||||
func (a *App) SaveComplianceReport(job *model.Compliance) (*model.Compliance, *model.AppError) {
|
||||
if license := a.Srv().License(); !*a.Config().ComplianceSettings.Enable || license == nil || !*license.Features.Compliance || a.Compliance() == nil {
|
||||
return nil, model.NewAppError("saveComplianceReport", "ent.compliance.licence_disable.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
job.Type = model.ComplianceTypeAdhoc
|
||||
|
||||
job, err := a.Srv().Store().Compliance().Save(job)
|
||||
if err != nil {
|
||||
var appErr *model.AppError
|
||||
switch {
|
||||
case errors.As(err, &appErr):
|
||||
return nil, appErr
|
||||
default:
|
||||
return nil, model.NewAppError("SaveComplianceReport", "app.compliance.save.saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
jCopy := job.DeepCopy()
|
||||
a.Srv().Go(func() {
|
||||
err := a.Compliance().RunComplianceJob(jCopy)
|
||||
if err != nil {
|
||||
mlog.Warn("Error running compliance job", mlog.Err(err))
|
||||
}
|
||||
})
|
||||
|
||||
return job, nil
|
||||
}
|
||||
|
||||
func (a *App) GetComplianceReport(reportId string) (*model.Compliance, *model.AppError) {
|
||||
if license := a.Srv().License(); !*a.Config().ComplianceSettings.Enable || license == nil || !*license.Features.Compliance || a.Compliance() == nil {
|
||||
return nil, model.NewAppError("downloadComplianceReport", "ent.compliance.licence_disable.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
compliance, err := a.Srv().Store().Compliance().Get(reportId)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return nil, model.NewAppError("GetComplianceReport", "app.compliance.get.finding.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
||||
default:
|
||||
return nil, model.NewAppError("GetComplianceReport", "app.compliance.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return compliance, nil
|
||||
}
|
||||
|
||||
func (a *App) GetComplianceFile(job *model.Compliance) ([]byte, *model.AppError) {
|
||||
f, err := os.ReadFile(*a.Config().ComplianceSettings.Directory + "compliance/" + job.JobName() + ".zip")
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("readFile", "api.file.read_file.reading_local.app_error", nil, "", http.StatusNotImplemented).Wrap(err)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
261
app/config.go
261
app/config.go
|
|
@ -1,261 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mail"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrorTermsOfServiceNoRowsFound = "app.terms_of_service.get.no_rows.app_error"
|
||||
)
|
||||
|
||||
func (s *Server) Config() *model.Config {
|
||||
return s.platform.Config()
|
||||
}
|
||||
|
||||
func (a *App) Config() *model.Config {
|
||||
return a.ch.cfgSvc.Config()
|
||||
}
|
||||
|
||||
func (a *App) EnvironmentConfig(filter func(reflect.StructField) bool) map[string]any {
|
||||
return a.Srv().platform.GetEnvironmentOverridesWithFilter(filter)
|
||||
}
|
||||
|
||||
func (a *App) UpdateConfig(f func(*model.Config)) {
|
||||
a.Srv().platform.UpdateConfig(f)
|
||||
}
|
||||
|
||||
func (a *App) ReloadConfig() error {
|
||||
return a.Srv().platform.ReloadConfig()
|
||||
}
|
||||
|
||||
func (a *App) ClientConfig() map[string]string {
|
||||
return a.ch.srv.platform.ClientConfig()
|
||||
}
|
||||
|
||||
func (a *App) ClientConfigHash() string {
|
||||
return a.ch.ClientConfigHash()
|
||||
}
|
||||
|
||||
func (a *App) LimitedClientConfig() map[string]string {
|
||||
return a.ch.srv.platform.LimitedClientConfig()
|
||||
}
|
||||
|
||||
func (a *App) AddConfigListener(listener func(*model.Config, *model.Config)) string {
|
||||
return a.Srv().platform.AddConfigListener(listener)
|
||||
}
|
||||
|
||||
// Removes a listener function by the unique ID returned when AddConfigListener was called
|
||||
func (a *App) RemoveConfigListener(id string) {
|
||||
a.Srv().platform.RemoveConfigListener(id)
|
||||
}
|
||||
|
||||
// ensurePostActionCookieSecret ensures that the key for encrypting PostActionCookie exists
|
||||
// and future calls to PostActionCookieSecret will always return a valid key, same on all
|
||||
// servers in the cluster
|
||||
func (ch *Channels) ensurePostActionCookieSecret() error {
|
||||
if ch.postActionCookieSecret != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var secret *model.SystemPostActionCookieSecret
|
||||
|
||||
value, err := ch.srv.Store().System().GetByName(model.SystemPostActionCookieSecretKey)
|
||||
if err == nil {
|
||||
if err := json.Unmarshal([]byte(value.Value), &secret); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't already have a key, try to generate one.
|
||||
if secret == nil {
|
||||
newSecret := &model.SystemPostActionCookieSecret{
|
||||
Secret: make([]byte, 32),
|
||||
}
|
||||
_, err := rand.Reader.Read(newSecret.Secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
system := &model.System{
|
||||
Name: model.SystemPostActionCookieSecretKey,
|
||||
}
|
||||
v, err := json.Marshal(newSecret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
system.Value = string(v)
|
||||
// If we were able to save the key, use it, otherwise log the error.
|
||||
if err = ch.srv.Store().System().Save(system); err != nil {
|
||||
mlog.Warn("Failed to save PostActionCookieSecret", mlog.Err(err))
|
||||
} else {
|
||||
secret = newSecret
|
||||
}
|
||||
}
|
||||
|
||||
// If we weren't able to save a new key above, another server must have beat us to it. Get the
|
||||
// key from the database, and if that fails, error out.
|
||||
if secret == nil {
|
||||
value, err := ch.srv.Store().System().GetByName(model.SystemPostActionCookieSecretKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(value.Value), &secret); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ch.postActionCookieSecret = secret.Secret
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) ensureInstallationDate() error {
|
||||
_, appErr := s.platform.GetSystemInstallDate()
|
||||
if appErr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
installDate, nErr := s.Store().User().InferSystemInstallDate()
|
||||
var installationDate int64
|
||||
if nErr == nil && installDate > 0 {
|
||||
installationDate = installDate
|
||||
} else {
|
||||
installationDate = utils.MillisFromTime(time.Now())
|
||||
}
|
||||
|
||||
if err := s.Store().System().SaveOrUpdate(&model.System{
|
||||
Name: model.SystemInstallationDateKey,
|
||||
Value: strconv.FormatInt(installationDate, 10),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) ensureFirstServerRunTimestamp() error {
|
||||
_, appErr := s.getFirstServerRunTimestamp()
|
||||
if appErr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := s.Store().System().SaveOrUpdate(&model.System{
|
||||
Name: model.SystemFirstServerRunTimestampKey,
|
||||
Value: strconv.FormatInt(utils.MillisFromTime(time.Now()), 10),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AsymmetricSigningKey will return a private key that can be used for asymmetric signing.
|
||||
func (ch *Channels) AsymmetricSigningKey() *ecdsa.PrivateKey {
|
||||
return ch.srv.platform.AsymmetricSigningKey()
|
||||
}
|
||||
|
||||
func (a *App) AsymmetricSigningKey() *ecdsa.PrivateKey {
|
||||
return a.ch.AsymmetricSigningKey()
|
||||
}
|
||||
|
||||
func (ch *Channels) PostActionCookieSecret() []byte {
|
||||
return ch.postActionCookieSecret
|
||||
}
|
||||
|
||||
func (a *App) PostActionCookieSecret() []byte {
|
||||
return a.ch.PostActionCookieSecret()
|
||||
}
|
||||
|
||||
func (a *App) GetCookieDomain() string {
|
||||
if *a.Config().ServiceSettings.AllowCookiesForSubdomains {
|
||||
if siteURL, err := url.Parse(*a.Config().ServiceSettings.SiteURL); err == nil {
|
||||
return siteURL.Hostname()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *App) GetSiteURL() string {
|
||||
return *a.Config().ServiceSettings.SiteURL
|
||||
}
|
||||
|
||||
// GetConfigFile proxies access to the given configuration file to the underlying config store.
|
||||
func (a *App) GetConfigFile(name string) ([]byte, error) {
|
||||
data, err := a.Srv().platform.GetConfigFile(name)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get config file %s", name)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// GetSanitizedConfig gets the configuration for a system admin without any secrets.
|
||||
func (a *App) GetSanitizedConfig() *model.Config {
|
||||
cfg := a.Config().Clone()
|
||||
cfg.Sanitize()
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
// GetEnvironmentConfig returns a map of configuration keys whose values have been overridden by an environment variable.
|
||||
// If filter is not nil and returns false for a struct field, that field will be omitted.
|
||||
func (a *App) GetEnvironmentConfig(filter func(reflect.StructField) bool) map[string]any {
|
||||
return a.EnvironmentConfig(filter)
|
||||
}
|
||||
|
||||
// SaveConfig replaces the active configuration, optionally notifying cluster peers.
|
||||
func (a *App) SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bool) (*model.Config, *model.Config, *model.AppError) {
|
||||
return a.Srv().platform.SaveConfig(newCfg, sendConfigChangeClusterMessage)
|
||||
}
|
||||
|
||||
func (a *App) HandleMessageExportConfig(cfg *model.Config, appCfg *model.Config) {
|
||||
// If the Message Export feature has been toggled in the System Console, rewrite the ExportFromTimestamp field to an
|
||||
// appropriate value. The rewriting occurs here to ensure it doesn't affect values written to the config file
|
||||
// directly and not through the System Console UI.
|
||||
if *cfg.MessageExportSettings.EnableExport != *appCfg.MessageExportSettings.EnableExport {
|
||||
if *cfg.MessageExportSettings.EnableExport && *cfg.MessageExportSettings.ExportFromTimestamp == int64(0) {
|
||||
// When the feature is toggled on, use the current timestamp as the start time for future exports.
|
||||
cfg.MessageExportSettings.ExportFromTimestamp = model.NewInt64(model.GetMillis())
|
||||
} else if !*cfg.MessageExportSettings.EnableExport {
|
||||
// When the feature is disabled, reset the timestamp so that the timestamp will be set if
|
||||
// the feature is re-enabled from the System Console in future.
|
||||
cfg.MessageExportSettings.ExportFromTimestamp = model.NewInt64(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) MailServiceConfig() *mail.SMTPConfig {
|
||||
emailSettings := s.platform.Config().EmailSettings
|
||||
hostname := utils.GetHostnameFromSiteURL(*s.platform.Config().ServiceSettings.SiteURL)
|
||||
cfg := mail.SMTPConfig{
|
||||
Hostname: hostname,
|
||||
ConnectionSecurity: *emailSettings.ConnectionSecurity,
|
||||
SkipServerCertificateVerification: *emailSettings.SkipServerCertificateVerification,
|
||||
ServerName: *emailSettings.SMTPServer,
|
||||
Server: *emailSettings.SMTPServer,
|
||||
Port: *emailSettings.SMTPPort,
|
||||
ServerTimeout: *emailSettings.SMTPServerTimeout,
|
||||
Username: *emailSettings.SMTPUsername,
|
||||
Password: *emailSettings.SMTPPassword,
|
||||
EnableSMTPAuth: *emailSettings.EnableSMTPAuth,
|
||||
SendEmailNotifications: *emailSettings.SendEmailNotifications,
|
||||
FeedbackName: *emailSettings.FeedbackName,
|
||||
FeedbackEmail: *emailSettings.FeedbackEmail,
|
||||
ReplyToAddress: *emailSettings.ReplyToAddress,
|
||||
}
|
||||
return &cfg
|
||||
}
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/store/storetest/mocks"
|
||||
"github.com/mattermost/mattermost-server/v6/utils"
|
||||
)
|
||||
|
||||
func TestAsymmetricSigningKey(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
assert.NotNil(t, th.App.AsymmetricSigningKey())
|
||||
assert.NotEmpty(t, th.App.ClientConfig()["AsymmetricSigningPublicKey"])
|
||||
}
|
||||
|
||||
func TestPostActionCookieSecret(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
assert.Equal(t, 32, len(th.App.PostActionCookieSecret()))
|
||||
}
|
||||
|
||||
func TestClientConfigWithComputed(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
|
||||
mockStore := th.App.Srv().Store().(*mocks.Store)
|
||||
mockUserStore := mocks.UserStore{}
|
||||
mockUserStore.On("Count", mock.Anything).Return(int64(10), nil)
|
||||
mockPostStore := mocks.PostStore{}
|
||||
mockPostStore.On("GetMaxPostSize").Return(65535, nil)
|
||||
mockSystemStore := mocks.SystemStore{}
|
||||
mockSystemStore.On("GetByName", "UpgradedFromTE").Return(&model.System{Name: "UpgradedFromTE", Value: "false"}, nil)
|
||||
mockSystemStore.On("GetByName", "InstallationDate").Return(&model.System{Name: "InstallationDate", Value: "10"}, nil)
|
||||
mockStore.On("User").Return(&mockUserStore)
|
||||
mockStore.On("Post").Return(&mockPostStore)
|
||||
mockStore.On("System").Return(&mockSystemStore)
|
||||
mockStore.On("GetDBSchemaVersion").Return(1, nil)
|
||||
|
||||
config := th.App.Srv().Platform().ClientConfigWithComputed()
|
||||
_, ok := config["NoAccounts"]
|
||||
assert.True(t, ok, "expected NoAccounts in returned config")
|
||||
_, ok = config["MaxPostSize"]
|
||||
assert.True(t, ok, "expected MaxPostSize in returned config")
|
||||
v, ok := config["SchemaVersion"]
|
||||
assert.True(t, ok, "expected SchemaVersion in returned config")
|
||||
assert.Equal(t, "1", v)
|
||||
}
|
||||
|
||||
func TestEnsureInstallationDate(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
tt := []struct {
|
||||
Name string
|
||||
PrevInstallationDate *int64
|
||||
UsersCreationDates []int64
|
||||
ExpectedInstallationDate *int64
|
||||
}{
|
||||
{
|
||||
Name: "New installation: no users, no installation date",
|
||||
PrevInstallationDate: nil,
|
||||
UsersCreationDates: nil,
|
||||
ExpectedInstallationDate: model.NewInt64(utils.MillisFromTime(time.Now())),
|
||||
},
|
||||
{
|
||||
Name: "Old installation: users, no installation date",
|
||||
PrevInstallationDate: nil,
|
||||
UsersCreationDates: []int64{10000000000, 30000000000, 20000000000},
|
||||
ExpectedInstallationDate: model.NewInt64(10000000000),
|
||||
},
|
||||
{
|
||||
Name: "New installation, second run: no users, installation date",
|
||||
PrevInstallationDate: model.NewInt64(80000000000),
|
||||
UsersCreationDates: []int64{10000000000, 30000000000, 20000000000},
|
||||
ExpectedInstallationDate: model.NewInt64(80000000000),
|
||||
},
|
||||
{
|
||||
Name: "Old installation already updated: users, installation date",
|
||||
PrevInstallationDate: model.NewInt64(90000000000),
|
||||
UsersCreationDates: []int64{10000000000, 30000000000, 20000000000},
|
||||
ExpectedInstallationDate: model.NewInt64(90000000000),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
sqlStore := th.GetSqlStore()
|
||||
sqlStore.GetMasterX().Exec("DELETE FROM Users")
|
||||
|
||||
for _, createAt := range tc.UsersCreationDates {
|
||||
user := th.CreateUser()
|
||||
user.CreateAt = createAt
|
||||
sqlStore.GetMasterX().Exec("UPDATE Users SET CreateAt = ? WHERE Id = ?", createAt, user.Id)
|
||||
}
|
||||
|
||||
if tc.PrevInstallationDate == nil {
|
||||
th.App.Srv().Store().System().PermanentDeleteByName(model.SystemInstallationDateKey)
|
||||
} else {
|
||||
th.App.Srv().Store().System().SaveOrUpdate(&model.System{
|
||||
Name: model.SystemInstallationDateKey,
|
||||
Value: strconv.FormatInt(*tc.PrevInstallationDate, 10),
|
||||
})
|
||||
}
|
||||
|
||||
err := th.App.Srv().ensureInstallationDate()
|
||||
|
||||
if tc.ExpectedInstallationDate == nil {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
|
||||
data, err := th.App.Srv().Store().System().GetByName(model.SystemInstallationDateKey)
|
||||
assert.NoError(t, err)
|
||||
value, _ := strconv.ParseInt(data.Value, 10, 64)
|
||||
assert.True(t, *tc.ExpectedInstallationDate <= value && *tc.ExpectedInstallationDate+1000 >= value)
|
||||
}
|
||||
|
||||
sqlStore.GetMasterX().Exec("DELETE FROM Users")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/request"
|
||||
"github.com/mattermost/mattermost-server/v6/plugin"
|
||||
"github.com/mattermost/mattermost-server/v6/store/sqlstore"
|
||||
)
|
||||
|
||||
// WithMaster adds the context value that master DB should be selected for this request.
|
||||
func WithMaster(ctx context.Context) context.Context {
|
||||
return sqlstore.WithMaster(ctx)
|
||||
}
|
||||
|
||||
func pluginContext(c request.CTX) *plugin.Context {
|
||||
context := &plugin.Context{
|
||||
RequestId: c.RequestId(),
|
||||
SessionId: c.Session().Id,
|
||||
IPAddress: c.IPAddress(),
|
||||
AcceptLanguage: c.AcceptLanguage(),
|
||||
UserAgent: c.UserAgent(),
|
||||
}
|
||||
return context
|
||||
}
|
||||
1322
app/email/email.go
1322
app/email/email.go
File diff suppressed because it is too large
Load diff
|
|
@ -1,426 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mail"
|
||||
)
|
||||
|
||||
func TestCondenseSiteURL(t *testing.T) {
|
||||
require.Equal(t, "", condenseSiteURL(""))
|
||||
require.Equal(t, "mattermost.com", condenseSiteURL("mattermost.com"))
|
||||
require.Equal(t, "mattermost.com", condenseSiteURL("mattermost.com/"))
|
||||
require.Equal(t, "chat.mattermost.com", condenseSiteURL("chat.mattermost.com"))
|
||||
require.Equal(t, "chat.mattermost.com", condenseSiteURL("chat.mattermost.com/"))
|
||||
require.Equal(t, "mattermost.com/subpath", condenseSiteURL("mattermost.com/subpath"))
|
||||
require.Equal(t, "mattermost.com/subpath", condenseSiteURL("mattermost.com/subpath/"))
|
||||
require.Equal(t, "chat.mattermost.com/subpath", condenseSiteURL("chat.mattermost.com/subpath"))
|
||||
require.Equal(t, "chat.mattermost.com/subpath", condenseSiteURL("chat.mattermost.com/subpath/"))
|
||||
|
||||
require.Equal(t, "mattermost.com:8080", condenseSiteURL("http://mattermost.com:8080"))
|
||||
require.Equal(t, "mattermost.com:8080", condenseSiteURL("http://mattermost.com:8080/"))
|
||||
require.Equal(t, "chat.mattermost.com:8080", condenseSiteURL("http://chat.mattermost.com:8080"))
|
||||
require.Equal(t, "chat.mattermost.com:8080", condenseSiteURL("http://chat.mattermost.com:8080/"))
|
||||
require.Equal(t, "mattermost.com:8080/subpath", condenseSiteURL("http://mattermost.com:8080/subpath"))
|
||||
require.Equal(t, "mattermost.com:8080/subpath", condenseSiteURL("http://mattermost.com:8080/subpath/"))
|
||||
require.Equal(t, "chat.mattermost.com:8080/subpath", condenseSiteURL("http://chat.mattermost.com:8080/subpath"))
|
||||
require.Equal(t, "chat.mattermost.com:8080/subpath", condenseSiteURL("http://chat.mattermost.com:8080/subpath/"))
|
||||
}
|
||||
|
||||
func TestSendInviteEmails(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
th.ConfigureInbucketMail()
|
||||
|
||||
emailTo := "test@example.com"
|
||||
|
||||
retrieveEmail := func(t *testing.T) mail.JSONMessageInbucket {
|
||||
t.Helper()
|
||||
var resultsMailbox mail.JSONMessageHeaderInbucket
|
||||
err2 := mail.RetryInbucket(5, func() error {
|
||||
var err error
|
||||
resultsMailbox, err = mail.GetMailBox(emailTo)
|
||||
return err
|
||||
})
|
||||
if err2 != nil {
|
||||
t.Skipf("No email was received, maybe due load on the server: %v", err2)
|
||||
}
|
||||
require.Len(t, resultsMailbox, 1)
|
||||
require.Contains(t, resultsMailbox[0].To[0], emailTo, "Wrong To: recipient")
|
||||
resultsEmail, err := mail.GetMessageFromMailbox(emailTo, resultsMailbox[0].ID)
|
||||
require.NoError(t, err, "Could not get message from mailbox")
|
||||
return resultsEmail
|
||||
}
|
||||
|
||||
verifyMailbox := func(t *testing.T) {
|
||||
t.Helper()
|
||||
email := retrieveEmail(t)
|
||||
require.Contains(t, email.Body.HTML, "http://testserver", "Wrong received message %s", email.Body.Text)
|
||||
require.Contains(t, email.Body.HTML, "test-user", "Wrong received message %s", email.Body.Text)
|
||||
require.Contains(t, email.Body.Text, "http://testserver", "Wrong received message %s", email.Body.Text)
|
||||
require.Contains(t, email.Body.Text, "test-user", "Wrong received message %s", email.Body.Text)
|
||||
}
|
||||
|
||||
th.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.EnableEmailInvitations = true
|
||||
*cfg.EmailSettings.SendEmailNotifications = false
|
||||
})
|
||||
t.Run("SendInviteEmails", func(t *testing.T) {
|
||||
mail.DeleteMailBox(emailTo)
|
||||
|
||||
err := th.service.SendInviteEmails(th.BasicTeam, "test-user", th.BasicUser.Id, []string{emailTo}, "http://testserver", nil, false, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
verifyMailbox(t)
|
||||
})
|
||||
|
||||
t.Run("SendInviteEmails can return error when SMTP connection fails", func(t *testing.T) {
|
||||
originalPort := *th.service.config().EmailSettings.SMTPPort
|
||||
th.UpdateConfig(func(cfg *model.Config) {
|
||||
os.Setenv("MM_EMAILSETTINGS_SMTPPORT", "5432")
|
||||
*cfg.EmailSettings.SMTPPort = "5432"
|
||||
})
|
||||
defer th.UpdateConfig(func(cfg *model.Config) {
|
||||
os.Setenv("MM_EMAILSETTINGS_SMTPPORT", originalPort)
|
||||
*cfg.EmailSettings.SMTPPort = originalPort
|
||||
})
|
||||
|
||||
err := th.service.SendInviteEmails(th.BasicTeam, "test-user", th.BasicUser.Id, []string{emailTo}, "http://testserver", nil, true, false, false)
|
||||
require.Error(t, err)
|
||||
|
||||
err = th.service.SendInviteEmails(th.BasicTeam, "test-user", th.BasicUser.Id, []string{emailTo}, "http://testserver", nil, false, false, false)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("SendGuestInviteEmails", func(t *testing.T) {
|
||||
mail.DeleteMailBox(emailTo)
|
||||
|
||||
err := th.service.SendGuestInviteEmails(
|
||||
th.BasicTeam,
|
||||
[]*model.Channel{th.BasicChannel},
|
||||
"test-user",
|
||||
th.BasicUser.Id,
|
||||
nil,
|
||||
[]string{emailTo},
|
||||
"http://testserver",
|
||||
"hello world",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
verifyMailbox(t)
|
||||
})
|
||||
|
||||
t.Run("SendGuestInviteEmail can return error when SMTP connection fails", func(t *testing.T) {
|
||||
originalPort := *th.service.config().EmailSettings.SMTPPort
|
||||
th.UpdateConfig(func(cfg *model.Config) {
|
||||
os.Setenv("MM_EMAILSETTINGS_SMTPPORT", "5432")
|
||||
*cfg.EmailSettings.SMTPPort = "5432"
|
||||
})
|
||||
defer th.UpdateConfig(func(cfg *model.Config) {
|
||||
os.Setenv("MM_EMAILSETTINGS_SMTPPORT", originalPort)
|
||||
*cfg.EmailSettings.SMTPPort = originalPort
|
||||
})
|
||||
|
||||
err := th.service.SendGuestInviteEmails(
|
||||
th.BasicTeam,
|
||||
[]*model.Channel{th.BasicChannel},
|
||||
"test-user",
|
||||
th.BasicUser.Id,
|
||||
nil,
|
||||
[]string{emailTo},
|
||||
"http://testserver",
|
||||
"hello world",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = th.service.SendGuestInviteEmails(
|
||||
th.BasicTeam,
|
||||
[]*model.Channel{th.BasicChannel},
|
||||
"test-user",
|
||||
th.BasicUser.Id,
|
||||
nil,
|
||||
[]string{emailTo},
|
||||
"http://testserver",
|
||||
"hello world",
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
require.Error(t, err)
|
||||
|
||||
})
|
||||
|
||||
t.Run("SendGuestInviteEmails should sanitize HTML input", func(t *testing.T) {
|
||||
mail.DeleteMailBox(emailTo)
|
||||
|
||||
message := `<a href="http://testserver">sanitized message</a>`
|
||||
err := th.service.SendGuestInviteEmails(
|
||||
th.BasicTeam,
|
||||
[]*model.Channel{th.BasicChannel},
|
||||
"test-user",
|
||||
th.BasicUser.Id,
|
||||
nil,
|
||||
[]string{emailTo},
|
||||
"http://testserver",
|
||||
message,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
email := retrieveEmail(t)
|
||||
require.NotContains(t, email.Body.HTML, message)
|
||||
require.Contains(t, email.Body.HTML, "sanitized message")
|
||||
require.Contains(t, email.Body.Text, "sanitized message")
|
||||
})
|
||||
|
||||
t.Run("SendInviteEmails should contain button URL with 'started by role' param for system user", func(t *testing.T) {
|
||||
mail.DeleteMailBox(emailTo)
|
||||
|
||||
err := th.service.SendInviteEmails(
|
||||
th.BasicTeam,
|
||||
"test-user",
|
||||
th.BasicUser.Id,
|
||||
[]string{emailTo},
|
||||
"http://testserver",
|
||||
nil,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
email := retrieveEmail(t)
|
||||
require.Contains(t, email.Body.HTML, "&sbr=su")
|
||||
})
|
||||
|
||||
t.Run("SendInviteEmails should contain button URL with 'started by role' param for system admin", func(t *testing.T) {
|
||||
mail.DeleteMailBox(emailTo)
|
||||
|
||||
err := th.service.SendInviteEmails(
|
||||
th.BasicTeam,
|
||||
"test-user",
|
||||
th.BasicUser.Id,
|
||||
[]string{emailTo},
|
||||
"http://testserver",
|
||||
nil,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
email := retrieveEmail(t)
|
||||
require.Contains(t, email.Body.HTML, "&sbr=sa")
|
||||
})
|
||||
|
||||
t.Run("SendInviteEmails should contain button URL with 'started by role' param for first system admin", func(t *testing.T) {
|
||||
mail.DeleteMailBox(emailTo)
|
||||
|
||||
err := th.service.SendInviteEmails(
|
||||
th.BasicTeam,
|
||||
"test-user",
|
||||
th.BasicUser.Id,
|
||||
[]string{emailTo},
|
||||
"http://testserver",
|
||||
nil,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
email := retrieveEmail(t)
|
||||
require.Contains(t, email.Body.HTML, "&sbr=fa")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSendCloudUpgradedEmail(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
th.ConfigureInbucketMail()
|
||||
|
||||
emailTo := "testclouduser@example.com"
|
||||
emailToUsername := strings.Split(emailTo, "@")[0]
|
||||
|
||||
t.Run("SendCloudMonthlyUpgradedEmail", func(t *testing.T) {
|
||||
verifyMailbox := func(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
var resultsMailbox mail.JSONMessageHeaderInbucket
|
||||
err2 := mail.RetryInbucket(5, func() error {
|
||||
var err error
|
||||
resultsMailbox, err = mail.GetMailBox(emailTo)
|
||||
return err
|
||||
})
|
||||
if err2 != nil {
|
||||
t.Skipf("No email was received, maybe due load on the server: %v", err2)
|
||||
}
|
||||
|
||||
require.Len(t, resultsMailbox, 1)
|
||||
require.Contains(t, resultsMailbox[0].To[0], emailTo, "Wrong To: recipient")
|
||||
resultsEmail, err := mail.GetMessageFromMailbox(emailTo, resultsMailbox[0].ID)
|
||||
require.NoError(t, err, "Could not get message from mailbox")
|
||||
require.Contains(t, resultsEmail.Body.Text, "You are now upgraded!", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Contains(t, resultsEmail.Body.Text, "SomeName workspace has now been upgraded", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Contains(t, resultsEmail.Body.Text, "You'll be billed from", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Contains(t, resultsEmail.Body.Text, "Open Mattermost", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Len(t, resultsEmail.Attachments, 0)
|
||||
}
|
||||
mail.DeleteMailBox(emailTo)
|
||||
|
||||
// Send Update to Monthly Plan email
|
||||
err := th.service.SendCloudUpgradeConfirmationEmail(emailTo, emailToUsername, "June 23, 2200", th.BasicUser.Locale, "https://example.com", "SomeName", false, make(map[string]io.Reader))
|
||||
require.NoError(t, err)
|
||||
|
||||
verifyMailbox(t)
|
||||
})
|
||||
|
||||
t.Run("SendCloudYearlyUpgradedEmail", func(t *testing.T) {
|
||||
verifyMailbox := func(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
var resultsMailbox mail.JSONMessageHeaderInbucket
|
||||
err2 := mail.RetryInbucket(5, func() error {
|
||||
var err error
|
||||
resultsMailbox, err = mail.GetMailBox(emailTo)
|
||||
return err
|
||||
})
|
||||
if err2 != nil {
|
||||
t.Skipf("No email was received, maybe due load on the server: %v", err2)
|
||||
}
|
||||
|
||||
require.Len(t, resultsMailbox, 1)
|
||||
require.Contains(t, resultsMailbox[0].To[0], emailTo, "Wrong To: recipient")
|
||||
resultsEmail, err := mail.GetMessageFromMailbox(emailTo, resultsMailbox[0].ID)
|
||||
require.NoError(t, err, "Could not get message from mailbox")
|
||||
require.Contains(t, resultsEmail.Body.Text, "You are now upgraded!", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Contains(t, resultsEmail.Body.Text, "SomeName workspace has now been upgraded", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Contains(t, resultsEmail.Body.Text, "View your invoice", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Len(t, resultsEmail.Attachments, 1)
|
||||
}
|
||||
mail.DeleteMailBox(emailTo)
|
||||
|
||||
// Send Update to Monthly Plan email
|
||||
var embeddedFiles = map[string]io.Reader{
|
||||
"filename": bytes.NewReader([]byte("Test")),
|
||||
}
|
||||
err := th.service.SendCloudUpgradeConfirmationEmail(emailTo, emailToUsername, "June 23, 2200", th.BasicUser.Locale, "https://example.com", "SomeName", true, embeddedFiles)
|
||||
require.NoError(t, err)
|
||||
|
||||
verifyMailbox(t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSendCloudWelcomeEmail(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
th.ConfigureInbucketMail()
|
||||
|
||||
emailTo := "testclouduser@example.com"
|
||||
|
||||
t.Run("TestSendCloudWelcomeEmail", func(t *testing.T) {
|
||||
verifyMailbox := func(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
var resultsMailbox mail.JSONMessageHeaderInbucket
|
||||
err2 := mail.RetryInbucket(5, func() error {
|
||||
var err error
|
||||
resultsMailbox, err = mail.GetMailBox(emailTo)
|
||||
return err
|
||||
})
|
||||
if err2 != nil {
|
||||
t.Skipf("No email was received, maybe due load on the server: %v", err2)
|
||||
}
|
||||
|
||||
require.Len(t, resultsMailbox, 1)
|
||||
require.Contains(t, resultsMailbox[0].To[0], emailTo, "Wrong To: recipient")
|
||||
resultsEmail, err := mail.GetMessageFromMailbox(emailTo, resultsMailbox[0].ID)
|
||||
require.NoError(t, err, "Could not get message from mailbox")
|
||||
require.Contains(t, resultsEmail.Subject, "Congratulations!", "Wrong subject message %s", resultsEmail.Subject)
|
||||
require.Contains(t, resultsEmail.Body.Text, "Your workspace is ready to go!", "Wrong body %s", resultsEmail.Body.Text)
|
||||
|
||||
}
|
||||
mail.DeleteMailBox(emailTo)
|
||||
|
||||
err := th.service.SendCloudWelcomeEmail(emailTo, th.BasicUser.Locale, "inviteID", "SomeName", "example.com", "https://example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
verifyMailbox(t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMailServiceConfig(t *testing.T) {
|
||||
configuredReplyTo := "feedbackexample@test.com"
|
||||
customReplyTo := "customreplyto@test.com"
|
||||
|
||||
emailService := Service{
|
||||
config: func() *model.Config {
|
||||
return &model.Config{
|
||||
ServiceSettings: model.ServiceSettings{
|
||||
SiteURL: model.NewString(""),
|
||||
},
|
||||
EmailSettings: model.EmailSettings{
|
||||
EnableSignUpWithEmail: new(bool),
|
||||
EnableSignInWithEmail: new(bool),
|
||||
EnableSignInWithUsername: new(bool),
|
||||
SendEmailNotifications: new(bool),
|
||||
UseChannelInEmailNotifications: new(bool),
|
||||
RequireEmailVerification: new(bool),
|
||||
FeedbackName: new(string),
|
||||
FeedbackEmail: new(string),
|
||||
ReplyToAddress: model.NewString(configuredReplyTo),
|
||||
FeedbackOrganization: new(string),
|
||||
EnableSMTPAuth: new(bool),
|
||||
SMTPUsername: new(string),
|
||||
SMTPPassword: new(string),
|
||||
SMTPServer: new(string),
|
||||
SMTPPort: new(string),
|
||||
SMTPServerTimeout: new(int),
|
||||
ConnectionSecurity: new(string),
|
||||
SendPushNotifications: new(bool),
|
||||
PushNotificationServer: new(string),
|
||||
PushNotificationContents: new(string),
|
||||
PushNotificationBuffer: new(int),
|
||||
EnableEmailBatching: new(bool),
|
||||
EmailBatchingBufferSize: new(int),
|
||||
EmailBatchingInterval: new(int),
|
||||
EnablePreviewModeBanner: new(bool),
|
||||
SkipServerCertificateVerification: new(bool),
|
||||
EmailNotificationContentsType: new(string),
|
||||
LoginButtonColor: new(string),
|
||||
LoginButtonBorderColor: new(string),
|
||||
LoginButtonTextColor: new(string),
|
||||
EnableInactivityEmail: new(bool),
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("use custom replyto instead of configured replyto", func(t *testing.T) {
|
||||
mailConfig := emailService.mailServiceConfig(customReplyTo)
|
||||
require.Equal(t, customReplyTo, mailConfig.ReplyToAddress)
|
||||
})
|
||||
|
||||
t.Run("use configured replyto", func(t *testing.T) {
|
||||
mailConfig := emailService.mailServiceConfig("")
|
||||
require.Equal(t, configuredReplyTo, mailConfig.ReplyToAddress)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,300 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/users"
|
||||
"github.com/mattermost/mattermost-server/v6/config"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/templates"
|
||||
"github.com/mattermost/mattermost-server/v6/store"
|
||||
"github.com/mattermost/mattermost-server/v6/store/storetest/mocks"
|
||||
"github.com/mattermost/mattermost-server/v6/testlib"
|
||||
)
|
||||
|
||||
type TestHelper struct {
|
||||
service *Service
|
||||
configStore *config.Store
|
||||
store store.Store
|
||||
workspace string
|
||||
|
||||
BasicTeam *model.Team
|
||||
BasicChannel *model.Channel
|
||||
BasicUser *model.User
|
||||
BasicUser2 *model.User
|
||||
|
||||
SystemAdminUser *model.User
|
||||
LogBuffer *bytes.Buffer
|
||||
}
|
||||
|
||||
func Setup(tb testing.TB) *TestHelper {
|
||||
if testing.Short() {
|
||||
tb.SkipNow()
|
||||
}
|
||||
dbStore := mainHelper.GetStore()
|
||||
dbStore.DropAllTables()
|
||||
dbStore.MarkSystemRanUnitTests()
|
||||
mainHelper.PreloadMigrations()
|
||||
|
||||
return setupTestHelper(dbStore, tb)
|
||||
}
|
||||
|
||||
func SetupWithStoreMock(tb testing.TB) *TestHelper {
|
||||
mockStore := testlib.GetMockStoreForSetupFunctions()
|
||||
th := setupTestHelper(mockStore, tb)
|
||||
statusMock := mocks.StatusStore{}
|
||||
statusMock.On("UpdateExpiredDNDStatuses").Return([]*model.Status{}, nil)
|
||||
statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil)
|
||||
statusMock.On("UpdateLastActivityAt", "user1", mock.Anything).Return(nil)
|
||||
statusMock.On("SaveOrUpdate", mock.AnythingOfType("*model.Status")).Return(nil)
|
||||
emptyMockStore := mocks.Store{}
|
||||
emptyMockStore.On("Close").Return(nil)
|
||||
emptyMockStore.On("Status").Return(&statusMock)
|
||||
th.service.store = &emptyMockStore
|
||||
return th
|
||||
}
|
||||
|
||||
func setupTestHelper(s store.Store, tb testing.TB) *TestHelper {
|
||||
tempWorkspace, err := os.MkdirTemp("", "userservicetest")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
configStore := config.NewTestMemoryStore()
|
||||
|
||||
config := configStore.Get()
|
||||
*config.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins")
|
||||
*config.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp")
|
||||
*config.PluginSettings.AutomaticPrepackagedPlugins = false
|
||||
*config.LogSettings.EnableSentry = false // disable error reporting during tests
|
||||
*config.AnnouncementSettings.AdminNoticesEnabled = false
|
||||
*config.AnnouncementSettings.UserNoticesEnabled = false
|
||||
*config.TeamSettings.MaxUsersPerTeam = 50
|
||||
*config.RateLimitSettings.Enable = false
|
||||
*config.TeamSettings.EnableOpenServer = true
|
||||
// Disable strict password requirements for test
|
||||
*config.PasswordSettings.MinimumLength = 5
|
||||
*config.PasswordSettings.Lowercase = false
|
||||
*config.PasswordSettings.Uppercase = false
|
||||
*config.PasswordSettings.Symbol = false
|
||||
*config.PasswordSettings.Number = false
|
||||
configStore.Set(config)
|
||||
|
||||
licenseFn := func() *model.License { return model.NewTestLicense() }
|
||||
|
||||
us, err := users.New(users.ServiceConfig{
|
||||
UserStore: s.User(),
|
||||
SessionStore: s.Session(),
|
||||
OAuthStore: s.OAuth(),
|
||||
ConfigFn: configStore.Get,
|
||||
LicenseFn: licenseFn,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
templatesDir, ok := templates.GetTemplateDirectory()
|
||||
if !ok {
|
||||
panic("failed find server templates")
|
||||
}
|
||||
htmlTemplateWatcher, errorsChan, err := templates.NewWithWatcher(templatesDir)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for err2 := range errorsChan {
|
||||
mlog.Error("Server templates error", mlog.Err(err2))
|
||||
}
|
||||
}()
|
||||
|
||||
service := &Service{
|
||||
store: s,
|
||||
userService: us,
|
||||
license: licenseFn,
|
||||
config: configStore.Get,
|
||||
templatesContainer: htmlTemplateWatcher,
|
||||
}
|
||||
|
||||
if err := service.setUpRateLimiters(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &TestHelper{
|
||||
service: service,
|
||||
configStore: configStore,
|
||||
store: s,
|
||||
LogBuffer: &bytes.Buffer{},
|
||||
workspace: tempWorkspace,
|
||||
}
|
||||
}
|
||||
|
||||
func (th *TestHelper) InitBasic() *TestHelper {
|
||||
th.BasicTeam = th.CreateTeam()
|
||||
|
||||
th.SystemAdminUser = th.CreateUser()
|
||||
th.SystemAdminUser, _ = th.service.userService.GetUser(th.SystemAdminUser.Id)
|
||||
th.addUserToTeam(th.BasicTeam, th.SystemAdminUser)
|
||||
|
||||
th.BasicUser = th.CreateUser()
|
||||
th.BasicUser, _ = th.service.userService.GetUser(th.BasicUser.Id)
|
||||
th.addUserToTeam(th.BasicTeam, th.BasicUser)
|
||||
|
||||
th.BasicUser2 = th.CreateUser()
|
||||
th.BasicUser2, _ = th.service.userService.GetUser(th.BasicUser2.Id)
|
||||
th.addUserToTeam(th.BasicTeam, th.BasicUser2)
|
||||
|
||||
th.BasicChannel = th.createChannel(th.BasicTeam, string(model.ChannelTypeOpen))
|
||||
th.addUserToChannel(th.BasicChannel, th.SystemAdminUser)
|
||||
th.addUserToChannel(th.BasicChannel, th.BasicUser)
|
||||
th.addUserToChannel(th.BasicChannel, th.BasicUser2)
|
||||
|
||||
return th
|
||||
}
|
||||
|
||||
func (th *TestHelper) CreateTeam() *model.Team {
|
||||
id := model.NewId()
|
||||
team := &model.Team{
|
||||
DisplayName: "dn_" + id,
|
||||
Name: "name" + id,
|
||||
Email: "success+" + id + "@simulator.amazonses.com",
|
||||
Type: model.TeamOpen,
|
||||
}
|
||||
|
||||
var err error
|
||||
if team, err = th.store.Team().Save(team); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return team
|
||||
}
|
||||
|
||||
func (th *TestHelper) createChannel(team *model.Team, channelType string) *model.Channel {
|
||||
id := model.NewId()
|
||||
|
||||
channel := &model.Channel{
|
||||
DisplayName: "dn_" + id,
|
||||
Name: "name_" + id,
|
||||
Type: model.ChannelType(channelType),
|
||||
TeamId: team.Id,
|
||||
CreatorId: th.BasicUser.Id,
|
||||
}
|
||||
|
||||
var err error
|
||||
if channel, err = th.store.Channel().Save(channel, *th.configStore.Get().TeamSettings.MaxChannelsPerTeam); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
func (th *TestHelper) addUserToChannel(channel *model.Channel, user *model.User) *model.ChannelMember {
|
||||
newMember := &model.ChannelMember{
|
||||
ChannelId: channel.Id,
|
||||
UserId: user.Id,
|
||||
NotifyProps: model.GetDefaultChannelNotifyProps(),
|
||||
SchemeGuest: user.IsGuest(),
|
||||
SchemeUser: !user.IsGuest(),
|
||||
}
|
||||
|
||||
var err error
|
||||
newMember, err = th.store.Channel().SaveMember(newMember)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return newMember
|
||||
}
|
||||
|
||||
func (th *TestHelper) addUserToTeam(team *model.Team, user *model.User) *model.TeamMember {
|
||||
tm := &model.TeamMember{
|
||||
TeamId: team.Id,
|
||||
UserId: user.Id,
|
||||
SchemeGuest: user.IsGuest(),
|
||||
SchemeUser: !user.IsGuest(),
|
||||
}
|
||||
|
||||
var err error
|
||||
tm, err = th.store.Team().SaveMember(tm, *th.service.config().TeamSettings.MaxUsersPerTeam)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return tm
|
||||
}
|
||||
|
||||
func (th *TestHelper) CreateUser() *model.User {
|
||||
return th.CreateUserOrGuest(false)
|
||||
}
|
||||
|
||||
func (th *TestHelper) CreateGuest() *model.User {
|
||||
return th.CreateUserOrGuest(true)
|
||||
}
|
||||
|
||||
func (th *TestHelper) CreateUserOrGuest(guest bool) *model.User {
|
||||
id := model.NewId()
|
||||
|
||||
user := &model.User{
|
||||
Email: "success+" + id + "@simulator.amazonses.com",
|
||||
Username: "un_" + id,
|
||||
Nickname: "nn_" + id,
|
||||
Password: "Password1",
|
||||
EmailVerified: true,
|
||||
}
|
||||
|
||||
var err error
|
||||
if guest {
|
||||
if user, err = th.service.userService.CreateUser(user, users.UserCreateOptions{Guest: true}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
if user, err = th.service.userService.CreateUser(user, users.UserCreateOptions{}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func (th *TestHelper) TearDown() {
|
||||
th.configStore.Close()
|
||||
|
||||
th.store.Close()
|
||||
|
||||
if th.workspace != "" {
|
||||
os.RemoveAll(th.workspace)
|
||||
}
|
||||
}
|
||||
|
||||
func (th *TestHelper) UpdateConfig(f func(*model.Config)) {
|
||||
if th.configStore.IsReadOnly() {
|
||||
return
|
||||
}
|
||||
old := th.configStore.Get()
|
||||
updated := old.Clone()
|
||||
f(updated)
|
||||
if _, _, err := th.configStore.Set(updated); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (th *TestHelper) ConfigureInbucketMail() {
|
||||
inbucket_host := os.Getenv("CI_INBUCKET_HOST")
|
||||
if inbucket_host == "" {
|
||||
inbucket_host = "localhost"
|
||||
}
|
||||
inbucket_port := os.Getenv("CI_INBUCKET_SMTP_PORT")
|
||||
if inbucket_port == "" {
|
||||
inbucket_port = "10025"
|
||||
}
|
||||
th.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.EmailSettings.SMTPServer = inbucket_host
|
||||
*cfg.EmailSettings.SMTPPort = inbucket_port
|
||||
})
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/testlib"
|
||||
)
|
||||
|
||||
var mainHelper *testlib.MainHelper
|
||||
var replicaFlag bool
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if f := flag.Lookup("mysql-replica"); f == nil {
|
||||
flag.BoolVar(&replicaFlag, "mysql-replica", false, "")
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
var options = testlib.HelperOptions{
|
||||
EnableStore: true,
|
||||
EnableResources: true,
|
||||
WithReadReplica: replicaFlag,
|
||||
}
|
||||
|
||||
mainHelper = testlib.NewMainHelperWithOptions(&options)
|
||||
defer mainHelper.Close()
|
||||
|
||||
mainHelper.Main(m)
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"html"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/i18n"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/utils"
|
||||
)
|
||||
|
||||
type FieldRow struct {
|
||||
Cells []*model.SlackAttachmentField
|
||||
}
|
||||
|
||||
type EmailMessageAttachment struct {
|
||||
model.SlackAttachment
|
||||
|
||||
Pretext template.HTML
|
||||
Text template.HTML
|
||||
FieldRows []FieldRow
|
||||
}
|
||||
|
||||
func (es *Service) GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string {
|
||||
if strings.TrimSpace(post.Message) != "" || len(post.FileIds) == 0 {
|
||||
return post.Message
|
||||
}
|
||||
|
||||
// extract the filenames from their paths and determine what type of files are attached
|
||||
infos, err := es.store.FileInfo().GetForPost(post.Id, true, false, true)
|
||||
if err != nil {
|
||||
mlog.Warn("Encountered error when getting files for notification message", mlog.String("post_id", post.Id), mlog.Err(err))
|
||||
}
|
||||
|
||||
filenames := make([]string, len(infos))
|
||||
onlyImages := true
|
||||
for i, info := range infos {
|
||||
if escaped, err := url.QueryUnescape(filepath.Base(info.Name)); err != nil {
|
||||
// this should never error since filepath was escaped using url.QueryEscape
|
||||
filenames[i] = escaped
|
||||
} else {
|
||||
filenames[i] = info.Name
|
||||
}
|
||||
|
||||
onlyImages = onlyImages && info.IsImage()
|
||||
}
|
||||
|
||||
props := map[string]any{"Filenames": strings.Join(filenames, ", ")}
|
||||
|
||||
if onlyImages {
|
||||
return translateFunc("api.post.get_message_for_notification.images_sent", len(filenames), props)
|
||||
}
|
||||
return translateFunc("api.post.get_message_for_notification.files_sent", len(filenames), props)
|
||||
}
|
||||
|
||||
func ProcessMessageAttachments(post *model.Post, siteURL string) []*EmailMessageAttachment {
|
||||
emailMessageAttachments := []*EmailMessageAttachment{}
|
||||
|
||||
for _, messageAttachment := range post.Attachments() {
|
||||
emailMessageAttachment := &EmailMessageAttachment{
|
||||
SlackAttachment: *messageAttachment,
|
||||
Pretext: prepareTextForEmail(messageAttachment.Pretext, siteURL),
|
||||
Text: prepareTextForEmail(messageAttachment.Text, siteURL),
|
||||
}
|
||||
|
||||
stripedTitle, err := utils.StripMarkdown(emailMessageAttachment.Title)
|
||||
if err != nil {
|
||||
mlog.Warn("Failed parse to markdown from messageatatchment title", mlog.String("post_id", post.Id), mlog.Err(err))
|
||||
stripedTitle = ""
|
||||
}
|
||||
|
||||
emailMessageAttachment.Title = stripedTitle
|
||||
|
||||
shortFieldRow := FieldRow{}
|
||||
|
||||
for i := range messageAttachment.Fields {
|
||||
// Create a new instance to avoid altering the original pointer reference
|
||||
// We update field value to parse markdown.
|
||||
// If we do that on the original pointer, the rendered text in mattermost
|
||||
// becomes invalid as its no longer a markdown string, but rather an HTML string.
|
||||
field := &model.SlackAttachmentField{
|
||||
Title: messageAttachment.Fields[i].Title,
|
||||
Value: messageAttachment.Fields[i].Value,
|
||||
Short: messageAttachment.Fields[i].Short,
|
||||
}
|
||||
|
||||
if stringValue, ok := field.Value.(string); ok {
|
||||
field.Value = prepareTextForEmail(stringValue, siteURL)
|
||||
}
|
||||
|
||||
if !field.Short {
|
||||
if len(shortFieldRow.Cells) > 0 {
|
||||
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
|
||||
shortFieldRow = FieldRow{}
|
||||
}
|
||||
|
||||
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, FieldRow{[]*model.SlackAttachmentField{field}})
|
||||
} else {
|
||||
shortFieldRow.Cells = append(shortFieldRow.Cells, field)
|
||||
|
||||
if len(shortFieldRow.Cells) == 2 {
|
||||
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
|
||||
shortFieldRow = FieldRow{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// collect any leftover short fields
|
||||
if len(shortFieldRow.Cells) > 0 {
|
||||
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
|
||||
shortFieldRow = FieldRow{}
|
||||
}
|
||||
|
||||
emailMessageAttachments = append(emailMessageAttachments, emailMessageAttachment)
|
||||
}
|
||||
|
||||
return emailMessageAttachments
|
||||
}
|
||||
|
||||
func prepareTextForEmail(text, siteURL string) template.HTML {
|
||||
escapedText := html.EscapeString(text)
|
||||
markdownText, err := utils.MarkdownToHTML(escapedText, siteURL)
|
||||
if err != nil {
|
||||
mlog.Warn("Encountered error while converting markdown to HTML", mlog.Err(err))
|
||||
return template.HTML(text)
|
||||
}
|
||||
|
||||
return template.HTML(markdownText)
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProcessMessageAttachments(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
post := &model.Post{
|
||||
Message: "This is the message",
|
||||
}
|
||||
|
||||
messageAttachments := []*model.SlackAttachment{
|
||||
{
|
||||
Color: "#FF0000",
|
||||
Pretext: "message attachment 1 pretext",
|
||||
AuthorName: "author name",
|
||||
AuthorLink: "https://example.com/slack_attachment_1/author_link",
|
||||
AuthorIcon: "https://example.com/slack_attachment_1/author_icon",
|
||||
Title: "message attachment 1 title",
|
||||
TitleLink: "https://example.com/slack_attachment_1/title_link",
|
||||
Text: "message attachment 1 text",
|
||||
ImageURL: "https://example.com/slack_attachment_1/image",
|
||||
ThumbURL: "https://example.com/slack_attachment_1/thumb",
|
||||
Fields: []*model.SlackAttachmentField{
|
||||
{
|
||||
Short: true,
|
||||
Title: "message attachment 1 field 1 title",
|
||||
Value: "message attachment 1 field 1 value",
|
||||
},
|
||||
{
|
||||
Short: false,
|
||||
Title: "message attachment 1 field 2 title",
|
||||
Value: "message attachment 1 field 2 value",
|
||||
},
|
||||
{
|
||||
Short: true,
|
||||
Title: "message attachment 1 field 3 title",
|
||||
Value: "message attachment 1 field 3 value",
|
||||
},
|
||||
{
|
||||
Short: true,
|
||||
Title: "message attachment 1 field 4 title",
|
||||
Value: "message attachment 1 field 4 value",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Color: "#FF0000",
|
||||
Pretext: "message attachment 2 pretext",
|
||||
AuthorName: "author name 2",
|
||||
Text: "message attachment 2 text",
|
||||
},
|
||||
}
|
||||
|
||||
model.ParseSlackAttachment(post, messageAttachments)
|
||||
|
||||
processedAttachmentsPost := ProcessMessageAttachments(post, "https://example.com")
|
||||
require.NotNil(t, processedAttachmentsPost)
|
||||
require.Len(t, processedAttachmentsPost, 2)
|
||||
require.Equal(t, processedAttachmentsPost[0].Color, "#FF0000")
|
||||
require.Equal(t, processedAttachmentsPost[0].FieldRows[0].Cells[0].Title, "message attachment 1 field 1 title")
|
||||
require.Equal(t, processedAttachmentsPost[1].Color, "#FF0000")
|
||||
}
|
||||
|
|
@ -1,176 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/throttled/throttled"
|
||||
"github.com/throttled/throttled/store/memstore"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/users"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/i18n"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/templates"
|
||||
"github.com/mattermost/mattermost-server/v6/store"
|
||||
)
|
||||
|
||||
const (
|
||||
emailRateLimitingMemstoreSize = 65536
|
||||
emailRateLimitingPerHour = 20
|
||||
emailRateLimitingMaxBurst = 20
|
||||
|
||||
TokenTypePasswordRecovery = "password_recovery"
|
||||
TokenTypeVerifyEmail = "verify_email"
|
||||
TokenTypeTeamInvitation = "team_invitation"
|
||||
TokenTypeGuestInvitation = "guest_invitation"
|
||||
TokenTypeCWSAccess = "cws_access_token"
|
||||
)
|
||||
|
||||
func condenseSiteURL(siteURL string) string {
|
||||
parsedSiteURL, _ := url.Parse(siteURL)
|
||||
if parsedSiteURL.Path == "" || parsedSiteURL.Path == "/" {
|
||||
return parsedSiteURL.Host
|
||||
}
|
||||
|
||||
return path.Join(parsedSiteURL.Host, parsedSiteURL.Path)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
config func() *model.Config
|
||||
license func() *model.License
|
||||
|
||||
userService *users.UserService
|
||||
store store.Store
|
||||
|
||||
templatesContainer *templates.Container
|
||||
perHourEmailRateLimiter *throttled.GCRARateLimiter
|
||||
perDayEmailRateLimiter *throttled.GCRARateLimiter
|
||||
EmailBatching *EmailBatchingJob
|
||||
}
|
||||
|
||||
type ServiceConfig struct {
|
||||
ConfigFn func() *model.Config
|
||||
LicenseFn func() *model.License
|
||||
|
||||
TemplatesContainer *templates.Container
|
||||
UserService *users.UserService
|
||||
Store store.Store
|
||||
}
|
||||
|
||||
func NewService(config ServiceConfig) (*Service, error) {
|
||||
if err := config.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
service := &Service{
|
||||
config: config.ConfigFn,
|
||||
templatesContainer: config.TemplatesContainer,
|
||||
license: config.LicenseFn,
|
||||
store: config.Store,
|
||||
userService: config.UserService,
|
||||
}
|
||||
if err := service.setUpRateLimiters(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
service.InitEmailBatching()
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func (es *Service) Stop() {
|
||||
mlog.Info("Shutting down Email batching service...")
|
||||
if es.EmailBatching != nil {
|
||||
es.EmailBatching.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ServiceConfig) validate() error {
|
||||
if c.ConfigFn == nil || c.Store == nil || c.LicenseFn == nil || c.TemplatesContainer == nil {
|
||||
return errors.New("invalid service config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *Service) setUpRateLimiters() error {
|
||||
store, err := memstore.New(emailRateLimitingMemstoreSize)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to setup email rate limiting memstore.")
|
||||
}
|
||||
|
||||
perHourQuota := throttled.RateQuota{
|
||||
MaxRate: throttled.PerHour(emailRateLimitingPerHour),
|
||||
MaxBurst: emailRateLimitingMaxBurst,
|
||||
}
|
||||
|
||||
perDayQuota := throttled.RateQuota{
|
||||
MaxRate: throttled.PerDay(1),
|
||||
MaxBurst: 0,
|
||||
}
|
||||
|
||||
perHourRateLimiter, err := throttled.NewGCRARateLimiter(store, perHourQuota)
|
||||
if err != nil || perHourRateLimiter == nil {
|
||||
return errors.Wrap(err, "Unable to setup email rate limiting GCRA rate limiter.")
|
||||
}
|
||||
|
||||
perDayRateLimiter, err := throttled.NewGCRARateLimiter(store, perDayQuota)
|
||||
if err != nil || perDayRateLimiter == nil {
|
||||
return errors.Wrap(err, "Unable to setup per day email rate limiting GCRA rate limiter.")
|
||||
}
|
||||
|
||||
es.perHourEmailRateLimiter = perHourRateLimiter
|
||||
es.perDayEmailRateLimiter = perDayRateLimiter
|
||||
return nil
|
||||
}
|
||||
|
||||
type ServiceInterface interface {
|
||||
GetPerDayEmailRateLimiter() *throttled.GCRARateLimiter
|
||||
NewEmailTemplateData(locale string) templates.Data
|
||||
SendEmailChangeVerifyEmail(newUserEmail, locale, siteURL, token string) error
|
||||
SendEmailChangeEmail(oldEmail, newEmail, locale, siteURL string) error
|
||||
SendVerifyEmail(userEmail, locale, siteURL, token, redirect string) error
|
||||
SendSignInChangeEmail(email, method, locale, siteURL string) error
|
||||
SendWelcomeEmail(userID string, email string, verified bool, disableWelcomeEmail bool, locale, siteURL, redirect string) error
|
||||
SendCloudUpgradeConfirmationEmail(userEmail, name, trialEndDate, locale, siteURL, workspaceName string, isYearly bool, embeddedFiles map[string]io.Reader) error
|
||||
SendCloudWelcomeEmail(userEmail, locale, teamInviteID, workSpaceName, dns, siteURL string) error
|
||||
SendPasswordChangeEmail(email, method, locale, siteURL string) error
|
||||
SendUserAccessTokenAddedEmail(email, locale, siteURL string) error
|
||||
SendPasswordResetEmail(email string, token *model.Token, locale, siteURL string) (bool, error)
|
||||
SendMfaChangeEmail(email string, activated bool, locale, siteURL string) error
|
||||
SendInviteEmails(team *model.Team, senderName string, senderUserId string, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) error
|
||||
SendGuestInviteEmails(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, message string, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) error
|
||||
SendInviteEmailsToTeamAndChannels(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, message string, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) ([]*model.EmailInviteWithError, error)
|
||||
SendDeactivateAccountEmail(email string, locale, siteURL string) error
|
||||
SendNotificationMail(to, subject, htmlBody string) error
|
||||
SendMailWithEmbeddedFiles(to, subject, htmlBody string, embeddedFiles map[string]io.Reader, messageID string, inReplyTo string, references string, category string) error
|
||||
SendLicenseUpForRenewalEmail(email, name, locale, siteURL, ctaTitle, ctaLink, ctaText string, daysToExpiration int) error
|
||||
SendPaymentFailedEmail(email string, locale string, failedPayment *model.FailedPayment, planName, siteURL string) (bool, error)
|
||||
// Cloud delinquency email sequence
|
||||
SendDelinquencyEmail7(email, locale, siteURL, planName string) error
|
||||
SendDelinquencyEmail14(email, locale, siteURL, planName string) error
|
||||
SendDelinquencyEmail30(email, locale, siteURL, planName string) error
|
||||
SendDelinquencyEmail45(email, locale, siteURL, planName, delinquencyDate string) error
|
||||
SendDelinquencyEmail60(email, locale, siteURL string) error
|
||||
SendDelinquencyEmail75(email, locale, siteURL, planName, delinquencyDate string) error
|
||||
SendDelinquencyEmail90(email, locale, siteURL string) error
|
||||
SendNoCardPaymentFailedEmail(email string, locale string, siteURL string) error
|
||||
SendRemoveExpiredLicenseEmail(ctaText, ctaLink, email, locale, siteURL string) error
|
||||
AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError
|
||||
GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string
|
||||
InitEmailBatching()
|
||||
SendChangeUsernameEmail(newUsername, email, locale, siteURL string) error
|
||||
CreateVerifyEmailToken(userID string, newEmail string) (*model.Token, error)
|
||||
SendLicenseInactivityEmail(email, name, locale, siteURL string) error
|
||||
Stop()
|
||||
}
|
||||
|
||||
func (es *Service) GetPerDayEmailRateLimiter() *throttled.GCRARateLimiter {
|
||||
return es.perDayEmailRateLimiter
|
||||
}
|
||||
|
||||
func (es *Service) GetPerHourEmailRateLimiter() *throttled.GCRARateLimiter {
|
||||
return es.perHourEmailRateLimiter
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mail"
|
||||
"github.com/mattermost/mattermost-server/v6/utils"
|
||||
)
|
||||
|
||||
func (es *Service) mailServiceConfig(replyToAddress string) *mail.SMTPConfig {
|
||||
emailSettings := es.config().EmailSettings
|
||||
hostname := utils.GetHostnameFromSiteURL(*es.config().ServiceSettings.SiteURL)
|
||||
|
||||
if replyToAddress == "" {
|
||||
replyToAddress = *emailSettings.ReplyToAddress
|
||||
}
|
||||
|
||||
cfg := mail.SMTPConfig{
|
||||
Hostname: hostname,
|
||||
ConnectionSecurity: *emailSettings.ConnectionSecurity,
|
||||
SkipServerCertificateVerification: *emailSettings.SkipServerCertificateVerification,
|
||||
ServerName: *emailSettings.SMTPServer,
|
||||
Server: *emailSettings.SMTPServer,
|
||||
Port: *emailSettings.SMTPPort,
|
||||
ServerTimeout: *emailSettings.SMTPServerTimeout,
|
||||
Username: *emailSettings.SMTPUsername,
|
||||
Password: *emailSettings.SMTPPassword,
|
||||
EnableSMTPAuth: *emailSettings.EnableSMTPAuth,
|
||||
SendEmailNotifications: *emailSettings.SendEmailNotifications,
|
||||
FeedbackName: *emailSettings.FeedbackName,
|
||||
FeedbackEmail: *emailSettings.FeedbackEmail,
|
||||
ReplyToAddress: replyToAddress,
|
||||
}
|
||||
return &cfg
|
||||
}
|
||||
|
||||
func (es *Service) GetTrackFlowStartedByRole(isFirstAdmin bool, isSystemAdmin bool) string {
|
||||
trackFlowStartedByRole := "su"
|
||||
|
||||
if isFirstAdmin {
|
||||
trackFlowStartedByRole = "fa"
|
||||
} else if isSystemAdmin {
|
||||
trackFlowStartedByRole = "sa"
|
||||
}
|
||||
|
||||
return trackFlowStartedByRole
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSendInviteEmailRateLimits(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.BasicTeam.AllowedDomains = "common.com"
|
||||
_, err := th.App.UpdateTeam(th.BasicTeam)
|
||||
require.Nilf(t, err, "%v, Should update the team", err)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.EnableEmailInvitations = true
|
||||
})
|
||||
|
||||
memberInvite := &model.MemberInvite{}
|
||||
memberInvite.Emails = make([]string, 22)
|
||||
for i := 0; i < 22; i++ {
|
||||
memberInvite.Emails[i] = "test-" + strconv.Itoa(i) + "@common.com"
|
||||
}
|
||||
err = th.App.InviteNewUsersToTeam(memberInvite.Emails, th.BasicTeam.Id, th.BasicUser.Id)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "app.email.rate_limit_exceeded.app_error", err.Id)
|
||||
assert.Equal(t, http.StatusRequestEntityTooLarge, err.StatusCode)
|
||||
|
||||
_, err = th.App.InviteNewUsersToTeamGracefully(memberInvite, th.BasicTeam.Id, th.BasicUser.Id, "")
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "app.email.rate_limit_exceeded.app_error", err.Id)
|
||||
assert.Equal(t, http.StatusRequestEntityTooLarge, err.StatusCode)
|
||||
}
|
||||
355
app/emoji.go
355
app/emoji.go
|
|
@ -1,355 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color/palette"
|
||||
"image/draw"
|
||||
"image/gif"
|
||||
_ "image/jpeg"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
_ "golang.org/x/image/webp"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/request"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/store"
|
||||
"github.com/mattermost/mattermost-server/v6/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxEmojiFileSize = 1 << 19 // 512 KiB
|
||||
MaxEmojiWidth = 128
|
||||
MaxEmojiHeight = 128
|
||||
MaxEmojiOriginalWidth = 1028
|
||||
MaxEmojiOriginalHeight = 1028
|
||||
)
|
||||
|
||||
func (a *App) CreateEmoji(c request.CTX, sessionUserId string, emoji *model.Emoji, multiPartImageData *multipart.Form) (*model.Emoji, *model.AppError) {
|
||||
if !*a.Config().ServiceSettings.EnableCustomEmoji {
|
||||
return nil, model.NewAppError("UploadEmojiImage", "api.emoji.disabled.app_error", nil, "", http.StatusForbidden)
|
||||
}
|
||||
|
||||
if *a.Config().FileSettings.DriverName == "" {
|
||||
return nil, model.NewAppError("GetEmoji", "api.emoji.storage.app_error", nil, "", http.StatusForbidden)
|
||||
}
|
||||
|
||||
// wipe the emoji id so that existing emojis can't get overwritten
|
||||
emoji.Id = ""
|
||||
|
||||
// do our best to validate the emoji before committing anything to the DB so that we don't have to clean up
|
||||
// orphaned files left over when validation fails later on
|
||||
emoji.PreSave()
|
||||
if appErr := emoji.IsValid(); appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
if emoji.CreatorId != sessionUserId {
|
||||
return nil, model.NewAppError("createEmoji", "api.emoji.create.other_user.app_error", nil, "", http.StatusForbidden)
|
||||
}
|
||||
|
||||
if existingEmoji, err := a.Srv().Store().Emoji().GetByName(context.Background(), emoji.Name, true); err == nil && existingEmoji != nil {
|
||||
return nil, model.NewAppError("createEmoji", "api.emoji.create.duplicate.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
}
|
||||
|
||||
imageData := multiPartImageData.File["image"]
|
||||
if len(imageData) == 0 {
|
||||
return nil, model.NewAppError("Context", "api.context.invalid_body_param.app_error", map[string]any{"Name": "createEmoji"}, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if appErr := a.UploadEmojiImage(c, emoji.Id, imageData[0]); appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
emoji, err := a.Srv().Store().Emoji().Save(emoji)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("CreateEmoji", "app.emoji.create.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventEmojiAdded, "", "", "", nil, "")
|
||||
emojiJSON, jsonErr := json.Marshal(emoji)
|
||||
if jsonErr != nil {
|
||||
return nil, model.NewAppError("CreateEmoji", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
|
||||
}
|
||||
message.Add("emoji", string(emojiJSON))
|
||||
a.Publish(message)
|
||||
return emoji, nil
|
||||
}
|
||||
|
||||
func (a *App) GetEmojiList(c request.CTX, page, perPage int, sort string) ([]*model.Emoji, *model.AppError) {
|
||||
list, err := a.Srv().Store().Emoji().GetList(page*perPage, perPage, sort)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetEmojiList", "app.emoji.get_list.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (a *App) UploadEmojiImage(c request.CTX, id string, imageData *multipart.FileHeader) *model.AppError {
|
||||
if !*a.Config().ServiceSettings.EnableCustomEmoji {
|
||||
return model.NewAppError("UploadEmojiImage", "api.emoji.disabled.app_error", nil, "", http.StatusForbidden)
|
||||
}
|
||||
|
||||
if *a.Config().FileSettings.DriverName == "" {
|
||||
return model.NewAppError("UploadEmojiImage", "api.emoji.storage.app_error", nil, "", http.StatusForbidden)
|
||||
}
|
||||
|
||||
file, err := imageData.Open()
|
||||
if err != nil {
|
||||
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.open.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
io.Copy(buf, file)
|
||||
|
||||
// make sure the file is an image and is within the required dimensions
|
||||
config, _, err := image.DecodeConfig(bytes.NewReader(buf.Bytes()))
|
||||
if err != nil {
|
||||
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.image.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
}
|
||||
|
||||
if config.Width > MaxEmojiOriginalWidth || config.Height > MaxEmojiOriginalHeight {
|
||||
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.too_large.app_error", map[string]any{
|
||||
"MaxWidth": MaxEmojiOriginalWidth,
|
||||
"MaxHeight": MaxEmojiOriginalHeight,
|
||||
}, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if config.Width > MaxEmojiWidth || config.Height > MaxEmojiHeight {
|
||||
data := buf.Bytes()
|
||||
newbuf := bytes.NewBuffer(nil)
|
||||
info, err := model.GetInfoForBytes(imageData.Filename, bytes.NewReader(data), len(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.MimeType == "image/gif" {
|
||||
gif_data, err := gif.DecodeAll(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_decode_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
}
|
||||
|
||||
resized_gif := resizeEmojiGif(gif_data)
|
||||
if err := gif.EncodeAll(newbuf, resized_gif); err != nil {
|
||||
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_encode_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
}
|
||||
|
||||
buf = newbuf
|
||||
} else {
|
||||
img, _, err := image.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.decode_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
}
|
||||
|
||||
resizedImg := resizeEmoji(img, config.Width, config.Height)
|
||||
if err := a.ch.imgEncoder.EncodePNG(newbuf, resizedImg); err != nil {
|
||||
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.encode_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
}
|
||||
buf = newbuf
|
||||
}
|
||||
}
|
||||
|
||||
_, appErr := a.WriteFile(buf, getEmojiImagePath(id))
|
||||
return appErr
|
||||
}
|
||||
|
||||
func (a *App) DeleteEmoji(c request.CTX, emoji *model.Emoji) *model.AppError {
|
||||
if err := a.Srv().Store().Emoji().Delete(emoji, model.GetMillis()); err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return model.NewAppError("DeleteEmoji", "app.emoji.delete.no_results", nil, "id="+emoji.Id, http.StatusNotFound).Wrap(err)
|
||||
default:
|
||||
return model.NewAppError("DeleteEmoji", "app.emoji.delete.app_error", nil, "id="+emoji.Id, http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
a.deleteEmojiImage(emoji.Id)
|
||||
a.deleteReactionsForEmoji(emoji.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) GetEmoji(c request.CTX, emojiId string) (*model.Emoji, *model.AppError) {
|
||||
if !*a.Config().ServiceSettings.EnableCustomEmoji {
|
||||
return nil, model.NewAppError("GetEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusForbidden)
|
||||
}
|
||||
|
||||
if *a.Config().FileSettings.DriverName == "" {
|
||||
return nil, model.NewAppError("GetEmoji", "api.emoji.storage.app_error", nil, "", http.StatusForbidden)
|
||||
}
|
||||
|
||||
emoji, err := a.Srv().Store().Emoji().Get(context.Background(), emojiId, true)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return emoji, model.NewAppError("GetEmoji", "app.emoji.get.no_result", nil, "", http.StatusNotFound).Wrap(err)
|
||||
default:
|
||||
return emoji, model.NewAppError("GetEmoji", "app.emoji.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return emoji, nil
|
||||
}
|
||||
|
||||
func (a *App) GetEmojiByName(c request.CTX, emojiName string) (*model.Emoji, *model.AppError) {
|
||||
if !*a.Config().ServiceSettings.EnableCustomEmoji {
|
||||
return nil, model.NewAppError("GetEmojiByName", "api.emoji.disabled.app_error", nil, "", http.StatusForbidden)
|
||||
}
|
||||
|
||||
if *a.Config().FileSettings.DriverName == "" {
|
||||
return nil, model.NewAppError("GetEmojiByName", "api.emoji.storage.app_error", nil, "", http.StatusForbidden)
|
||||
}
|
||||
|
||||
emoji, err := a.Srv().Store().Emoji().GetByName(context.Background(), emojiName, true)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return emoji, model.NewAppError("GetEmojiByName", "app.emoji.get_by_name.no_result", nil, "", http.StatusNotFound).Wrap(err)
|
||||
default:
|
||||
return emoji, model.NewAppError("GetEmojiByName", "app.emoji.get_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return emoji, nil
|
||||
}
|
||||
|
||||
func (a *App) GetMultipleEmojiByName(c request.CTX, names []string) ([]*model.Emoji, *model.AppError) {
|
||||
if !*a.Config().ServiceSettings.EnableCustomEmoji {
|
||||
return nil, model.NewAppError("GetMultipleEmojiByName", "api.emoji.disabled.app_error", nil, "", http.StatusForbidden)
|
||||
}
|
||||
|
||||
emoji, err := a.Srv().Store().Emoji().GetMultipleByName(names)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetMultipleEmojiByName", "app.emoji.get_by_name.app_error", nil, fmt.Sprintf("names=%v, %v", names, err.Error()), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return emoji, nil
|
||||
}
|
||||
|
||||
func (a *App) GetEmojiImage(c request.CTX, emojiId string) ([]byte, string, *model.AppError) {
|
||||
_, storeErr := a.Srv().Store().Emoji().Get(context.Background(), emojiId, true)
|
||||
if storeErr != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(storeErr, &nfErr):
|
||||
return nil, "", model.NewAppError("GetEmojiImage", "app.emoji.get.no_result", nil, "", http.StatusNotFound).Wrap(storeErr)
|
||||
default:
|
||||
return nil, "", model.NewAppError("GetEmojiImage", "app.emoji.get.app_error", nil, "", http.StatusInternalServerError).Wrap(storeErr)
|
||||
}
|
||||
}
|
||||
|
||||
img, appErr := a.ReadFile(getEmojiImagePath(emojiId))
|
||||
if appErr != nil {
|
||||
return nil, "", model.NewAppError("getEmojiImage", "api.emoji.get_image.read.app_error", nil, "", http.StatusNotFound).Wrap(appErr)
|
||||
}
|
||||
|
||||
_, imageType, err := image.DecodeConfig(bytes.NewReader(img))
|
||||
if err != nil {
|
||||
return nil, "", model.NewAppError("getEmojiImage", "api.emoji.get_image.decode.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
return img, imageType, nil
|
||||
}
|
||||
|
||||
func (a *App) SearchEmoji(c request.CTX, name string, prefixOnly bool, limit int) ([]*model.Emoji, *model.AppError) {
|
||||
if !*a.Config().ServiceSettings.EnableCustomEmoji {
|
||||
return nil, model.NewAppError("SearchEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusForbidden)
|
||||
}
|
||||
|
||||
list, err := a.Srv().Store().Emoji().Search(name, prefixOnly, limit)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("SearchEmoji", "app.emoji.get_by_name.app_error", nil, "name="+name+", "+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// GetEmojiStaticURL returns a relative static URL for system default emojis,
|
||||
// and the API route for custom ones. Errors if not found or if custom and deleted.
|
||||
func (a *App) GetEmojiStaticURL(c request.CTX, emojiName string) (string, *model.AppError) {
|
||||
subPath, _ := utils.GetSubpathFromConfig(a.Config())
|
||||
|
||||
if id, found := model.GetSystemEmojiId(emojiName); found {
|
||||
return path.Join(subPath, "/static/emoji", id+".png"), nil
|
||||
}
|
||||
|
||||
emoji, err := a.Srv().Store().Emoji().GetByName(context.Background(), emojiName, true)
|
||||
if err == nil {
|
||||
return path.Join(subPath, "/api/v4/emoji", emoji.Id, "image"), nil
|
||||
}
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return "", model.NewAppError("GetEmojiStaticURL", "app.emoji.get_by_name.no_result", nil, "", http.StatusNotFound).Wrap(err)
|
||||
default:
|
||||
return "", model.NewAppError("GetEmojiStaticURL", "app.emoji.get_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
func resizeEmojiGif(gifImg *gif.GIF) *gif.GIF {
|
||||
// Create a new RGBA image to hold the incremental frames.
|
||||
firstFrame := gifImg.Image[0].Bounds()
|
||||
b := image.Rect(0, 0, firstFrame.Dx(), firstFrame.Dy())
|
||||
img := image.NewRGBA(b)
|
||||
|
||||
resizedImage := image.Image(nil)
|
||||
// Resize each frame.
|
||||
for index, frame := range gifImg.Image {
|
||||
bounds := frame.Bounds()
|
||||
draw.Draw(img, bounds, frame, bounds.Min, draw.Over)
|
||||
resizedImage = resizeEmoji(img, firstFrame.Dx(), firstFrame.Dy())
|
||||
gifImg.Image[index] = imageToPaletted(resizedImage)
|
||||
}
|
||||
// Set new gif width and height
|
||||
gifImg.Config.Width = resizedImage.Bounds().Dx()
|
||||
gifImg.Config.Height = resizedImage.Bounds().Dy()
|
||||
return gifImg
|
||||
}
|
||||
|
||||
func getEmojiImagePath(id string) string {
|
||||
return "emoji/" + id + "/image"
|
||||
}
|
||||
|
||||
func resizeEmoji(img image.Image, width int, height int) image.Image {
|
||||
emojiWidth := float64(width)
|
||||
emojiHeight := float64(height)
|
||||
|
||||
if emojiHeight <= MaxEmojiHeight && emojiWidth <= MaxEmojiWidth {
|
||||
return img
|
||||
}
|
||||
return imaging.Fit(img, MaxEmojiWidth, MaxEmojiHeight, imaging.Lanczos)
|
||||
}
|
||||
|
||||
func imageToPaletted(img image.Image) *image.Paletted {
|
||||
b := img.Bounds()
|
||||
pm := image.NewPaletted(b, palette.Plan9)
|
||||
draw.FloydSteinberg.Draw(pm, b, img, image.Point{})
|
||||
return pm
|
||||
}
|
||||
|
||||
func (a *App) deleteEmojiImage(id string) {
|
||||
if err := a.MoveFile(getEmojiImagePath(id), "emoji/"+id+"/image_deleted"); err != nil {
|
||||
mlog.Warn("Failed to rename image when deleting emoji", mlog.String("emoji_id", id))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) deleteReactionsForEmoji(emojiName string) {
|
||||
if err := a.Srv().Store().Reaction().DeleteAllWithEmojiName(emojiName); err != nil {
|
||||
mlog.Warn("Unable to delete reactions when deleting emoji", mlog.String("emoji_name", emojiName), mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/v6/einterfaces"
|
||||
ejobs "github.com/mattermost/mattermost-server/v6/einterfaces/jobs"
|
||||
)
|
||||
|
||||
var accountMigrationInterface func(*App) einterfaces.AccountMigrationInterface
|
||||
|
||||
func RegisterAccountMigrationInterface(f func(*App) einterfaces.AccountMigrationInterface) {
|
||||
accountMigrationInterface = f
|
||||
}
|
||||
|
||||
var complianceInterface func(*App) einterfaces.ComplianceInterface
|
||||
|
||||
func RegisterComplianceInterface(f func(*App) einterfaces.ComplianceInterface) {
|
||||
complianceInterface = f
|
||||
}
|
||||
|
||||
var dataRetentionInterface func(*App) einterfaces.DataRetentionInterface
|
||||
|
||||
func RegisterDataRetentionInterface(f func(*App) einterfaces.DataRetentionInterface) {
|
||||
dataRetentionInterface = f
|
||||
}
|
||||
|
||||
var jobsDataRetentionJobInterface func(*Server) ejobs.DataRetentionJobInterface
|
||||
|
||||
func RegisterJobsDataRetentionJobInterface(f func(*Server) ejobs.DataRetentionJobInterface) {
|
||||
jobsDataRetentionJobInterface = f
|
||||
}
|
||||
|
||||
var jobsMessageExportJobInterface func(*Server) ejobs.MessageExportJobInterface
|
||||
|
||||
func RegisterJobsMessageExportJobInterface(f func(*Server) ejobs.MessageExportJobInterface) {
|
||||
jobsMessageExportJobInterface = f
|
||||
}
|
||||
|
||||
var jobsElasticsearchAggregatorInterface func(*Server) ejobs.ElasticsearchAggregatorInterface
|
||||
|
||||
func RegisterJobsElasticsearchAggregatorInterface(f func(*Server) ejobs.ElasticsearchAggregatorInterface) {
|
||||
jobsElasticsearchAggregatorInterface = f
|
||||
}
|
||||
|
||||
var jobsElasticsearchIndexerInterface func(*Server) ejobs.IndexerJobInterface
|
||||
|
||||
func RegisterJobsElasticsearchIndexerInterface(f func(*Server) ejobs.IndexerJobInterface) {
|
||||
jobsElasticsearchIndexerInterface = f
|
||||
}
|
||||
|
||||
var jobsLdapSyncInterface func(*App) ejobs.LdapSyncInterface
|
||||
|
||||
func RegisterJobsLdapSyncInterface(f func(*App) ejobs.LdapSyncInterface) {
|
||||
jobsLdapSyncInterface = f
|
||||
}
|
||||
|
||||
var ldapInterface func(*App) einterfaces.LdapInterface
|
||||
|
||||
func RegisterLdapInterface(f func(*App) einterfaces.LdapInterface) {
|
||||
ldapInterface = f
|
||||
}
|
||||
|
||||
var messageExportInterface func(*App) einterfaces.MessageExportInterface
|
||||
|
||||
func RegisterMessageExportInterface(f func(*App) einterfaces.MessageExportInterface) {
|
||||
messageExportInterface = f
|
||||
}
|
||||
|
||||
var cloudInterface func(*Server) einterfaces.CloudInterface
|
||||
|
||||
func RegisterCloudInterface(f func(*Server) einterfaces.CloudInterface) {
|
||||
cloudInterface = f
|
||||
}
|
||||
|
||||
var samlInterfaceNew func(*App) einterfaces.SamlInterface
|
||||
|
||||
func RegisterNewSamlInterface(f func(*App) einterfaces.SamlInterface) {
|
||||
samlInterfaceNew = f
|
||||
}
|
||||
|
||||
var notificationInterface func(*App) einterfaces.NotificationInterface
|
||||
|
||||
func RegisterNotificationInterface(f func(*App) einterfaces.NotificationInterface) {
|
||||
notificationInterface = f
|
||||
}
|
||||
|
||||
func (s *Server) initEnterprise() {
|
||||
if cloudInterface != nil {
|
||||
s.Cloud = cloudInterface(s)
|
||||
}
|
||||
}
|
||||
853
app/export.go
853
app/export.go
|
|
@ -1,853 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/imports"
|
||||
"github.com/mattermost/mattermost-server/v6/app/request"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/store"
|
||||
)
|
||||
|
||||
// We use this map to identify the exportable preferences.
|
||||
// Here we link the preference category and name, to the name of the relevant field in the import struct.
|
||||
var exportablePreferences = map[imports.ComparablePreference]string{{
|
||||
Category: model.PreferenceCategoryTheme,
|
||||
Name: "",
|
||||
}: "Theme", {
|
||||
Category: model.PreferenceCategoryAdvancedSettings,
|
||||
Name: "feature_enabled_markdown_preview",
|
||||
}: "UseMarkdownPreview", {
|
||||
Category: model.PreferenceCategoryAdvancedSettings,
|
||||
Name: "formatting",
|
||||
}: "UseFormatting", {
|
||||
Category: model.PreferenceCategorySidebarSettings,
|
||||
Name: "show_unread_section",
|
||||
}: "ShowUnreadSection", {
|
||||
Category: model.PreferenceCategoryDisplaySettings,
|
||||
Name: model.PreferenceNameUseMilitaryTime,
|
||||
}: "UseMilitaryTime", {
|
||||
Category: model.PreferenceCategoryDisplaySettings,
|
||||
Name: model.PreferenceNameCollapseSetting,
|
||||
}: "CollapsePreviews", {
|
||||
Category: model.PreferenceCategoryDisplaySettings,
|
||||
Name: model.PreferenceNameMessageDisplay,
|
||||
}: "MessageDisplay", {
|
||||
Category: model.PreferenceCategoryDisplaySettings,
|
||||
Name: "channel_display_mode",
|
||||
}: "CollapseConsecutive", {
|
||||
Category: model.PreferenceCategoryDisplaySettings,
|
||||
Name: "collapse_consecutive_messages",
|
||||
}: "ColorizeUsernames", {
|
||||
Category: model.PreferenceCategoryDisplaySettings,
|
||||
Name: "colorize_usernames",
|
||||
}: "ChannelDisplayMode", {
|
||||
Category: model.PreferenceCategoryTutorialSteps,
|
||||
Name: "",
|
||||
}: "TutorialStep", {
|
||||
Category: model.PreferenceCategoryNotifications,
|
||||
Name: model.PreferenceNameEmailInterval,
|
||||
}: "EmailInterval",
|
||||
}
|
||||
|
||||
func (a *App) BulkExport(ctx request.CTX, writer io.Writer, outPath string, job *model.Job, opts model.BulkExportOpts) *model.AppError {
|
||||
var zipWr *zip.Writer
|
||||
if opts.CreateArchive {
|
||||
var err error
|
||||
zipWr = zip.NewWriter(writer)
|
||||
defer zipWr.Close()
|
||||
writer, err = zipWr.Create("import.jsonl")
|
||||
if err != nil {
|
||||
return model.NewAppError("BulkExport", "app.export.zip_create.error",
|
||||
nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
if job != nil && job.Data == nil {
|
||||
job.Data = make(model.StringMap)
|
||||
}
|
||||
|
||||
ctx.Logger().Info("Bulk export: exporting version")
|
||||
if err := a.exportVersion(writer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.Logger().Info("Bulk export: exporting teams")
|
||||
teamNames, err := a.exportAllTeams(ctx, job, writer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.Logger().Info("Bulk export: exporting channels")
|
||||
if err = a.exportAllChannels(ctx, job, writer, teamNames); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.Logger().Info("Bulk export: exporting users")
|
||||
if err = a.exportAllUsers(ctx, job, writer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.Logger().Info("Bulk export: exporting posts")
|
||||
attachments, err := a.exportAllPosts(ctx, job, writer, opts.IncludeAttachments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.Logger().Info("Bulk export: exporting emoji")
|
||||
emojiPaths, err := a.exportCustomEmoji(ctx, job, writer, outPath, "exported_emoji", !opts.CreateArchive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.Logger().Info("Bulk export: exporting direct channels")
|
||||
if err = a.exportAllDirectChannels(ctx, job, writer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.Logger().Info("Bulk export: exporting direct posts")
|
||||
directAttachments, err := a.exportAllDirectPosts(ctx, job, writer, opts.IncludeAttachments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.IncludeAttachments {
|
||||
ctx.Logger().Info("Bulk export: exporting file attachments")
|
||||
for _, attachment := range attachments {
|
||||
if err := a.exportFile(outPath, *attachment.Path, zipWr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, attachment := range directAttachments {
|
||||
if err := a.exportFile(outPath, *attachment.Path, zipWr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, emojiPath := range emojiPaths {
|
||||
if err := a.exportFile(outPath, emojiPath, zipWr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "attachments_exported", len(attachments)+len(directAttachments)+len(emojiPaths))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) exportWriteLine(w io.Writer, line *imports.LineImportData) *model.AppError {
|
||||
b, err := json.Marshal(line)
|
||||
if err != nil {
|
||||
return model.NewAppError("BulkExport", "app.export.export_write_line.json_marshall.error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
}
|
||||
|
||||
if _, err := w.Write(append(b, '\n')); err != nil {
|
||||
return model.NewAppError("BulkExport", "app.export.export_write_line.io_writer.error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) exportVersion(writer io.Writer) *model.AppError {
|
||||
version := 1
|
||||
|
||||
info := &imports.VersionInfoImportData{
|
||||
Generator: "mattermost-server",
|
||||
Version: fmt.Sprintf("%s (%s, enterprise: %s)", model.CurrentVersion, model.BuildHash, model.BuildEnterpriseReady),
|
||||
Created: time.Now().Format(time.RFC3339Nano),
|
||||
}
|
||||
|
||||
versionLine := &imports.LineImportData{
|
||||
Type: "version",
|
||||
Version: &version,
|
||||
Info: info,
|
||||
}
|
||||
|
||||
return a.exportWriteLine(writer, versionLine)
|
||||
}
|
||||
|
||||
func (a *App) exportAllTeams(ctx request.CTX, job *model.Job, writer io.Writer) (map[string]bool, *model.AppError) {
|
||||
afterId := strings.Repeat("0", 26)
|
||||
teamNames := make(map[string]bool)
|
||||
cnt := 0
|
||||
for {
|
||||
teams, err := a.Srv().Store().Team().GetAllForExportAfter(1000, afterId)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("exportAllTeams", "app.team.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
if len(teams) == 0 {
|
||||
break
|
||||
}
|
||||
cnt += len(teams)
|
||||
updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "teams_exported", cnt)
|
||||
|
||||
for _, team := range teams {
|
||||
afterId = team.Id
|
||||
|
||||
// Skip deleted.
|
||||
if team.DeleteAt != 0 {
|
||||
continue
|
||||
}
|
||||
teamNames[team.Name] = true
|
||||
|
||||
teamLine := ImportLineFromTeam(team)
|
||||
if err := a.exportWriteLine(writer, teamLine); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return teamNames, nil
|
||||
}
|
||||
|
||||
func (a *App) exportAllChannels(ctx request.CTX, job *model.Job, writer io.Writer, teamNames map[string]bool) *model.AppError {
|
||||
afterId := strings.Repeat("0", 26)
|
||||
cnt := 0
|
||||
for {
|
||||
channels, err := a.Srv().Store().Channel().GetAllChannelsForExportAfter(1000, afterId)
|
||||
|
||||
if err != nil {
|
||||
return model.NewAppError("exportAllChannels", "app.channel.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
if len(channels) == 0 {
|
||||
break
|
||||
}
|
||||
cnt += len(channels)
|
||||
updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "channels_exported", cnt)
|
||||
|
||||
for _, channel := range channels {
|
||||
afterId = channel.Id
|
||||
|
||||
// Skip deleted.
|
||||
if channel.DeleteAt != 0 {
|
||||
continue
|
||||
}
|
||||
// Skip channels on deleted teams.
|
||||
if ok := teamNames[channel.TeamName]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
channelLine := ImportLineFromChannel(channel)
|
||||
if err := a.exportWriteLine(writer, channelLine); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) exportAllUsers(ctx request.CTX, job *model.Job, writer io.Writer) *model.AppError {
|
||||
afterId := strings.Repeat("0", 26)
|
||||
cnt := 0
|
||||
for {
|
||||
users, err := a.Srv().Store().User().GetAllAfter(1000, afterId)
|
||||
|
||||
if err != nil {
|
||||
return model.NewAppError("exportAllUsers", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
break
|
||||
}
|
||||
cnt += len(users)
|
||||
updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "users_exported", cnt)
|
||||
|
||||
for _, user := range users {
|
||||
afterId = user.Id
|
||||
|
||||
// Gathering here the exportable preferences to pass them on to ImportLineFromUser
|
||||
exportedPrefs := make(map[string]*string)
|
||||
allPrefs, err := a.GetPreferencesForUser(user.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, pref := range allPrefs {
|
||||
// We need to manage the special cases
|
||||
// Here we manage Tutorial steps
|
||||
if pref.Category == model.PreferenceCategoryTutorialSteps {
|
||||
pref.Name = ""
|
||||
// Then the email interval
|
||||
} else if pref.Category == model.PreferenceCategoryNotifications && pref.Name == model.PreferenceNameEmailInterval {
|
||||
switch pref.Value {
|
||||
case model.PreferenceEmailIntervalNoBatchingSeconds:
|
||||
pref.Value = model.PreferenceEmailIntervalImmediately
|
||||
case model.PreferenceEmailIntervalFifteenAsSeconds:
|
||||
pref.Value = model.PreferenceEmailIntervalFifteen
|
||||
case model.PreferenceEmailIntervalHourAsSeconds:
|
||||
pref.Value = model.PreferenceEmailIntervalHour
|
||||
case "0":
|
||||
pref.Value = ""
|
||||
}
|
||||
}
|
||||
id, ok := exportablePreferences[imports.ComparablePreference{
|
||||
Category: pref.Category,
|
||||
Name: pref.Name,
|
||||
}]
|
||||
if ok {
|
||||
prefPtr := pref.Value
|
||||
if prefPtr != "" {
|
||||
exportedPrefs[id] = &prefPtr
|
||||
} else {
|
||||
exportedPrefs[id] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userLine := ImportLineFromUser(user, exportedPrefs)
|
||||
|
||||
userLine.User.NotifyProps = a.buildUserNotifyProps(user.NotifyProps)
|
||||
|
||||
// Do the Team Memberships.
|
||||
members, err := a.buildUserTeamAndChannelMemberships(user.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userLine.User.Teams = members
|
||||
|
||||
if err := a.exportWriteLine(writer, userLine); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) buildUserTeamAndChannelMemberships(userID string) (*[]imports.UserTeamImportData, *model.AppError) {
|
||||
var memberships []imports.UserTeamImportData
|
||||
|
||||
members, err := a.Srv().Store().Team().GetTeamMembersForExport(userID)
|
||||
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("buildUserTeamAndChannelMemberships", "app.team.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
for _, member := range members {
|
||||
// Skip deleted.
|
||||
if member.DeleteAt != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
memberData := ImportUserTeamDataFromTeamMember(member)
|
||||
|
||||
// Do the Channel Memberships.
|
||||
channelMembers, err := a.buildUserChannelMemberships(userID, member.TeamId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the user theme
|
||||
themePreference, nErr := a.Srv().Store().Preference().Get(member.UserId, model.PreferenceCategoryTheme, member.TeamId)
|
||||
if nErr == nil {
|
||||
memberData.Theme = &themePreference.Value
|
||||
}
|
||||
|
||||
memberData.Channels = channelMembers
|
||||
|
||||
memberships = append(memberships, *memberData)
|
||||
}
|
||||
|
||||
return &memberships, nil
|
||||
}
|
||||
|
||||
func (a *App) buildUserChannelMemberships(userID string, teamID string) (*[]imports.UserChannelImportData, *model.AppError) {
|
||||
members, nErr := a.Srv().Store().Channel().GetChannelMembersForExport(userID, teamID)
|
||||
if nErr != nil {
|
||||
return nil, model.NewAppError("buildUserChannelMemberships", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
|
||||
category := model.PreferenceCategoryFavoriteChannel
|
||||
preferences, err := a.GetPreferenceByCategoryForUser(userID, category)
|
||||
if err != nil && err.StatusCode != http.StatusNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
memberships := make([]imports.UserChannelImportData, len(members))
|
||||
for i, member := range members {
|
||||
memberships[i] = *ImportUserChannelDataFromChannelMemberAndPreferences(member, &preferences)
|
||||
}
|
||||
return &memberships, nil
|
||||
}
|
||||
|
||||
func (a *App) buildUserNotifyProps(notifyProps model.StringMap) *imports.UserNotifyPropsImportData {
|
||||
|
||||
getProp := func(key string) *string {
|
||||
if v, ok := notifyProps[key]; ok {
|
||||
return &v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return &imports.UserNotifyPropsImportData{
|
||||
Desktop: getProp(model.DesktopNotifyProp),
|
||||
DesktopSound: getProp(model.DesktopSoundNotifyProp),
|
||||
Email: getProp(model.EmailNotifyProp),
|
||||
Mobile: getProp(model.PushNotifyProp),
|
||||
MobilePushStatus: getProp(model.PushStatusNotifyProp),
|
||||
ChannelTrigger: getProp(model.ChannelMentionsNotifyProp),
|
||||
CommentsTrigger: getProp(model.CommentsNotifyProp),
|
||||
MentionKeys: getProp(model.MentionKeysNotifyProp),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) exportAllPosts(ctx request.CTX, job *model.Job, writer io.Writer, withAttachments bool) ([]imports.AttachmentImportData, *model.AppError) {
|
||||
var attachments []imports.AttachmentImportData
|
||||
afterId := strings.Repeat("0", 26)
|
||||
var postProcessCount uint64
|
||||
logCheckpoint := time.Now()
|
||||
|
||||
cnt := 0
|
||||
for {
|
||||
if time.Since(logCheckpoint) > 5*time.Minute {
|
||||
ctx.Logger().Debug(fmt.Sprintf("Bulk Export: processed %d posts", postProcessCount))
|
||||
logCheckpoint = time.Now()
|
||||
}
|
||||
|
||||
posts, nErr := a.Srv().Store().Post().GetParentsForExportAfter(1000, afterId)
|
||||
if nErr != nil {
|
||||
return nil, model.NewAppError("exportAllPosts", "app.post.get_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
|
||||
if len(posts) == 0 {
|
||||
return attachments, nil
|
||||
}
|
||||
cnt += len(posts)
|
||||
updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "posts_exported", cnt)
|
||||
|
||||
for _, post := range posts {
|
||||
afterId = post.Id
|
||||
postProcessCount++
|
||||
|
||||
// Skip deleted.
|
||||
if post.DeleteAt != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
postLine := ImportLineForPost(post)
|
||||
|
||||
replies, replyAttachments, err := a.buildPostReplies(ctx, post.Id, withAttachments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if withAttachments && len(replyAttachments) > 0 {
|
||||
attachments = append(attachments, replyAttachments...)
|
||||
}
|
||||
|
||||
postLine.Post.Replies = &replies
|
||||
postLine.Post.Reactions = &[]imports.ReactionImportData{}
|
||||
if post.HasReactions {
|
||||
postLine.Post.Reactions, err = a.BuildPostReactions(ctx, post.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(post.FileIds) > 0 {
|
||||
postAttachments, err := a.buildPostAttachments(post.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
postLine.Post.Attachments = &postAttachments
|
||||
|
||||
if withAttachments && len(postAttachments) > 0 {
|
||||
attachments = append(attachments, postAttachments...)
|
||||
}
|
||||
}
|
||||
|
||||
if err := a.exportWriteLine(writer, postLine); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) buildPostReplies(ctx request.CTX, postID string, withAttachments bool) ([]imports.ReplyImportData, []imports.AttachmentImportData, *model.AppError) {
|
||||
var replies []imports.ReplyImportData
|
||||
var attachments []imports.AttachmentImportData
|
||||
|
||||
replyPosts, nErr := a.Srv().Store().Post().GetRepliesForExport(postID)
|
||||
if nErr != nil {
|
||||
return nil, nil, model.NewAppError("buildPostReplies", "app.post.get_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
|
||||
for _, reply := range replyPosts {
|
||||
replyImportObject := ImportReplyFromPost(reply)
|
||||
if reply.HasReactions {
|
||||
var appErr *model.AppError
|
||||
replyImportObject.Reactions, appErr = a.BuildPostReactions(ctx, reply.Id)
|
||||
if appErr != nil {
|
||||
return nil, nil, appErr
|
||||
}
|
||||
}
|
||||
if len(reply.FileIds) > 0 {
|
||||
postAttachments, appErr := a.buildPostAttachments(reply.Id)
|
||||
if appErr != nil {
|
||||
return nil, nil, appErr
|
||||
}
|
||||
replyImportObject.Attachments = &postAttachments
|
||||
if withAttachments && len(postAttachments) > 0 {
|
||||
attachments = append(attachments, postAttachments...)
|
||||
}
|
||||
}
|
||||
|
||||
replies = append(replies, *replyImportObject)
|
||||
}
|
||||
|
||||
return replies, attachments, nil
|
||||
}
|
||||
|
||||
func (a *App) BuildPostReactions(ctx request.CTX, postID string) (*[]ReactionImportData, *model.AppError) {
|
||||
var reactionsOfPost []imports.ReactionImportData
|
||||
|
||||
reactions, nErr := a.Srv().Store().Reaction().GetForPost(postID, true)
|
||||
if nErr != nil {
|
||||
return nil, model.NewAppError("BuildPostReactions", "app.reaction.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
|
||||
for _, reaction := range reactions {
|
||||
user, err := a.Srv().Store().User().Get(context.Background(), reaction.UserId)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
if errors.As(err, &nfErr) { // this is a valid case, the user that reacted might've been deleted by now
|
||||
ctx.Logger().Info("Skipping reactions by user since the entity doesn't exist anymore", mlog.String("user_id", reaction.UserId))
|
||||
continue
|
||||
}
|
||||
return nil, model.NewAppError("BuildPostReactions", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
reactionsOfPost = append(reactionsOfPost, *ImportReactionFromPost(user, reaction))
|
||||
}
|
||||
|
||||
return &reactionsOfPost, nil
|
||||
|
||||
}
|
||||
|
||||
func (a *App) buildPostAttachments(postID string) ([]imports.AttachmentImportData, *model.AppError) {
|
||||
infos, nErr := a.Srv().Store().FileInfo().GetForPost(postID, false, false, false)
|
||||
if nErr != nil {
|
||||
return nil, model.NewAppError("buildPostAttachments", "app.file_info.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
|
||||
attachments := make([]imports.AttachmentImportData, 0, len(infos))
|
||||
for _, info := range infos {
|
||||
attachments = append(attachments, imports.AttachmentImportData{Path: &info.Path})
|
||||
}
|
||||
|
||||
return attachments, nil
|
||||
}
|
||||
|
||||
func (a *App) exportCustomEmoji(c request.CTX, job *model.Job, writer io.Writer, outPath, exportDir string, exportFiles bool) ([]string, *model.AppError) {
|
||||
var emojiPaths []string
|
||||
pageNumber := 0
|
||||
cnt := 0
|
||||
for {
|
||||
customEmojiList, err := a.GetEmojiList(c, pageNumber, 100, model.EmojiSortByName)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(customEmojiList) == 0 {
|
||||
break
|
||||
}
|
||||
cnt += len(customEmojiList)
|
||||
updateJobProgress(c.Logger(), a.Srv().Store(), job, "emojis_exported", cnt)
|
||||
|
||||
pageNumber++
|
||||
|
||||
emojiPath := filepath.Join(*a.Config().FileSettings.Directory, "emoji")
|
||||
pathToDir := filepath.Join(outPath, exportDir)
|
||||
if exportFiles {
|
||||
if _, err := os.Stat(pathToDir); os.IsNotExist(err) {
|
||||
os.Mkdir(pathToDir, os.ModePerm)
|
||||
}
|
||||
}
|
||||
|
||||
for _, emoji := range customEmojiList {
|
||||
emojiImagePath := filepath.Join(emojiPath, emoji.Id, "image")
|
||||
filePath := filepath.Join(exportDir, emoji.Id, "image")
|
||||
if exportFiles {
|
||||
err := a.copyEmojiImages(emoji.Id, emojiImagePath, pathToDir)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("BulkExport", "app.export.export_custom_emoji.copy_emoji_images.error", nil, "err="+err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
} else {
|
||||
filePath = filepath.Join("emoji", emoji.Id, "image")
|
||||
emojiPaths = append(emojiPaths, filePath)
|
||||
}
|
||||
|
||||
emojiImportObject := ImportLineFromEmoji(emoji, filePath)
|
||||
if err := a.exportWriteLine(writer, emojiImportObject); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return emojiPaths, nil
|
||||
}
|
||||
|
||||
// Copies emoji files from 'data/emoji' dir to 'exported_emoji' dir
|
||||
func (a *App) copyEmojiImages(emojiId string, emojiImagePath string, pathToDir string) error {
|
||||
fromPath, err := os.Open(emojiImagePath)
|
||||
if fromPath == nil || err != nil {
|
||||
return errors.New("Error reading " + emojiImagePath + "file")
|
||||
}
|
||||
defer fromPath.Close()
|
||||
|
||||
emojiDir := pathToDir + "/" + emojiId
|
||||
|
||||
if _, err = os.Stat(emojiDir); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return errors.Wrapf(err, "Error fetching file info of emoji directory %v", emojiDir)
|
||||
}
|
||||
|
||||
if err = os.Mkdir(emojiDir, os.ModePerm); err != nil {
|
||||
return errors.Wrapf(err, "Error creating emoji directory %v", emojiDir)
|
||||
}
|
||||
}
|
||||
|
||||
toPath, err := os.OpenFile(emojiDir+"/image", os.O_RDWR|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
return errors.New("Error creating the image file " + err.Error())
|
||||
}
|
||||
defer toPath.Close()
|
||||
|
||||
_, err = io.Copy(toPath, fromPath)
|
||||
if err != nil {
|
||||
return errors.New("Error copying emojis " + err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) exportAllDirectChannels(ctx request.CTX, job *model.Job, writer io.Writer) *model.AppError {
|
||||
afterId := strings.Repeat("0", 26)
|
||||
cnt := 0
|
||||
for {
|
||||
channels, err := a.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, afterId)
|
||||
if err != nil {
|
||||
return model.NewAppError("exportAllDirectChannels", "app.channel.get_all_direct.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
if len(channels) == 0 {
|
||||
break
|
||||
}
|
||||
cnt += len(channels)
|
||||
updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "direct_channels_exported", cnt)
|
||||
|
||||
for _, channel := range channels {
|
||||
afterId = channel.Id
|
||||
|
||||
// Skip if there are no active members in the channel
|
||||
if len(*channel.Members) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip deleted.
|
||||
if channel.DeleteAt != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
favoritedBy, err := a.buildFavoritedByList(channel.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
channelLine := ImportLineFromDirectChannel(channel, favoritedBy)
|
||||
if err := a.exportWriteLine(writer, channelLine); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) buildFavoritedByList(channelID string) ([]string, *model.AppError) {
|
||||
prefs, err := a.Srv().Store().Preference().GetCategoryAndName(model.PreferenceCategoryFavoriteChannel, channelID)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("buildFavoritedByList", "app.preference.get_category.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
userIDs := make([]string, 0, len(prefs))
|
||||
for _, pref := range prefs {
|
||||
if pref.Value != "true" {
|
||||
continue
|
||||
}
|
||||
|
||||
user, err := a.Srv().Store().User().Get(context.Background(), pref.UserId)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("buildFavoritedByList", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
userIDs = append(userIDs, user.Username)
|
||||
}
|
||||
|
||||
return userIDs, nil
|
||||
}
|
||||
|
||||
func (a *App) exportAllDirectPosts(ctx request.CTX, job *model.Job, writer io.Writer, withAttachments bool) ([]imports.AttachmentImportData, *model.AppError) {
|
||||
var attachments []imports.AttachmentImportData
|
||||
afterId := strings.Repeat("0", 26)
|
||||
var postProcessCount uint64
|
||||
logCheckpoint := time.Now()
|
||||
|
||||
cnt := 0
|
||||
for {
|
||||
if time.Since(logCheckpoint) > 5*time.Minute {
|
||||
ctx.Logger().Debug(fmt.Sprintf("Bulk Export: processed %d direct posts", postProcessCount))
|
||||
logCheckpoint = time.Now()
|
||||
}
|
||||
|
||||
posts, err := a.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, afterId)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("exportAllDirectPosts", "app.post.get_direct_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
if len(posts) == 0 {
|
||||
break
|
||||
}
|
||||
cnt += len(posts)
|
||||
updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "direct_posts_exported", cnt)
|
||||
|
||||
for _, post := range posts {
|
||||
afterId = post.Id
|
||||
postProcessCount++
|
||||
|
||||
// Skip deleted.
|
||||
if post.DeleteAt != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle attachments.
|
||||
var postAttachments []imports.AttachmentImportData
|
||||
var err *model.AppError
|
||||
if len(post.FileIds) > 0 {
|
||||
postAttachments, err = a.buildPostAttachments(post.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if withAttachments && len(postAttachments) > 0 {
|
||||
attachments = append(attachments, postAttachments...)
|
||||
}
|
||||
}
|
||||
|
||||
// Do the Replies.
|
||||
replies, replyAttachments, err := a.buildPostReplies(ctx, post.Id, withAttachments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if withAttachments && len(replyAttachments) > 0 {
|
||||
attachments = append(attachments, replyAttachments...)
|
||||
}
|
||||
|
||||
postLine := ImportLineForDirectPost(post)
|
||||
postLine.DirectPost.Replies = &replies
|
||||
if len(postAttachments) > 0 {
|
||||
postLine.DirectPost.Attachments = &postAttachments
|
||||
}
|
||||
if err := a.exportWriteLine(writer, postLine); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return attachments, nil
|
||||
}
|
||||
|
||||
func (a *App) exportFile(outPath, filePath string, zipWr *zip.Writer) *model.AppError {
|
||||
var wr io.Writer
|
||||
var err error
|
||||
rd, appErr := a.FileReader(filePath)
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
defer rd.Close()
|
||||
|
||||
if zipWr != nil {
|
||||
wr, err = zipWr.CreateHeader(&zip.FileHeader{
|
||||
Name: filepath.Join(model.ExportDataDir, filePath),
|
||||
Method: zip.Store,
|
||||
})
|
||||
if err != nil {
|
||||
return model.NewAppError("exportFileAttachment", "app.export.export_attachment.zip_create_header.error",
|
||||
nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
} else {
|
||||
filePath = filepath.Join(outPath, model.ExportDataDir, filePath)
|
||||
if err = os.MkdirAll(filepath.Dir(filePath), 0700); err != nil {
|
||||
return model.NewAppError("exportFileAttachment", "app.export.export_attachment.mkdirall.error",
|
||||
nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
wr, err = os.Create(filePath)
|
||||
if err != nil {
|
||||
return model.NewAppError("exportFileAttachment", "app.export.export_attachment.create_file.error",
|
||||
nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
defer wr.(*os.File).Close()
|
||||
}
|
||||
|
||||
if _, err := io.Copy(wr, rd); err != nil {
|
||||
return model.NewAppError("exportFileAttachment", "app.export.export_attachment.copy_file.error",
|
||||
nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) ListExports() ([]string, *model.AppError) {
|
||||
exports, appErr := a.ListDirectory(*a.Config().ExportSettings.Directory)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
results := make([]string, len(exports))
|
||||
for i := range exports {
|
||||
results[i] = filepath.Base(exports[i])
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (a *App) DeleteExport(name string) *model.AppError {
|
||||
filePath := filepath.Join(*a.Config().ExportSettings.Directory, name)
|
||||
|
||||
if ok, err := a.FileExists(filePath); err != nil {
|
||||
return err
|
||||
} else if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return a.RemoveFile(filePath)
|
||||
}
|
||||
|
||||
func updateJobProgress(logger mlog.LoggerIFace, store store.Store, job *model.Job, key string, value int) {
|
||||
if job != nil {
|
||||
job.Data[key] = strconv.Itoa(value)
|
||||
if _, err2 := store.Job().UpdateOptimistically(job, model.JobStatusInProgress); err2 != nil {
|
||||
logger.Warn("Failed to update job status", mlog.Err(err2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,769 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/utils"
|
||||
"github.com/mattermost/mattermost-server/v6/utils/fileutils"
|
||||
)
|
||||
|
||||
func TestReactionsOfPost(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
post := th.BasicPost
|
||||
post.HasReactions = true
|
||||
th.BasicUser2.DeleteAt = 1234
|
||||
reactionObject := model.Reaction{
|
||||
UserId: th.BasicUser.Id,
|
||||
PostId: post.Id,
|
||||
EmojiName: "emoji",
|
||||
CreateAt: model.GetMillis(),
|
||||
}
|
||||
reactionObjectDeleted := model.Reaction{
|
||||
UserId: th.BasicUser2.Id,
|
||||
PostId: post.Id,
|
||||
EmojiName: "emoji",
|
||||
CreateAt: model.GetMillis(),
|
||||
}
|
||||
|
||||
th.App.SaveReactionForPost(th.Context, &reactionObject)
|
||||
th.App.SaveReactionForPost(th.Context, &reactionObjectDeleted)
|
||||
reactionsOfPost, err := th.App.BuildPostReactions(th.Context, post.Id)
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.Equal(t, reactionObject.EmojiName, *(*reactionsOfPost)[0].EmojiName)
|
||||
}
|
||||
|
||||
func TestExportUserNotifyProps(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
|
||||
userNotifyProps := model.StringMap{
|
||||
model.DesktopNotifyProp: model.UserNotifyAll,
|
||||
model.DesktopSoundNotifyProp: "true",
|
||||
model.EmailNotifyProp: "true",
|
||||
model.PushNotifyProp: model.UserNotifyAll,
|
||||
model.PushStatusNotifyProp: model.StatusOnline,
|
||||
model.ChannelMentionsNotifyProp: "true",
|
||||
model.CommentsNotifyProp: model.CommentsNotifyRoot,
|
||||
model.MentionKeysNotifyProp: "valid,misc",
|
||||
}
|
||||
|
||||
exportNotifyProps := th.App.buildUserNotifyProps(userNotifyProps)
|
||||
|
||||
require.Equal(t, userNotifyProps[model.DesktopNotifyProp], *exportNotifyProps.Desktop)
|
||||
require.Equal(t, userNotifyProps[model.DesktopSoundNotifyProp], *exportNotifyProps.DesktopSound)
|
||||
require.Equal(t, userNotifyProps[model.EmailNotifyProp], *exportNotifyProps.Email)
|
||||
require.Equal(t, userNotifyProps[model.PushNotifyProp], *exportNotifyProps.Mobile)
|
||||
require.Equal(t, userNotifyProps[model.PushStatusNotifyProp], *exportNotifyProps.MobilePushStatus)
|
||||
require.Equal(t, userNotifyProps[model.ChannelMentionsNotifyProp], *exportNotifyProps.ChannelTrigger)
|
||||
require.Equal(t, userNotifyProps[model.CommentsNotifyProp], *exportNotifyProps.CommentsTrigger)
|
||||
require.Equal(t, userNotifyProps[model.MentionKeysNotifyProp], *exportNotifyProps.MentionKeys)
|
||||
}
|
||||
|
||||
func TestExportUserChannels(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
channel := th.BasicChannel
|
||||
user := th.BasicUser
|
||||
team := th.BasicTeam
|
||||
channelName := channel.Name
|
||||
notifyProps := model.StringMap{
|
||||
model.DesktopNotifyProp: model.UserNotifyAll,
|
||||
model.PushNotifyProp: model.UserNotifyNone,
|
||||
}
|
||||
preference := model.Preference{
|
||||
UserId: user.Id,
|
||||
Category: model.PreferenceCategoryFavoriteChannel,
|
||||
Name: channel.Id,
|
||||
Value: "true",
|
||||
}
|
||||
|
||||
_, appErr := th.App.MarkChannelsAsViewed(th.Context, []string{th.BasicPost.ChannelId}, user.Id, "", true)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
var preferences model.Preferences
|
||||
preferences = append(preferences, preference)
|
||||
err := th.App.Srv().Store().Preference().Save(preferences)
|
||||
require.NoError(t, err)
|
||||
|
||||
th.App.UpdateChannelMemberNotifyProps(th.Context, notifyProps, channel.Id, user.Id)
|
||||
exportData, appErr := th.App.buildUserChannelMemberships(user.Id, team.Id)
|
||||
require.Nil(t, appErr)
|
||||
assert.Equal(t, len(*exportData), 3)
|
||||
for _, data := range *exportData {
|
||||
if *data.Name == channelName {
|
||||
assert.Equal(t, "all", *data.NotifyProps.Desktop)
|
||||
assert.Equal(t, "none", *data.NotifyProps.Mobile)
|
||||
assert.Equal(t, "all", *data.NotifyProps.MarkUnread) // default value
|
||||
assert.True(t, *data.Favorite)
|
||||
assert.NotEqualValues(t, 0, *data.LastViewedAt)
|
||||
assert.NotEqualValues(t, 0, *data.MsgCount)
|
||||
} else { // default values
|
||||
assert.Equal(t, "default", *data.NotifyProps.Desktop)
|
||||
assert.Equal(t, "default", *data.NotifyProps.Mobile)
|
||||
assert.Equal(t, "all", *data.NotifyProps.MarkUnread)
|
||||
assert.False(t, *data.Favorite)
|
||||
assert.EqualValues(t, 0, *data.LastViewedAt)
|
||||
assert.EqualValues(t, 0, *data.MsgCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyEmojiImages(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
|
||||
emoji := &model.Emoji{
|
||||
Id: model.NewId(),
|
||||
}
|
||||
|
||||
// Creating a dir named `exported_emoji_test` in the root of the repo
|
||||
pathToDir := "../exported_emoji_test"
|
||||
|
||||
os.Mkdir(pathToDir, 0777)
|
||||
defer os.RemoveAll(pathToDir)
|
||||
|
||||
filePath := "../data/emoji/" + emoji.Id
|
||||
emojiImagePath := filePath + "/image"
|
||||
|
||||
var _, err = os.Stat(filePath)
|
||||
if os.IsNotExist(err) {
|
||||
os.MkdirAll(filePath, 0777)
|
||||
}
|
||||
|
||||
// Creating a file with the name `image` to copy it to `exported_emoji_test`
|
||||
os.OpenFile(filePath+"/image", os.O_RDONLY|os.O_CREATE, 0777)
|
||||
defer os.RemoveAll(filePath)
|
||||
|
||||
copyError := th.App.copyEmojiImages(emoji.Id, emojiImagePath, pathToDir)
|
||||
require.NoError(t, copyError)
|
||||
|
||||
_, err = os.Stat(pathToDir + "/" + emoji.Id + "/image")
|
||||
require.False(t, os.IsNotExist(err), "File should exist ")
|
||||
}
|
||||
|
||||
func TestExportCustomEmoji(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
filePath := "../demo.json"
|
||||
|
||||
fileWriter, err := os.Create(filePath)
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(filePath)
|
||||
|
||||
dirNameToExportEmoji := "exported_emoji_test"
|
||||
defer os.RemoveAll("../" + dirNameToExportEmoji)
|
||||
|
||||
outPath, err := filepath.Abs(filePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, appErr := th.App.exportCustomEmoji(th.Context, nil, fileWriter, outPath, dirNameToExportEmoji, false)
|
||||
require.Nil(t, appErr, "should not have failed")
|
||||
}
|
||||
|
||||
func TestExportAllUsers(t *testing.T) {
|
||||
th1 := Setup(t)
|
||||
defer th1.TearDown()
|
||||
|
||||
// Adding a user and deactivating it to check whether it gets included in bulk export
|
||||
user := th1.CreateUser()
|
||||
_, err := th1.App.UpdateActive(th1.Context, user, false)
|
||||
require.Nil(t, err)
|
||||
|
||||
var b bytes.Buffer
|
||||
err = th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{})
|
||||
require.Nil(t, err)
|
||||
|
||||
th2 := Setup(t)
|
||||
defer th2.TearDown()
|
||||
err, i := th2.App.BulkImport(th2.Context, &b, nil, false, 5)
|
||||
assert.Nil(t, err)
|
||||
assert.EqualValues(t, 0, i)
|
||||
|
||||
users1, err := th1.App.GetUsersFromProfiles(&model.UserGetOptions{
|
||||
Page: 0,
|
||||
PerPage: 10,
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
users2, err := th2.App.GetUsersFromProfiles(&model.UserGetOptions{
|
||||
Page: 0,
|
||||
PerPage: 10,
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, len(users1), len(users2))
|
||||
assert.ElementsMatch(t, users1, users2)
|
||||
|
||||
// Checking whether deactivated users were included in bulk export
|
||||
deletedUsers1, err := th1.App.GetUsersFromProfiles(&model.UserGetOptions{
|
||||
Inactive: true,
|
||||
Page: 0,
|
||||
PerPage: 10,
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
deletedUsers2, err := th1.App.GetUsersFromProfiles(&model.UserGetOptions{
|
||||
Inactive: true,
|
||||
Page: 0,
|
||||
PerPage: 10,
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, len(deletedUsers1), len(deletedUsers2))
|
||||
assert.ElementsMatch(t, deletedUsers1, deletedUsers2)
|
||||
}
|
||||
|
||||
func TestExportDMChannel(t *testing.T) {
|
||||
t.Run("Export a DM channel to another server", func(t *testing.T) {
|
||||
th1 := Setup(t).InitBasic()
|
||||
defer th1.TearDown()
|
||||
|
||||
// DM Channel
|
||||
ch := th1.CreateDmChannel(th1.BasicUser2)
|
||||
|
||||
th1.App.Srv().Store().Preference().Save(model.Preferences{
|
||||
{
|
||||
UserId: th1.BasicUser2.Id,
|
||||
Category: model.PreferenceCategoryFavoriteChannel,
|
||||
Name: ch.Id,
|
||||
Value: "true",
|
||||
},
|
||||
})
|
||||
|
||||
var b bytes.Buffer
|
||||
err := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{})
|
||||
require.Nil(t, err)
|
||||
|
||||
channels, nErr := th1.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000")
|
||||
require.NoError(t, nErr)
|
||||
assert.Equal(t, 1, len(channels))
|
||||
|
||||
th2 := Setup(t).InitBasic()
|
||||
defer th2.TearDown()
|
||||
|
||||
channels, nErr = th2.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000")
|
||||
require.NoError(t, nErr)
|
||||
assert.Equal(t, 0, len(channels))
|
||||
|
||||
// import the exported channel
|
||||
err, i := th2.App.BulkImport(th2.Context, &b, nil, false, 5)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, 0, i)
|
||||
|
||||
// Ensure the Members of the imported DM channel is the same was from the exported
|
||||
channels, nErr = th2.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000")
|
||||
require.NoError(t, nErr)
|
||||
require.Equal(t, 1, len(channels))
|
||||
assert.ElementsMatch(t, []string{th1.BasicUser.Username, th1.BasicUser2.Username}, *channels[0].Members)
|
||||
|
||||
// Ensure the favorited channel was retained
|
||||
fav, nErr := th2.App.Srv().Store().Preference().Get(th2.BasicUser2.Id, model.PreferenceCategoryFavoriteChannel, channels[0].Id)
|
||||
require.NoError(t, nErr)
|
||||
require.NotNil(t, fav)
|
||||
require.Equal(t, "true", fav.Value)
|
||||
})
|
||||
|
||||
t.Run("Invalid DM channel export", func(t *testing.T) {
|
||||
th1 := Setup(t).InitBasic()
|
||||
defer th1.TearDown()
|
||||
|
||||
// DM Channel
|
||||
th1.CreateDmChannel(th1.BasicUser2)
|
||||
|
||||
channels, nErr := th1.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000")
|
||||
require.NoError(t, nErr)
|
||||
assert.Equal(t, 1, len(channels))
|
||||
|
||||
th1.App.PermanentDeleteUser(th1.Context, th1.BasicUser2)
|
||||
th1.App.PermanentDeleteUser(th1.Context, th1.BasicUser)
|
||||
|
||||
var b bytes.Buffer
|
||||
err := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{})
|
||||
require.Nil(t, err)
|
||||
|
||||
th2 := Setup(t).InitBasic()
|
||||
defer th2.TearDown()
|
||||
|
||||
// import the exported channel
|
||||
err, _ = th2.App.BulkImport(th2.Context, &b, nil, true, 5)
|
||||
require.Nil(t, err)
|
||||
|
||||
channels, nErr = th2.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000")
|
||||
require.NoError(t, nErr)
|
||||
assert.Empty(t, channels)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExportDMChannelToSelf(t *testing.T) {
|
||||
th1 := Setup(t).InitBasic()
|
||||
defer th1.TearDown()
|
||||
|
||||
// DM Channel with self (me channel)
|
||||
th1.CreateDmChannel(th1.BasicUser)
|
||||
|
||||
var b bytes.Buffer
|
||||
err := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{})
|
||||
require.Nil(t, err)
|
||||
|
||||
channels, nErr := th1.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000")
|
||||
require.NoError(t, nErr)
|
||||
assert.Equal(t, 1, len(channels))
|
||||
|
||||
th2 := Setup(t)
|
||||
defer th2.TearDown()
|
||||
|
||||
channels, nErr = th2.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000")
|
||||
require.NoError(t, nErr)
|
||||
assert.Equal(t, 0, len(channels))
|
||||
|
||||
// import the exported channel
|
||||
err, i := th2.App.BulkImport(th2.Context, &b, nil, false, 5)
|
||||
assert.Nil(t, err)
|
||||
assert.EqualValues(t, 0, i)
|
||||
|
||||
channels, nErr = th2.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000")
|
||||
require.NoError(t, nErr)
|
||||
assert.Equal(t, 1, len(channels))
|
||||
assert.Equal(t, 1, len((*channels[0].Members)))
|
||||
assert.Equal(t, th1.BasicUser.Username, (*channels[0].Members)[0])
|
||||
}
|
||||
|
||||
func TestExportGMChannel(t *testing.T) {
|
||||
th1 := Setup(t).InitBasic()
|
||||
|
||||
user1 := th1.CreateUser()
|
||||
th1.LinkUserToTeam(user1, th1.BasicTeam)
|
||||
user2 := th1.CreateUser()
|
||||
th1.LinkUserToTeam(user2, th1.BasicTeam)
|
||||
|
||||
// GM Channel
|
||||
th1.CreateGroupChannel(th1.Context, user1, user2)
|
||||
|
||||
var b bytes.Buffer
|
||||
err := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{})
|
||||
require.Nil(t, err)
|
||||
|
||||
channels, nErr := th1.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000")
|
||||
require.NoError(t, nErr)
|
||||
assert.Equal(t, 1, len(channels))
|
||||
|
||||
th1.TearDown()
|
||||
|
||||
th2 := Setup(t)
|
||||
defer th2.TearDown()
|
||||
|
||||
channels, nErr = th2.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000")
|
||||
require.NoError(t, nErr)
|
||||
assert.Equal(t, 0, len(channels))
|
||||
}
|
||||
|
||||
func TestExportGMandDMChannels(t *testing.T) {
|
||||
th1 := Setup(t).InitBasic()
|
||||
|
||||
// DM Channel
|
||||
th1.CreateDmChannel(th1.BasicUser2)
|
||||
|
||||
user1 := th1.CreateUser()
|
||||
th1.LinkUserToTeam(user1, th1.BasicTeam)
|
||||
user2 := th1.CreateUser()
|
||||
th1.LinkUserToTeam(user2, th1.BasicTeam)
|
||||
|
||||
// GM Channel
|
||||
th1.CreateGroupChannel(th1.Context, user1, user2)
|
||||
|
||||
var b bytes.Buffer
|
||||
err := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{})
|
||||
require.Nil(t, err)
|
||||
|
||||
channels, nErr := th1.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000")
|
||||
require.NoError(t, nErr)
|
||||
assert.Equal(t, 2, len(channels))
|
||||
|
||||
th1.TearDown()
|
||||
|
||||
th2 := Setup(t)
|
||||
defer th2.TearDown()
|
||||
|
||||
channels, nErr = th2.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000")
|
||||
require.NoError(t, nErr)
|
||||
assert.Equal(t, 0, len(channels))
|
||||
|
||||
// import the exported channel
|
||||
err, i := th2.App.BulkImport(th2.Context, &b, nil, false, 5)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 0, i)
|
||||
|
||||
// Ensure the Members of the imported GM channel is the same was from the exported
|
||||
channels, nErr = th2.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000")
|
||||
require.NoError(t, nErr)
|
||||
|
||||
// Adding some determinism so its possible to assert on slice index
|
||||
sort.Slice(channels, func(i, j int) bool { return channels[i].Type > channels[j].Type })
|
||||
assert.Equal(t, 2, len(channels))
|
||||
assert.ElementsMatch(t, []string{th1.BasicUser.Username, user1.Username, user2.Username}, *channels[0].Members)
|
||||
assert.ElementsMatch(t, []string{th1.BasicUser.Username, th1.BasicUser2.Username}, *channels[1].Members)
|
||||
}
|
||||
|
||||
func TestExportDMandGMPost(t *testing.T) {
|
||||
th1 := Setup(t).InitBasic()
|
||||
|
||||
// DM Channel
|
||||
dmChannel := th1.CreateDmChannel(th1.BasicUser2)
|
||||
dmMembers := []string{th1.BasicUser.Username, th1.BasicUser2.Username}
|
||||
|
||||
user1 := th1.CreateUser()
|
||||
th1.LinkUserToTeam(user1, th1.BasicTeam)
|
||||
user2 := th1.CreateUser()
|
||||
th1.LinkUserToTeam(user2, th1.BasicTeam)
|
||||
|
||||
// GM Channel
|
||||
gmChannel := th1.CreateGroupChannel(th1.Context, user1, user2)
|
||||
gmMembers := []string{th1.BasicUser.Username, user1.Username, user2.Username}
|
||||
|
||||
// DM posts
|
||||
p1 := &model.Post{
|
||||
ChannelId: dmChannel.Id,
|
||||
Message: "aa" + model.NewId() + "a",
|
||||
UserId: th1.BasicUser.Id,
|
||||
}
|
||||
th1.App.CreatePost(th1.Context, p1, dmChannel, false, true)
|
||||
|
||||
p2 := &model.Post{
|
||||
ChannelId: dmChannel.Id,
|
||||
Message: "bb" + model.NewId() + "a",
|
||||
UserId: th1.BasicUser.Id,
|
||||
}
|
||||
th1.App.CreatePost(th1.Context, p2, dmChannel, false, true)
|
||||
|
||||
// GM posts
|
||||
p3 := &model.Post{
|
||||
ChannelId: gmChannel.Id,
|
||||
Message: "cc" + model.NewId() + "a",
|
||||
UserId: th1.BasicUser.Id,
|
||||
}
|
||||
th1.App.CreatePost(th1.Context, p3, gmChannel, false, true)
|
||||
|
||||
p4 := &model.Post{
|
||||
ChannelId: gmChannel.Id,
|
||||
Message: "dd" + model.NewId() + "a",
|
||||
UserId: th1.BasicUser.Id,
|
||||
}
|
||||
th1.App.CreatePost(th1.Context, p4, gmChannel, false, true)
|
||||
|
||||
posts, err := th1.App.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, "0000000")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 4, len(posts))
|
||||
|
||||
var b bytes.Buffer
|
||||
appErr := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
th1.TearDown()
|
||||
|
||||
th2 := Setup(t)
|
||||
defer th2.TearDown()
|
||||
|
||||
posts, err = th2.App.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, "0000000")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, len(posts))
|
||||
|
||||
// import the exported posts
|
||||
appErr, i := th2.App.BulkImport(th2.Context, &b, nil, false, 5)
|
||||
assert.Nil(t, appErr)
|
||||
assert.Equal(t, 0, i)
|
||||
|
||||
posts, err = th2.App.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, "0000000")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Adding some determinism so its possible to assert on slice index
|
||||
sort.Slice(posts, func(i, j int) bool { return posts[i].Message > posts[j].Message })
|
||||
assert.Equal(t, 4, len(posts))
|
||||
assert.ElementsMatch(t, gmMembers, *posts[0].ChannelMembers)
|
||||
assert.ElementsMatch(t, gmMembers, *posts[1].ChannelMembers)
|
||||
assert.ElementsMatch(t, dmMembers, *posts[2].ChannelMembers)
|
||||
assert.ElementsMatch(t, dmMembers, *posts[3].ChannelMembers)
|
||||
}
|
||||
|
||||
func TestExportPostWithProps(t *testing.T) {
|
||||
th1 := Setup(t).InitBasic()
|
||||
|
||||
attachments := []*model.SlackAttachment{{Footer: "footer"}}
|
||||
|
||||
// DM Channel
|
||||
dmChannel := th1.CreateDmChannel(th1.BasicUser2)
|
||||
dmMembers := []string{th1.BasicUser.Username, th1.BasicUser2.Username}
|
||||
|
||||
user1 := th1.CreateUser()
|
||||
th1.LinkUserToTeam(user1, th1.BasicTeam)
|
||||
user2 := th1.CreateUser()
|
||||
th1.LinkUserToTeam(user2, th1.BasicTeam)
|
||||
|
||||
// GM Channel
|
||||
gmChannel := th1.CreateGroupChannel(th1.Context, user1, user2)
|
||||
gmMembers := []string{th1.BasicUser.Username, user1.Username, user2.Username}
|
||||
|
||||
// DM posts
|
||||
p1 := &model.Post{
|
||||
ChannelId: dmChannel.Id,
|
||||
Message: "aa" + model.NewId() + "a",
|
||||
Props: map[string]any{
|
||||
"attachments": attachments,
|
||||
},
|
||||
UserId: th1.BasicUser.Id,
|
||||
}
|
||||
th1.App.CreatePost(th1.Context, p1, dmChannel, false, true)
|
||||
|
||||
p2 := &model.Post{
|
||||
ChannelId: gmChannel.Id,
|
||||
Message: "dd" + model.NewId() + "a",
|
||||
Props: map[string]any{
|
||||
"attachments": attachments,
|
||||
},
|
||||
UserId: th1.BasicUser.Id,
|
||||
}
|
||||
th1.App.CreatePost(th1.Context, p2, gmChannel, false, true)
|
||||
|
||||
posts, err := th1.App.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, "0000000")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, posts, 2)
|
||||
require.NotEmpty(t, posts[0].Props)
|
||||
require.NotEmpty(t, posts[1].Props)
|
||||
|
||||
var b bytes.Buffer
|
||||
appErr := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
th1.TearDown()
|
||||
|
||||
th2 := Setup(t)
|
||||
defer th2.TearDown()
|
||||
|
||||
posts, err = th2.App.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, "0000000")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, posts, 0)
|
||||
|
||||
// import the exported posts
|
||||
appErr, i := th2.App.BulkImport(th2.Context, &b, nil, false, 5)
|
||||
assert.Nil(t, appErr)
|
||||
assert.Equal(t, 0, i)
|
||||
|
||||
posts, err = th2.App.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, "0000000")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Adding some determinism so its possible to assert on slice index
|
||||
sort.Slice(posts, func(i, j int) bool { return posts[i].Message > posts[j].Message })
|
||||
assert.Len(t, posts, 2)
|
||||
assert.ElementsMatch(t, gmMembers, *posts[0].ChannelMembers)
|
||||
assert.ElementsMatch(t, dmMembers, *posts[1].ChannelMembers)
|
||||
assert.Contains(t, posts[0].Props["attachments"].([]any)[0], "footer")
|
||||
assert.Contains(t, posts[1].Props["attachments"].([]any)[0], "footer")
|
||||
}
|
||||
|
||||
func TestExportDMPostWithSelf(t *testing.T) {
|
||||
th1 := Setup(t).InitBasic()
|
||||
|
||||
// DM Channel with self (me channel)
|
||||
dmChannel := th1.CreateDmChannel(th1.BasicUser)
|
||||
|
||||
th1.CreatePost(dmChannel)
|
||||
|
||||
var b bytes.Buffer
|
||||
err := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{})
|
||||
require.Nil(t, err)
|
||||
|
||||
posts, nErr := th1.App.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, "0000000")
|
||||
require.NoError(t, nErr)
|
||||
assert.Equal(t, 1, len(posts))
|
||||
|
||||
th1.TearDown()
|
||||
|
||||
th2 := Setup(t)
|
||||
defer th2.TearDown()
|
||||
|
||||
posts, nErr = th2.App.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, "0000000")
|
||||
require.NoError(t, nErr)
|
||||
assert.Equal(t, 0, len(posts))
|
||||
|
||||
// import the exported posts
|
||||
err, i := th2.App.BulkImport(th2.Context, &b, nil, false, 5)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 0, i)
|
||||
|
||||
posts, nErr = th2.App.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, "0000000")
|
||||
require.NoError(t, nErr)
|
||||
assert.Equal(t, 1, len(posts))
|
||||
assert.Equal(t, 1, len((*posts[0].ChannelMembers)))
|
||||
assert.Equal(t, th1.BasicUser.Username, (*posts[0].ChannelMembers)[0])
|
||||
}
|
||||
|
||||
func TestBulkExport(t *testing.T) {
|
||||
th := Setup(t)
|
||||
testsDir, _ := fileutils.FindDir("tests")
|
||||
|
||||
dir, err := os.MkdirTemp("", "import_test")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
extractImportFile := func(filePath string) *os.File {
|
||||
importFile, err2 := os.Open(filePath)
|
||||
require.NoError(t, err2)
|
||||
defer importFile.Close()
|
||||
|
||||
info, err2 := importFile.Stat()
|
||||
require.NoError(t, err2)
|
||||
|
||||
paths, err2 := utils.UnzipToPath(importFile, info.Size(), dir)
|
||||
require.NoError(t, err2)
|
||||
require.NotEmpty(t, paths)
|
||||
|
||||
jsonFile, err2 := os.Open(filepath.Join(dir, "import.jsonl"))
|
||||
require.NoError(t, err2)
|
||||
|
||||
return jsonFile
|
||||
}
|
||||
|
||||
jsonFile := extractImportFile(filepath.Join(testsDir, "import_test.zip"))
|
||||
defer jsonFile.Close()
|
||||
|
||||
appErr, _ := th.App.BulkImportWithPath(th.Context, jsonFile, nil, false, 1, dir)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
exportFile, err := os.Create(filepath.Join(dir, "export.zip"))
|
||||
require.NoError(t, err)
|
||||
defer exportFile.Close()
|
||||
|
||||
opts := model.BulkExportOpts{
|
||||
IncludeAttachments: true,
|
||||
CreateArchive: true,
|
||||
}
|
||||
appErr = th.App.BulkExport(th.Context, exportFile, dir, nil, opts)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
th.TearDown()
|
||||
th = Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
jsonFile = extractImportFile(filepath.Join(dir, "export.zip"))
|
||||
defer jsonFile.Close()
|
||||
|
||||
appErr, _ = th.App.BulkImportWithPath(th.Context, jsonFile, nil, false, 1, filepath.Join(dir, "data"))
|
||||
require.Nil(t, appErr)
|
||||
}
|
||||
|
||||
func TestBuildPostReplies(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
createPostWithAttachments := func(th *TestHelper, n int, rootID string) *model.Post {
|
||||
var fileIDs []string
|
||||
for i := 0; i < n; i++ {
|
||||
info, err := th.App.Srv().Store().FileInfo().Save(&model.FileInfo{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: fmt.Sprintf("file%d", i),
|
||||
Path: fmt.Sprintf("/data/file%d", i),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
fileIDs = append(fileIDs, info.Id)
|
||||
}
|
||||
|
||||
post, err := th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser.Id, ChannelId: th.BasicChannel.Id, RootId: rootID, FileIds: fileIDs}, th.BasicChannel, false, true)
|
||||
require.Nil(t, err)
|
||||
|
||||
return post
|
||||
}
|
||||
|
||||
t.Run("basic post", func(t *testing.T) {
|
||||
data, attachments, err := th.App.buildPostReplies(th.Context, th.BasicPost.Id, true)
|
||||
require.Nil(t, err)
|
||||
require.Empty(t, data)
|
||||
require.Empty(t, attachments)
|
||||
})
|
||||
|
||||
t.Run("root post with attachments and no replies", func(t *testing.T) {
|
||||
post := createPostWithAttachments(th, 5, "")
|
||||
data, attachments, err := th.App.buildPostReplies(th.Context, post.Id, true)
|
||||
require.Nil(t, err)
|
||||
require.Empty(t, data)
|
||||
require.Empty(t, attachments)
|
||||
})
|
||||
|
||||
t.Run("root post with attachments and a reply", func(t *testing.T) {
|
||||
post := createPostWithAttachments(th, 5, "")
|
||||
createPostWithAttachments(th, 0, post.Id)
|
||||
data, attachments, err := th.App.buildPostReplies(th.Context, post.Id, true)
|
||||
require.Nil(t, err)
|
||||
require.Len(t, data, 1)
|
||||
require.Empty(t, attachments)
|
||||
})
|
||||
|
||||
t.Run("root post with attachments and multiple replies with attachments", func(t *testing.T) {
|
||||
post := createPostWithAttachments(th, 5, "")
|
||||
reply1 := createPostWithAttachments(th, 2, post.Id)
|
||||
reply2 := createPostWithAttachments(th, 3, post.Id)
|
||||
data, attachments, err := th.App.buildPostReplies(th.Context, post.Id, true)
|
||||
require.Nil(t, err)
|
||||
require.Len(t, data, 2)
|
||||
require.Len(t, attachments, 5)
|
||||
if reply1.Id < reply2.Id {
|
||||
require.Len(t, *data[0].Attachments, 2)
|
||||
require.Len(t, *data[1].Attachments, 3)
|
||||
} else {
|
||||
require.Len(t, *data[1].Attachments, 2)
|
||||
require.Len(t, *data[0].Attachments, 3)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestExportDeletedTeams(t *testing.T) {
|
||||
th1 := Setup(t).InitBasic()
|
||||
defer th1.TearDown()
|
||||
|
||||
team1 := th1.CreateTeam()
|
||||
channel1 := th1.CreateChannel(th1.Context, team1)
|
||||
th1.CreatePost(channel1)
|
||||
|
||||
// Delete the team to check that this is handled correctly on import.
|
||||
err := th1.App.SoftDeleteTeam(team1.Id)
|
||||
require.Nil(t, err)
|
||||
|
||||
var b bytes.Buffer
|
||||
err = th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{})
|
||||
require.Nil(t, err)
|
||||
|
||||
th2 := Setup(t)
|
||||
defer th2.TearDown()
|
||||
err, i := th2.App.BulkImport(th2.Context, &b, nil, false, 5)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 0, i)
|
||||
|
||||
teams1, err := th1.App.GetAllTeams()
|
||||
assert.Nil(t, err)
|
||||
teams2, err := th2.App.GetAllTeams()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, len(teams1), len(teams2))
|
||||
assert.ElementsMatch(t, teams1, teams2)
|
||||
|
||||
channels1, err := th1.App.GetAllChannels(th1.Context, 0, 10, model.ChannelSearchOpts{})
|
||||
assert.Nil(t, err)
|
||||
channels2, err := th2.App.GetAllChannels(th1.Context, 0, 10, model.ChannelSearchOpts{})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, len(channels1), len(channels2))
|
||||
assert.ElementsMatch(t, channels1, channels2)
|
||||
for _, team := range teams2 {
|
||||
assert.NotContains(t, team.Name, team1.Name)
|
||||
assert.NotContains(t, team.Id, team1.Id)
|
||||
}
|
||||
}
|
||||
1464
app/file.go
1464
app/file.go
File diff suppressed because it is too large
Load diff
651
app/file_test.go
651
app/file_test.go
|
|
@ -1,651 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
eMocks "github.com/mattermost/mattermost-server/v6/einterfaces/mocks"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/services/searchengine/mocks"
|
||||
filesStoreMocks "github.com/mattermost/mattermost-server/v6/shared/filestore/mocks"
|
||||
"github.com/mattermost/mattermost-server/v6/store"
|
||||
storemocks "github.com/mattermost/mattermost-server/v6/store/storetest/mocks"
|
||||
"github.com/mattermost/mattermost-server/v6/utils/fileutils"
|
||||
)
|
||||
|
||||
func TestGeneratePublicLinkHash(t *testing.T) {
|
||||
filename1 := model.NewId() + "/" + model.NewRandomString(16) + ".txt"
|
||||
filename2 := model.NewId() + "/" + model.NewRandomString(16) + ".txt"
|
||||
salt1 := model.NewRandomString(32)
|
||||
salt2 := model.NewRandomString(32)
|
||||
|
||||
hash1 := GeneratePublicLinkHash(filename1, salt1)
|
||||
hash2 := GeneratePublicLinkHash(filename2, salt1)
|
||||
hash3 := GeneratePublicLinkHash(filename1, salt2)
|
||||
|
||||
hash := GeneratePublicLinkHash(filename1, salt1)
|
||||
assert.Equal(t, hash, hash1, "hash should be equal for the same file name and salt")
|
||||
|
||||
assert.NotEqual(t, hash1, hash2, "hashes for different files should not be equal")
|
||||
|
||||
assert.NotEqual(t, hash1, hash3, "hashes for the same file with different salts should not be equal")
|
||||
}
|
||||
|
||||
func TestDoUploadFile(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
teamID := model.NewId()
|
||||
channelID := model.NewId()
|
||||
userID := model.NewId()
|
||||
filename := "test"
|
||||
data := []byte("abcd")
|
||||
|
||||
info1, err := th.App.DoUploadFile(th.Context, time.Date(2007, 2, 4, 1, 2, 3, 4, time.Local), teamID, channelID, userID, filename, data)
|
||||
require.Nil(t, err, "DoUploadFile should succeed with valid data")
|
||||
defer func() {
|
||||
th.App.Srv().Store().FileInfo().PermanentDelete(info1.Id)
|
||||
th.App.RemoveFile(info1.Path)
|
||||
}()
|
||||
|
||||
value := fmt.Sprintf("20070204/teams/%v/channels/%v/users/%v/%v/%v", teamID, channelID, userID, info1.Id, filename)
|
||||
assert.Equal(t, value, info1.Path, "stored file at incorrect path")
|
||||
|
||||
info2, err := th.App.DoUploadFile(th.Context, time.Date(2007, 2, 4, 1, 2, 3, 4, time.Local), teamID, channelID, userID, filename, data)
|
||||
require.Nil(t, err, "DoUploadFile should succeed with valid data")
|
||||
defer func() {
|
||||
th.App.Srv().Store().FileInfo().PermanentDelete(info2.Id)
|
||||
th.App.RemoveFile(info2.Path)
|
||||
}()
|
||||
|
||||
value = fmt.Sprintf("20070204/teams/%v/channels/%v/users/%v/%v/%v", teamID, channelID, userID, info2.Id, filename)
|
||||
assert.Equal(t, value, info2.Path, "stored file at incorrect path")
|
||||
|
||||
info3, err := th.App.DoUploadFile(th.Context, time.Date(2008, 3, 5, 1, 2, 3, 4, time.Local), teamID, channelID, userID, filename, data)
|
||||
require.Nil(t, err, "DoUploadFile should succeed with valid data")
|
||||
defer func() {
|
||||
th.App.Srv().Store().FileInfo().PermanentDelete(info3.Id)
|
||||
th.App.RemoveFile(info3.Path)
|
||||
}()
|
||||
|
||||
value = fmt.Sprintf("20080305/teams/%v/channels/%v/users/%v/%v/%v", teamID, channelID, userID, info3.Id, filename)
|
||||
assert.Equal(t, value, info3.Path, "stored file at incorrect path")
|
||||
|
||||
info4, err := th.App.DoUploadFile(th.Context, time.Date(2009, 3, 5, 1, 2, 3, 4, time.Local), "../../"+teamID, "../../"+channelID, "../../"+userID, "../../"+filename, data)
|
||||
require.Nil(t, err, "DoUploadFile should succeed with valid data")
|
||||
defer func() {
|
||||
th.App.Srv().Store().FileInfo().PermanentDelete(info4.Id)
|
||||
th.App.RemoveFile(info4.Path)
|
||||
}()
|
||||
|
||||
value = fmt.Sprintf("20090305/teams/%v/channels/%v/users/%v/%v/%v", teamID, channelID, userID, info4.Id, filename)
|
||||
assert.Equal(t, value, info4.Path, "stored file at incorrect path")
|
||||
}
|
||||
|
||||
func TestUploadFile(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
channelID := th.BasicChannel.Id
|
||||
filename := "test"
|
||||
data := []byte("abcd")
|
||||
|
||||
info1, err := th.App.UploadFile(th.Context, data, "wrong", filename)
|
||||
require.NotNil(t, err, "Wrong Channel ID.")
|
||||
require.Nil(t, info1, "Channel ID does not exist.")
|
||||
|
||||
info1, err = th.App.UploadFile(th.Context, data, "", filename)
|
||||
require.Nil(t, err, "empty channel IDs should be valid")
|
||||
require.NotNil(t, info1)
|
||||
|
||||
info1, err = th.App.UploadFile(th.Context, data, channelID, filename)
|
||||
require.Nil(t, err, "UploadFile should succeed with valid data")
|
||||
defer func() {
|
||||
th.App.Srv().Store().FileInfo().PermanentDelete(info1.Id)
|
||||
th.App.RemoveFile(info1.Path)
|
||||
}()
|
||||
|
||||
value := fmt.Sprintf("%v/teams/noteam/channels/%v/users/nouser/%v/%v",
|
||||
time.Now().Format("20060102"), channelID, info1.Id, filename)
|
||||
assert.Equal(t, value, info1.Path, "Stored file at incorrect path")
|
||||
}
|
||||
|
||||
func TestParseOldFilenames(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
fileID := model.NewId()
|
||||
|
||||
tests := []struct {
|
||||
description string
|
||||
filenames []string
|
||||
channelID string
|
||||
userID string
|
||||
expected [][]string
|
||||
}{
|
||||
{
|
||||
description: "Empty input should result in empty output",
|
||||
filenames: []string{},
|
||||
channelID: th.BasicChannel.Id,
|
||||
userID: th.BasicUser.Id,
|
||||
expected: [][]string{},
|
||||
},
|
||||
{
|
||||
description: "Filename with invalid format should not parse",
|
||||
filenames: []string{"/path/to/some/file.png"},
|
||||
channelID: th.BasicChannel.Id,
|
||||
userID: th.BasicUser.Id,
|
||||
expected: [][]string{},
|
||||
},
|
||||
{
|
||||
description: "ChannelId in Filename should not match",
|
||||
filenames: []string{
|
||||
fmt.Sprintf("/%v/%v/%v/file.png", model.NewId(), th.BasicUser.Id, fileID),
|
||||
},
|
||||
channelID: th.BasicChannel.Id,
|
||||
userID: th.BasicUser.Id,
|
||||
expected: [][]string{},
|
||||
},
|
||||
{
|
||||
description: "UserId in Filename should not match",
|
||||
filenames: []string{
|
||||
fmt.Sprintf("/%v/%v/%v/file.png", th.BasicChannel.Id, model.NewId(), fileID),
|
||||
},
|
||||
channelID: th.BasicChannel.Id,
|
||||
userID: th.BasicUser.Id,
|
||||
expected: [][]string{},
|
||||
},
|
||||
{
|
||||
description: "../ in filename should not parse",
|
||||
filenames: []string{
|
||||
fmt.Sprintf("/%v/%v/%v/../../../file.png", th.BasicChannel.Id, th.BasicUser.Id, fileID),
|
||||
},
|
||||
channelID: th.BasicChannel.Id,
|
||||
userID: th.BasicUser.Id,
|
||||
expected: [][]string{},
|
||||
},
|
||||
{
|
||||
description: "Should only parse valid filenames",
|
||||
filenames: []string{
|
||||
fmt.Sprintf("/%v/%v/%v/../otherfile.png", th.BasicChannel.Id, th.BasicUser.Id, fileID),
|
||||
fmt.Sprintf("/%v/%v/%v/file.png", th.BasicChannel.Id, th.BasicUser.Id, fileID),
|
||||
},
|
||||
channelID: th.BasicChannel.Id,
|
||||
userID: th.BasicUser.Id,
|
||||
expected: [][]string{
|
||||
{
|
||||
th.BasicChannel.Id,
|
||||
th.BasicUser.Id,
|
||||
fileID,
|
||||
"file.png",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Valid Filename should parse",
|
||||
filenames: []string{
|
||||
fmt.Sprintf("/%v/%v/%v/file.png", th.BasicChannel.Id, th.BasicUser.Id, fileID),
|
||||
},
|
||||
channelID: th.BasicChannel.Id,
|
||||
userID: th.BasicUser.Id,
|
||||
expected: [][]string{
|
||||
{
|
||||
th.BasicChannel.Id,
|
||||
th.BasicUser.Id,
|
||||
fileID,
|
||||
"file.png",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(tt *testing.T) {
|
||||
result := parseOldFilenames(test.filenames, test.channelID, test.userID)
|
||||
require.Equal(tt, result, test.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetInfoForFilename(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
post := th.BasicPost
|
||||
teamID := th.BasicTeam.Id
|
||||
|
||||
info := th.App.getInfoForFilename(post, teamID, post.ChannelId, post.UserId, "someid", "somefile.png")
|
||||
assert.Nil(t, info, "Test non-existent file")
|
||||
}
|
||||
|
||||
func TestFindTeamIdForFilename(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
teamID := th.App.findTeamIdForFilename(th.BasicPost, "someid", "somefile.png")
|
||||
assert.Equal(t, th.BasicTeam.Id, teamID)
|
||||
|
||||
_, err := th.App.CreateTeamWithUser(th.Context, &model.Team{Email: th.BasicUser.Email, Name: "zz" + model.NewId(), DisplayName: "Joram's Test Team", Type: model.TeamOpen}, th.BasicUser.Id)
|
||||
require.Nil(t, err)
|
||||
|
||||
teamID = th.App.findTeamIdForFilename(th.BasicPost, "someid", "somefile.png")
|
||||
assert.Equal(t, "", teamID)
|
||||
}
|
||||
|
||||
func TestMigrateFilenamesToFileInfos(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
post := th.BasicPost
|
||||
infos := th.App.MigrateFilenamesToFileInfos(post)
|
||||
assert.Equal(t, 0, len(infos))
|
||||
|
||||
post.Filenames = []string{fmt.Sprintf("/%v/%v/%v/blargh.png", th.BasicChannel.Id, th.BasicUser.Id, "someid")}
|
||||
infos = th.App.MigrateFilenamesToFileInfos(post)
|
||||
assert.Equal(t, 0, len(infos))
|
||||
|
||||
path, _ := fileutils.FindDir("tests")
|
||||
file, fileErr := os.Open(filepath.Join(path, "test.png"))
|
||||
require.NoError(t, fileErr)
|
||||
defer file.Close()
|
||||
|
||||
fileID := model.NewId()
|
||||
fpath := fmt.Sprintf("/teams/%v/channels/%v/users/%v/%v/test.png", th.BasicTeam.Id, th.BasicChannel.Id, th.BasicUser.Id, fileID)
|
||||
_, err := th.App.WriteFile(file, fpath)
|
||||
require.Nil(t, err)
|
||||
rpost, err := th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser.Id, ChannelId: th.BasicChannel.Id, Filenames: []string{fmt.Sprintf("/%v/%v/%v/test.png", th.BasicChannel.Id, th.BasicUser.Id, fileID)}}, th.BasicChannel, false, true)
|
||||
require.Nil(t, err)
|
||||
|
||||
infos = th.App.MigrateFilenamesToFileInfos(rpost)
|
||||
assert.Equal(t, 1, len(infos))
|
||||
|
||||
rpost, err = th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser.Id, ChannelId: th.BasicChannel.Id, Filenames: []string{fmt.Sprintf("/%v/%v/%v/../../test.png", th.BasicChannel.Id, th.BasicUser.Id, fileID)}}, th.BasicChannel, false, true)
|
||||
require.Nil(t, err)
|
||||
|
||||
infos = th.App.MigrateFilenamesToFileInfos(rpost)
|
||||
assert.Equal(t, 0, len(infos))
|
||||
}
|
||||
|
||||
func TestCreateZipFileAndAddFiles(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
mockBackend := filesStoreMocks.FileBackend{}
|
||||
mockBackend.On("WriteFile", mock.Anything, "directory-to-heaven/zip-file-name-to-heaven.zip").Return(int64(666), errors.New("only those who dare to fail greatly can ever achieve greatly"))
|
||||
|
||||
err := th.App.CreateZipFileAndAddFiles(&mockBackend, []model.FileData{}, "zip-file-name-to-heaven.zip", "directory-to-heaven")
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, err.Error(), "only those who dare to fail greatly can ever achieve greatly")
|
||||
|
||||
mockBackend = filesStoreMocks.FileBackend{}
|
||||
mockBackend.On("WriteFile", mock.Anything, "directory-to-heaven/zip-file-name-to-heaven.zip").Return(int64(666), nil)
|
||||
err = th.App.CreateZipFileAndAddFiles(&mockBackend, []model.FileData{}, "zip-file-name-to-heaven.zip", "directory-to-heaven")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCopyFileInfos(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
teamID := model.NewId()
|
||||
channelID := model.NewId()
|
||||
userID := model.NewId()
|
||||
filename := "test"
|
||||
data := []byte("abcd")
|
||||
|
||||
info1, err := th.App.DoUploadFile(th.Context, time.Date(2007, 2, 4, 1, 2, 3, 4, time.Local), teamID, channelID, userID, filename, data)
|
||||
require.Nil(t, err)
|
||||
defer func() {
|
||||
th.App.Srv().Store().FileInfo().PermanentDelete(info1.Id)
|
||||
th.App.RemoveFile(info1.Path)
|
||||
}()
|
||||
|
||||
infoIds, err := th.App.CopyFileInfos(userID, []string{info1.Id})
|
||||
require.Nil(t, err)
|
||||
|
||||
info2, err := th.App.GetFileInfo(infoIds[0])
|
||||
require.Nil(t, err)
|
||||
defer func() {
|
||||
th.App.Srv().Store().FileInfo().PermanentDelete(info2.Id)
|
||||
th.App.RemoveFile(info2.Path)
|
||||
}()
|
||||
|
||||
assert.NotEqual(t, info1.Id, info2.Id, "should not be equal")
|
||||
assert.Equal(t, info2.PostId, "", "should be empty string")
|
||||
}
|
||||
|
||||
func TestGenerateThumbnailImage(t *testing.T) {
|
||||
t.Run("test generating thumbnail image", func(t *testing.T) {
|
||||
// given
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
img := createDummyImage()
|
||||
dataPath, _ := fileutils.FindDir("data")
|
||||
thumbnailName := "thumb.jpg"
|
||||
thumbnailPath := filepath.Join(dataPath, thumbnailName)
|
||||
|
||||
// when
|
||||
th.App.generateThumbnailImage(img, "jpg", thumbnailName)
|
||||
defer os.Remove(thumbnailPath)
|
||||
|
||||
// then
|
||||
outputImage, err := os.Stat(thumbnailPath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(957), outputImage.Size())
|
||||
})
|
||||
}
|
||||
|
||||
func createDummyImage() *image.RGBA {
|
||||
width := 200
|
||||
height := 100
|
||||
upperLeftCorner := image.Point{0, 0}
|
||||
lowerRightCorner := image.Point{width, height}
|
||||
return image.NewRGBA(image.Rectangle{upperLeftCorner, lowerRightCorner})
|
||||
}
|
||||
|
||||
func TestSearchFilesInTeamForUser(t *testing.T) {
|
||||
perPage := 5
|
||||
searchTerm := "searchTerm"
|
||||
|
||||
setup := func(t *testing.T, enableElasticsearch bool) (*TestHelper, []*model.FileInfo) {
|
||||
th := Setup(t).InitBasic()
|
||||
|
||||
fileInfos := make([]*model.FileInfo, 7)
|
||||
for i := 0; i < cap(fileInfos); i++ {
|
||||
fileInfo, err := th.App.Srv().Store().FileInfo().Save(&model.FileInfo{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
PostId: th.BasicPost.Id,
|
||||
Name: searchTerm,
|
||||
Path: searchTerm,
|
||||
Extension: "jpg",
|
||||
MimeType: "image/jpeg",
|
||||
})
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
fileInfos[i] = fileInfo
|
||||
}
|
||||
|
||||
if enableElasticsearch {
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("elastic_search"))
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ElasticsearchSettings.EnableIndexing = true
|
||||
*cfg.ElasticsearchSettings.EnableSearching = true
|
||||
})
|
||||
} else {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ElasticsearchSettings.EnableSearching = false
|
||||
})
|
||||
}
|
||||
|
||||
return th, fileInfos
|
||||
}
|
||||
|
||||
t.Run("should return everything as first page of fileInfos from database", func(t *testing.T) {
|
||||
th, fileInfos := setup(t, false)
|
||||
defer th.TearDown()
|
||||
|
||||
page := 0
|
||||
|
||||
results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierFiles)
|
||||
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, results)
|
||||
assert.Equal(t, []string{
|
||||
fileInfos[6].Id,
|
||||
fileInfos[5].Id,
|
||||
fileInfos[4].Id,
|
||||
fileInfos[3].Id,
|
||||
fileInfos[2].Id,
|
||||
fileInfos[1].Id,
|
||||
fileInfos[0].Id,
|
||||
}, results.Order)
|
||||
})
|
||||
|
||||
t.Run("should not return later pages of fileInfos from database", func(t *testing.T) {
|
||||
th, _ := setup(t, false)
|
||||
defer th.TearDown()
|
||||
|
||||
page := 1
|
||||
|
||||
results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierFiles)
|
||||
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, results)
|
||||
assert.Equal(t, []string{}, results.Order)
|
||||
})
|
||||
|
||||
t.Run("should return first page of fileInfos from ElasticSearch", func(t *testing.T) {
|
||||
th, fileInfos := setup(t, true)
|
||||
defer th.TearDown()
|
||||
|
||||
page := 0
|
||||
resultsPage := []string{
|
||||
fileInfos[6].Id,
|
||||
fileInfos[5].Id,
|
||||
fileInfos[4].Id,
|
||||
fileInfos[3].Id,
|
||||
fileInfos[2].Id,
|
||||
}
|
||||
|
||||
es := &mocks.SearchEngineInterface{}
|
||||
es.On("SearchFiles", mock.Anything, mock.Anything, page, perPage).Return(resultsPage, nil)
|
||||
es.On("Start").Return(nil).Maybe()
|
||||
es.On("IsActive").Return(true)
|
||||
es.On("IsSearchEnabled").Return(true)
|
||||
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = es
|
||||
defer func() {
|
||||
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = nil
|
||||
}()
|
||||
|
||||
results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierFiles)
|
||||
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, results)
|
||||
assert.Equal(t, resultsPage, results.Order)
|
||||
es.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("should return later pages of fileInfos from ElasticSearch", func(t *testing.T) {
|
||||
th, fileInfos := setup(t, true)
|
||||
defer th.TearDown()
|
||||
|
||||
page := 1
|
||||
resultsPage := []string{
|
||||
fileInfos[1].Id,
|
||||
fileInfos[0].Id,
|
||||
}
|
||||
|
||||
es := &mocks.SearchEngineInterface{}
|
||||
es.On("SearchFiles", mock.Anything, mock.Anything, page, perPage).Return(resultsPage, nil)
|
||||
es.On("Start").Return(nil).Maybe()
|
||||
es.On("IsActive").Return(true)
|
||||
es.On("IsSearchEnabled").Return(true)
|
||||
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = es
|
||||
defer func() {
|
||||
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = nil
|
||||
}()
|
||||
|
||||
results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierFiles)
|
||||
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, results)
|
||||
assert.Equal(t, resultsPage, results.Order)
|
||||
es.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("should fall back to database if ElasticSearch fails on first page", func(t *testing.T) {
|
||||
th, fileInfos := setup(t, true)
|
||||
defer th.TearDown()
|
||||
|
||||
page := 0
|
||||
|
||||
es := &mocks.SearchEngineInterface{}
|
||||
es.On("SearchFiles", mock.Anything, mock.Anything, page, perPage).Return(nil, &model.AppError{})
|
||||
es.On("GetName").Return("mock")
|
||||
es.On("Start").Return(nil).Maybe()
|
||||
es.On("IsActive").Return(true)
|
||||
es.On("IsSearchEnabled").Return(true)
|
||||
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = es
|
||||
defer func() {
|
||||
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = nil
|
||||
}()
|
||||
|
||||
results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierFiles)
|
||||
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, results)
|
||||
assert.Equal(t, []string{
|
||||
fileInfos[6].Id,
|
||||
fileInfos[5].Id,
|
||||
fileInfos[4].Id,
|
||||
fileInfos[3].Id,
|
||||
fileInfos[2].Id,
|
||||
fileInfos[1].Id,
|
||||
fileInfos[0].Id,
|
||||
}, results.Order)
|
||||
es.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("should return nothing if ElasticSearch fails on later pages", func(t *testing.T) {
|
||||
th, _ := setup(t, true)
|
||||
defer th.TearDown()
|
||||
|
||||
page := 1
|
||||
|
||||
es := &mocks.SearchEngineInterface{}
|
||||
es.On("SearchFiles", mock.Anything, mock.Anything, page, perPage).Return(nil, &model.AppError{})
|
||||
es.On("GetName").Return("mock")
|
||||
es.On("Start").Return(nil).Maybe()
|
||||
es.On("IsActive").Return(true)
|
||||
es.On("IsSearchEnabled").Return(true)
|
||||
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = es
|
||||
defer func() {
|
||||
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = nil
|
||||
}()
|
||||
|
||||
results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierFiles)
|
||||
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, []string{}, results.Order)
|
||||
es.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExtractContentFromFileInfo(t *testing.T) {
|
||||
app := &App{}
|
||||
fi := &model.FileInfo{
|
||||
MimeType: "image/jpeg",
|
||||
}
|
||||
|
||||
// Test that we don't process images.
|
||||
require.NoError(t, app.ExtractContentFromFileInfo(fi))
|
||||
}
|
||||
|
||||
func TestGetLastAccessibleFileTime(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
|
||||
r, err := th.App.GetLastAccessibleFileTime()
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, int64(0), r)
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
mockStore := th.App.Srv().Store().(*storemocks.Store)
|
||||
|
||||
mockSystemStore := storemocks.SystemStore{}
|
||||
mockStore.On("System").Return(&mockSystemStore)
|
||||
mockSystemStore.On("GetByName", mock.Anything).Return(nil, store.NewErrNotFound("", ""))
|
||||
r, err = th.App.GetLastAccessibleFileTime()
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, int64(0), r)
|
||||
|
||||
mockSystemStore = storemocks.SystemStore{}
|
||||
mockStore.On("System").Return(&mockSystemStore)
|
||||
mockSystemStore.On("GetByName", mock.Anything).Return(nil, errors.New("test"))
|
||||
_, err = th.App.GetLastAccessibleFileTime()
|
||||
require.NotNil(t, err)
|
||||
|
||||
mockSystemStore = storemocks.SystemStore{}
|
||||
mockStore.On("System").Return(&mockSystemStore)
|
||||
mockSystemStore.On("GetByName", mock.Anything).Return(&model.System{Name: model.SystemLastAccessibleFileTime, Value: "10"}, nil)
|
||||
r, err = th.App.GetLastAccessibleFileTime()
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, int64(10), r)
|
||||
}
|
||||
|
||||
func TestComputeLastAccessibleFileTime(t *testing.T) {
|
||||
t.Run("Updates the time, if cloud limit is applicable", func(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
cloud := &eMocks.CloudInterface{}
|
||||
th.App.Srv().Cloud = cloud
|
||||
|
||||
cloud.Mock.On("GetCloudLimits", mock.Anything).Return(&model.ProductLimits{
|
||||
Files: &model.FilesLimits{
|
||||
TotalStorage: model.NewInt64(1),
|
||||
},
|
||||
}, nil)
|
||||
|
||||
mockStore := th.App.Srv().Store().(*storemocks.Store)
|
||||
mockFileStore := storemocks.FileInfoStore{}
|
||||
mockFileStore.On("GetUptoNSizeFileTime", mock.Anything).Return(int64(1), nil)
|
||||
mockSystemStore := storemocks.SystemStore{}
|
||||
mockSystemStore.On("SaveOrUpdate", mock.Anything).Return(nil)
|
||||
mockStore.On("FileInfo").Return(&mockFileStore)
|
||||
mockStore.On("System").Return(&mockSystemStore)
|
||||
|
||||
err := th.App.ComputeLastAccessibleFileTime()
|
||||
require.NoError(t, err)
|
||||
|
||||
mockSystemStore.AssertCalled(t, "SaveOrUpdate", mock.Anything)
|
||||
})
|
||||
|
||||
t.Run("Removes the time, if cloud limit is not applicable", func(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
cloud := &eMocks.CloudInterface{}
|
||||
th.App.Srv().Cloud = cloud
|
||||
|
||||
cloud.Mock.On("GetCloudLimits", mock.Anything).Return(nil, nil)
|
||||
|
||||
mockStore := th.App.Srv().Store().(*storemocks.Store)
|
||||
mockFileStore := storemocks.FileInfoStore{}
|
||||
mockFileStore.On("GetUptoNSizeFileTime", mock.Anything).Return(int64(1), nil)
|
||||
mockSystemStore := storemocks.SystemStore{}
|
||||
mockSystemStore.On("GetByName", mock.Anything).Return(&model.System{Name: model.SystemLastAccessibleFileTime, Value: "10"}, nil)
|
||||
mockSystemStore.On("PermanentDeleteByName", mock.Anything).Return(nil, nil)
|
||||
mockSystemStore.On("SaveOrUpdate", mock.Anything).Return(nil)
|
||||
mockStore.On("FileInfo").Return(&mockFileStore)
|
||||
mockStore.On("System").Return(&mockSystemStore)
|
||||
|
||||
err := th.App.ComputeLastAccessibleFileTime()
|
||||
require.NoError(t, err)
|
||||
|
||||
mockSystemStore.AssertNotCalled(t, "SaveOrUpdate", mock.Anything)
|
||||
mockSystemStore.AssertCalled(t, "PermanentDeleteByName", mock.Anything)
|
||||
|
||||
})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue