Merge: Complete Mattermost Server with Community Enterprise
Full Mattermost server source with integrated Community Enterprise features. Includes vendor directory for offline/air-gapped builds. Structure: - enterprise-impl/: Enterprise feature implementations - enterprise-community/: Init files that register implementations - enterprise/: Bridge imports (community_imports.go) - vendor/: All dependencies for offline builds Build (online): go build ./cmd/mattermost Build (offline/air-gapped): go build -mod=vendor ./cmd/mattermost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
fad2fe9d3c
commit
ec1f89217a
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
# Binaries
|
||||
mattermost
|
||||
*.exe
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Local config
|
||||
config/config.json
|
||||
860
Makefile
Normal file
860
Makefile
Normal file
@ -0,0 +1,860 @@
|
||||
.PHONY: build package run stop run-client run-server run-node run-haserver stop-haserver stop-client stop-server restart restart-server restart-client restart-haserver start-docker update-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 test-mmctl-unit test-mmctl-e2e test-mmctl test-mmctl-coverage mmctl-build mmctl-docs new-migration migrations-extract test-public mocks-public
|
||||
|
||||
ROOT := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
|
||||
ifeq ($(OS),Windows_NT)
|
||||
PLATFORM := Windows
|
||||
else
|
||||
PLATFORM := $(shell uname)
|
||||
endif
|
||||
|
||||
# Detect Apple Silicon and set a flag.
|
||||
ifeq ($(shell uname)/$(shell uname -m),Darwin/arm64)
|
||||
ARM_BASED_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)
|
||||
BUILD_TAGS =
|
||||
|
||||
|
||||
# Docker
|
||||
export COMPOSE_PROJECT_NAME=mattermost-server
|
||||
|
||||
# If we don't set the build number it defaults to dev
|
||||
ifeq ($(BUILD_NUMBER),)
|
||||
BUILD_DATE := n/a
|
||||
BUILD_NUMBER := dev
|
||||
endif
|
||||
|
||||
# Go test sum configuration
|
||||
GOTESTSUM_FORMAT ?= testname
|
||||
GOTESTSUM_JUNITFILE ?= report.xml
|
||||
GOTESTSUM_JSONFILE ?= gotestsum.json
|
||||
|
||||
# Go test coverage
|
||||
ENABLE_COVERAGE ?= false
|
||||
ifeq ($(ENABLE_COVERAGE),true)
|
||||
COVERAGE_FLAG = -coverprofile=cover.out -covermode=atomic
|
||||
endif
|
||||
|
||||
# mmctl
|
||||
MMCTL_BUILD_TAGS =
|
||||
MMCTL_TESTFLAGS ?= -timeout 30m
|
||||
MMCTL_PKG = github.com/mattermost/mattermost/server/v8/cmd/mmctl/commands
|
||||
MMCTL_BUILD_DATE = $(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
MMCTL_LDFLAGS += -X "$(MMCTL_PKG).buildDate=$(MMCTL_BUILD_DATE)"
|
||||
|
||||
# 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)/.),)
|
||||
MMCTL_TESTFLAGS += -ldflags '-X "$(MMCTL_PKG).EnableEnterpriseTests=true" -X "github.com/mattermost/mattermost/server/public/model.BuildEnterpriseReady=true"'
|
||||
MMCTL_BUILD_TAGS += enterprise
|
||||
|
||||
ifeq ($(BUILD_ENTERPRISE),true)
|
||||
BUILD_ENTERPRISE_READY = true
|
||||
BUILD_TYPE_NAME = enterprise
|
||||
BUILD_HASH_ENTERPRISE = $(shell cd $(BUILD_ENTERPRISE_DIR) && git rev-parse HEAD)
|
||||
BUILD_TAGS += enterprise
|
||||
else
|
||||
BUILD_ENTERPRISE_READY = false
|
||||
BUILD_TYPE_NAME = team
|
||||
endif
|
||||
else
|
||||
BUILD_ENTERPRISE_READY = false
|
||||
BUILD_TYPE_NAME = team
|
||||
endif
|
||||
|
||||
FIPS_ENABLED ?= false
|
||||
ifeq ($(FIPS_ENABLED),true)
|
||||
BUILD_TYPE_NAME := $(BUILD_TYPE_NAME)-fips
|
||||
endif
|
||||
|
||||
# Clean up the old means of importing enterprise source, if it exists
|
||||
ifneq ($(wildcard channels/imports/imports.go),)
|
||||
IGNORE := $(shell rm -f channels/imports/imports.go)
|
||||
endif
|
||||
|
||||
# Source available, already included with enterprise but also available during development.
|
||||
ifeq ($(BUILD_NUMBER),dev)
|
||||
BUILD_TAGS += sourceavailable
|
||||
endif
|
||||
|
||||
# Webapp
|
||||
BUILD_WEBAPP_DIR ?= ../webapp
|
||||
|
||||
# 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
|
||||
|
||||
ifeq ($(FIPS_ENABLED),true)
|
||||
BUILD_TAGS += requirefips
|
||||
endif
|
||||
|
||||
DELVE ?= dlv
|
||||
LDFLAGS += -X "github.com/mattermost/mattermost/server/public/model.BuildNumber=$(BUILD_NUMBER)"
|
||||
LDFLAGS += -X "github.com/mattermost/mattermost/server/public/model.BuildDate=$(BUILD_DATE)"
|
||||
LDFLAGS += -X "github.com/mattermost/mattermost/server/public/model.BuildHash=$(BUILD_HASH)"
|
||||
LDFLAGS += -X "github.com/mattermost/mattermost/server/public/model.BuildHashEnterprise=$(BUILD_HASH_ENTERPRISE)"
|
||||
LDFLAGS += -X "github.com/mattermost/mattermost/server/public/model.BuildEnterpriseReady=$(BUILD_ENTERPRISE_READY)"
|
||||
|
||||
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)
|
||||
GO_COMPATIBILITY_TEST_VERSIONS := 1.22.7 1.23.6
|
||||
|
||||
# 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)/darwin_amd64/mattermost
|
||||
DIST_PATH_OSX_ARM64=$(DIST_ROOT)/darwin_arm64/mattermost
|
||||
DIST_PATH_WIN=$(DIST_ROOT)/windows/mattermost
|
||||
|
||||
# Packages lists
|
||||
TE_PACKAGES=$(shell $(GO) list ./public/...) $(shell $(GO) list ./... | grep -vE 'server/v8/cmd/mmctl')
|
||||
MMCTL_PACKAGES=$(shell $(GO) list ./... | grep -E 'server/v8/cmd/mmctl')
|
||||
|
||||
TEMPLATES_DIR=templates
|
||||
|
||||
# Plugins Packages
|
||||
PLUGIN_PACKAGES ?= $(PLUGIN_PACKAGES:)
|
||||
PLUGIN_PACKAGES += mattermost-plugin-calls-v1.11.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-github-v2.5.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.11.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-jira-v4.4.1
|
||||
PLUGIN_PACKAGES += mattermost-plugin-playbooks-v2.6.1
|
||||
PLUGIN_PACKAGES += mattermost-plugin-servicenow-v2.4.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-zoom-v1.10.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-agents-v1.6.3
|
||||
PLUGIN_PACKAGES += mattermost-plugin-boards-v9.2.1
|
||||
PLUGIN_PACKAGES += mattermost-plugin-user-survey-v1.1.1
|
||||
PLUGIN_PACKAGES += mattermost-plugin-mscalendar-v1.4.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-msteams-meetings-v2.3.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-metrics-v0.7.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-channel-export-v1.2.1
|
||||
|
||||
# Overwrite the definition of PLUGIN_PACKAGES with the list of FIPS-ready plugins
|
||||
# Note that the '+' in the file name is encoded as %2B for the URL we use to
|
||||
# download the package from to work. This will no longer be needed when we unify
|
||||
# the way we pre-package FIPS and non-FIPS plugins.
|
||||
ifeq ($(FIPS_ENABLED),true)
|
||||
PLUGIN_PACKAGES = mattermost-plugin-playbooks-v2.6.1%2B0e01d28-fips
|
||||
PLUGIN_PACKAGES += mattermost-plugin-agents-v1.6.3%2B4acccc8-fips
|
||||
PLUGIN_PACKAGES += mattermost-plugin-boards-v9.2.1%2Bdf49b26-fips
|
||||
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
|
||||
|
||||
CONFIG_FILE_PATH ?= ./config/config.json
|
||||
|
||||
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
|
||||
include public/Makefile
|
||||
|
||||
LDFLAGS += -X "github.com/mattermost/mattermost/server/public/model.MockCWS=$(MM_ENABLE_CWS_MOCK)"
|
||||
LDFLAGS += -X "github.com/mattermost/mattermost/server/public/model.MattermostGiphySdkKey=$(MM_GIPHY_SDK_KEY)"
|
||||
|
||||
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 ($(ARM_BASED_MAC),true)
|
||||
$(info Apple Silicon detected, applying elasticsearch override)
|
||||
DOCKER_COMPOSE_OVERRIDE := -f docker-compose.makefile.m1.yml $(DOCKER_COMPOSE_OVERRIDE)
|
||||
endif
|
||||
|
||||
# Support for pgvector postgres image
|
||||
MM_USE_PGVECTOR ?= false
|
||||
ifeq ($(MM_USE_PGVECTOR),true)
|
||||
$(info Using pgvector/pgvector image for PostgreSQL)
|
||||
DOCKER_COMPOSE_OVERRIDE := -f docker-compose.pgvector.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
|
||||
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
|
||||
ifeq ($(MM_USE_PGVECTOR),true)
|
||||
@echo Using pgvector PostgreSQL image
|
||||
endif
|
||||
|
||||
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/custom-schema-objectID.ldif | docker compose -f docker-compose.makefile.yml $(DOCKER_COMPOSE_OVERRIDE) exec -T openldap bash -c 'ldapadd -Y EXTERNAL -H ldapi:/// -w mostest || true';
|
||||
cat tests/custom-schema-cpa.ldif | docker compose -f docker-compose.makefile.yml ${DOCKER_COMPOSE_OVERRIDE} exec -T openldap bash -c 'ldapadd -Y EXTERNAL -H ldapi:/// -w mostest || true';
|
||||
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
|
||||
endif
|
||||
|
||||
update-docker: stop-docker ## Updates the docker containers for local development.
|
||||
@echo Updating docker containers
|
||||
|
||||
$(GO) run ./build/docker-compose-generator/main.go $(ENABLED_DOCKER_SERVICES) | docker compose -f docker-compose.makefile.yml -f /dev/stdin $(DOCKER_COMPOSE_OVERRIDE) up --no-start
|
||||
|
||||
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
|
||||
|
||||
docker volume rm mattermost-server_postgres-14-data || true
|
||||
endif
|
||||
|
||||
plugin-checker:
|
||||
$(GO) run $(GOFLAGS) ./public/plugin/checker
|
||||
|
||||
prepackaged-plugins: ## Populate the prepackaged-plugins directory.
|
||||
@echo Downloading prepackaged plugins: $(PLUGIN_PACKAGES)
|
||||
mkdir -p prepackaged_plugins
|
||||
@cd prepackaged_plugins && for plugin_package in $(PLUGIN_PACKAGES) ; do \
|
||||
curl -f -O -L https://plugins.releases.mattermost.com/release/$$plugin_package.tar.gz; \
|
||||
curl -f -O -L https://plugins.releases.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 compiling."
|
||||
else
|
||||
$(MAKE) mmctl-build
|
||||
endif
|
||||
|
||||
golang-versions: ## Install Golang versions used for compatibility testing (e.g. plugins)
|
||||
@for version in $(GO_COMPATIBILITY_TEST_VERSIONS); do \
|
||||
$(GO) install golang.org/dl/go$$version@latest && \
|
||||
$(GOBIN)/go$$version download; \
|
||||
done
|
||||
export GO_COMPATIBILITY_TEST_VERSIONS="${GO_COMPATIBILITY_TEST_VERSIONS}"
|
||||
|
||||
golangci-lint: ## Run golangci-lint on codebase
|
||||
$(GO) install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.6.0
|
||||
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
||||
$(GOBIN)/golangci-lint run ./... ./public/... $(BUILD_ENTERPRISE_DIR)/...
|
||||
else
|
||||
$(GOBIN)/golangci-lint run ./... ./public/...
|
||||
endif
|
||||
|
||||
i18n-extract: ## Extract strings for translation from the source code
|
||||
cd ../tools/mmgotool && $(GO) install .
|
||||
$(GOBIN)/mmgotool i18n extract --portal-dir=""
|
||||
|
||||
i18n-check: ## Exit on empty translation strings and translation source strings
|
||||
cd ../tools/mmgotool && $(GO) install .
|
||||
$(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.53.4
|
||||
$(GOBIN)/mockery --config channels/store/.mockery.yaml
|
||||
|
||||
cache-mocks:
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.53.4
|
||||
$(GOBIN)/mockery --config platform/services/cache/.mockery.yaml
|
||||
|
||||
|
||||
store-layers: ## Generate layers for the store
|
||||
$(GO) generate $(GOFLAGS) ./channels/store
|
||||
|
||||
new-migration: ## Creates a new migration. Run with make new-migration name=<>
|
||||
$(GO) install github.com/mattermost/morph/cmd/morph@1e0640c
|
||||
@echo "Generating new migration for postgres"
|
||||
$(GOBIN)/morph new script $(name) --driver postgres --dir channels/db/migrations --sequence
|
||||
|
||||
filestore-mocks: ## Creates mock files.
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.53.4
|
||||
$(GOBIN)/mockery --config platform/shared/filestore/.mockery.yaml
|
||||
|
||||
ldap-mocks: ## Creates mock files for ldap.
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.53.4
|
||||
$(GOBIN)/mockery --dir $(BUILD_ENTERPRISE_DIR)/ldap --all --inpackage --note 'Regenerate this file using `make ldap-mocks`.'
|
||||
|
||||
plugin-mocks: ## Creates mock files for plugins.
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.53.4
|
||||
$(GOBIN)/mockery --config public/plugin/.mockery.yaml
|
||||
|
||||
einterfaces-mocks: ## Creates mock files for einterfaces.
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.53.4
|
||||
$(GOBIN)/mockery --config einterfaces/.mockery.yaml
|
||||
|
||||
searchengine-mocks: ## Creates mock files for searchengines.
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.53.4
|
||||
$(GOBIN)/mockery --config platform/services/searchengine/.mockery.yaml
|
||||
|
||||
sharedchannel-mocks: ## Creates mock files for shared channels.
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.53.4
|
||||
$(GOBIN)/mockery --config platform/services/sharedchannel/.mockery.yaml
|
||||
|
||||
misc-mocks: ## Creates mocks for misc interfaces.
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.53.4
|
||||
$(GOBIN)/mockery --config channels/utils/.mockery.yaml
|
||||
|
||||
email-mocks: ## Creates mocks for misc interfaces.
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.53.4
|
||||
$(GOBIN)/mockery --config channels/app/email/.mockery.yaml
|
||||
|
||||
platform-mocks: ## Creates mocks for platform interfaces.
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.53.4
|
||||
$(GOBIN)/mockery --config channels/app/platform/.mockery.yaml
|
||||
|
||||
mmctl-mocks: ## Creates mocks for mmctl
|
||||
$(GO) install github.com/golang/mock/mockgen@v1.6.0
|
||||
$(GOBIN)/mockgen -destination=cmd/mmctl/mocks/client_mock.go -copyright_file=cmd/mmctl/mocks/copyright.txt -package=mocks github.com/mattermost/mattermost/server/v8/cmd/mmctl/client Client
|
||||
|
||||
pluginapi: ## Generates api and hooks glue code for plugins
|
||||
cd ./public && $(GO) generate $(GOFLAGS) ./plugin
|
||||
|
||||
mocks: store-mocks filestore-mocks ldap-mocks plugin-mocks einterfaces-mocks searchengine-mocks sharedchannel-mocks misc-mocks email-mocks platform-mocks mmctl-mocks mocks-public cache-mocks
|
||||
|
||||
layers: store-layers pluginapi
|
||||
|
||||
generated: mocks layers
|
||||
|
||||
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: ## Sets up your go.work file
|
||||
ifneq ($(IGNORE_GO_WORK_IF_EXISTS),true)
|
||||
@echo "Creating a go.work file"
|
||||
rm -f go.work
|
||||
$(GO) work init
|
||||
$(GO) work use .
|
||||
$(GO) work use ./public
|
||||
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
||||
$(GO) work use ../../enterprise
|
||||
endif
|
||||
endif
|
||||
|
||||
check-style: plugin-checker vet golangci-lint ## Runs style/lint checks
|
||||
|
||||
gotestsum:
|
||||
$(GO) install gotest.tools/gotestsum@v1.11.0
|
||||
|
||||
test-compile: gotestsum ## Compile tests.
|
||||
@echo COMPILE TESTS
|
||||
|
||||
for package in $(TE_PACKAGES) $(EE_PACKAGES); do \
|
||||
$(GO) test $(GOFLAGS) -c $$package; \
|
||||
done
|
||||
|
||||
modules-tidy: ## Tidy Go modules
|
||||
mv enterprise/external_imports.go enterprise/external_imports.go.orig
|
||||
-$(GO) mod tidy
|
||||
-cd public && $(GO) mod tidy
|
||||
mv enterprise/external_imports.go.orig enterprise/external_imports.go
|
||||
|
||||
test-server-pre: check-prereqs-enterprise start-docker gotestsum golang-versions ## Runs tests.
|
||||
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
||||
@echo Running all tests
|
||||
else
|
||||
@echo Running only TE tests
|
||||
endif
|
||||
|
||||
test-server-race: export GOTESTSUM_FORMAT := $(GOTESTSUM_FORMAT)
|
||||
test-server-race: export GOTESTSUM_JUNITFILE := $(GOTESTSUM_JUNITFILE)
|
||||
test-server-race: export GOTESTSUM_JSONFILE := $(GOTESTSUM_JSONFILE)
|
||||
test-server-race: test-server-pre
|
||||
ifeq ($(IS_CI),true)
|
||||
GOMAXPROCS=4 $(GOBIN)/gotestsum --rerun-fails=3 --packages="$(TE_PACKAGES) $(EE_PACKAGES)" -- -race $(GOFLAGS) -timeout=2h
|
||||
else
|
||||
$(GOBIN)/gotestsum --rerun-fails=3 --packages="$(TE_PACKAGES) $(EE_PACKAGES)" -- -race $(GOFLAGS) -timeout=2h
|
||||
endif
|
||||
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: export GOTESTSUM_FORMAT := $(GOTESTSUM_FORMAT)
|
||||
test-server: export GOTESTSUM_JUNITFILE := $(GOTESTSUM_JUNITFILE)
|
||||
test-server: export GOTESTSUM_JSONFILE := $(GOTESTSUM_JSONFILE)
|
||||
test-server: test-server-pre
|
||||
$(GOBIN)/gotestsum --rerun-fails=3 --packages="$(TE_PACKAGES) $(EE_PACKAGES)" -- $(GOFLAGS) -timeout=90m $(COVERAGE_FLAG)
|
||||
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: export GOTESTSUM_FORMAT := $(GOTESTSUM_FORMAT)
|
||||
test-server-ee: export GOTESTSUM_JUNITFILE := $(GOTESTSUM_JUNITFILE)
|
||||
test-server-ee: export GOTESTSUM_JSONFILE := $(GOTESTSUM_JSONFILE)
|
||||
test-server-ee: check-prereqs-enterprise start-docker gotestsum ## Runs EE tests.
|
||||
@echo Running only EE tests
|
||||
$(GOBIN)/gotestsum --packages="$(EE_PACKAGES)" -- $(GOFLAGS) -timeout=20m
|
||||
|
||||
test-server-quick: export GOTESTSUM_FORMAT := $(GOTESTSUM_FORMAT)
|
||||
test-server-quick: export GOTESTSUM_JUNITFILE := $(GOTESTSUM_JUNITFILE)
|
||||
test-server-quick: export GOTESTSUM_JSONFILE := $(GOTESTSUM_JSONFILE)
|
||||
test-server-quick: check-prereqs-enterprise ## Runs only quick tests.
|
||||
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
||||
@echo Running all tests
|
||||
$(GOBIN)/gotestsum --packages="$(TE_PACKAGES) $(EE_PACKAGES)" -- $(GOFLAGS) -short
|
||||
else
|
||||
@echo Running only TE tests
|
||||
$(GOBIN)/gotestsum --packages="$(TE_PACKAGES)" -- $(GOFLAGS) -short
|
||||
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: export MM_SERVICESETTINGS_ENABLELOCALMODE := true
|
||||
test-data: run-server inject-test-data stop-server ## 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-server; \
|
||||
fi
|
||||
|
||||
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 ========================================================================
|
||||
|
||||
test-mmctl-unit: export GOTESTSUM_FORMAT := $(GOTESTSUM_FORMAT)
|
||||
test-mmctl-unit: export GOTESTSUM_JUNITFILE := $(GOTESTSUM_JUNITFILE)
|
||||
test-mmctl-unit: export GOTESTSUM_JSONFILE := $(GOTESTSUM_JSONFILE)
|
||||
test-mmctl-unit: check-prereqs-enterprise gotestsum
|
||||
@echo Running mmctl unit tests
|
||||
$(GOBIN)/gotestsum --packages="$(MMCTL_PACKAGES)" -- -tags 'unit $(MMCTL_BUILD_TAGS)' $(MMCTL_TESTFLAGS)
|
||||
|
||||
test-mmctl-e2e: export GOTESTSUM_FORMAT := $(GOTESTSUM_FORMAT)
|
||||
test-mmctl-e2e: export GOTESTSUM_JUNITFILE := $(GOTESTSUM_JUNITFILE)
|
||||
test-mmctl-e2e: export GOTESTSUM_JSONFILE := $(GOTESTSUM_JSONFILE)
|
||||
test-mmctl-e2e: check-prereqs-enterprise gotestsum start-docker
|
||||
@echo Running mmctl e2e tests
|
||||
$(GOBIN)/gotestsum --packages="$(MMCTL_PACKAGES)" -- -tags 'e2e $(MMCTL_BUILD_TAGS)' $(MMCTL_TESTFLAGS)
|
||||
|
||||
test-mmctl: export GOTESTSUM_FORMAT := $(GOTESTSUM_FORMAT)
|
||||
test-mmctl: export GOTESTSUM_JUNITFILE := $(GOTESTSUM_JUNITFILE)
|
||||
test-mmctl: export GOTESTSUM_JSONFILE := $(GOTESTSUM_JSONFILE)
|
||||
test-mmctl: check-prereqs-enterprise gotestsum start-docker
|
||||
@echo Running all mmctl tests
|
||||
$(GOBIN)/gotestsum --packages="$(MMCTL_PACKAGES)" -- -tags 'unit e2e $(MMCTL_BUILD_TAGS)' $(MMCTL_TESTFLAGS)
|
||||
|
||||
test-mmctl-coverage: export GOTESTSUM_FORMAT := $(GOTESTSUM_FORMAT)
|
||||
test-mmctl-coverage: export GOTESTSUM_JUNITFILE := $(GOTESTSUM_JUNITFILE)
|
||||
test-mmctl-coverage: export GOTESTSUM_JSONFILE := $(GOTESTSUM_JSONFILE)
|
||||
test-mmctl-coverage: check-prereqs-enterprise gotestsum start-docker
|
||||
@echo Running all mmctl tests with coverage
|
||||
$(GOBIN)/gotestsum --packages="$(MMCTL_PACKAGES)" -- -tags 'unit e2e $(MMCTL_BUILD_TAGS)' -coverprofile=mmctlcover.out $(MMCTL_TESTFLAGS)
|
||||
$(GO) tool cover -html=mmctlcover.out
|
||||
|
||||
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 client ## Starts the server.
|
||||
@echo Running mattermost for development
|
||||
ifeq ($(MM_USE_PGVECTOR),true)
|
||||
@echo With pgvector PostgreSQL support enabled
|
||||
endif
|
||||
|
||||
mkdir -p $(BUILD_WEBAPP_DIR)/channels/dist/files
|
||||
$(GO) run $(GOFLAGS) -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' $(PLATFORM_FILES) $(RUN_IN_BACKGROUND)
|
||||
|
||||
run-server-pgvector: ## Starts the server with pgvector PostgreSQL image.
|
||||
@MM_USE_PGVECTOR=true $(MAKE) run-server
|
||||
|
||||
debug-server: start-docker ## Compile and start server using delve.
|
||||
mkdir -p $(BUILD_WEBAPP_DIR)/channels/dist/files
|
||||
$(DELVE) debug $(PLATFORM_FILES) --build-flags="-ldflags '\
|
||||
-X github.com/mattermost/mattermost/server/public/model.BuildNumber=$(BUILD_NUMBER)\
|
||||
-X \"github.com/mattermost/mattermost/server/public/model.BuildDate=$(BUILD_DATE)\"\
|
||||
-X github.com/mattermost/mattermost/server/public/model.BuildHash=$(BUILD_HASH)\
|
||||
-X github.com/mattermost/mattermost/server/public/model.BuildHashEnterprise=$(BUILD_HASH_ENTERPRISE)\
|
||||
-X github.com/mattermost/mattermost/server/public/model.BuildEnterpriseReady=$(BUILD_ENTERPRISE_READY)'\
|
||||
-tags '$(BUILD_TAGS)'"
|
||||
|
||||
debug-server-headless: start-docker ## Debug server from within an IDE like VSCode or IntelliJ.
|
||||
mkdir -p $(BUILD_WEBAPP_DIR)/channels/dist/files
|
||||
$(DELVE) debug --headless --listen=:2345 --api-version=2 --accept-multiclient $(PLATFORM_FILES) --build-flags="-ldflags '\
|
||||
-X github.com/mattermost/mattermost/server/public/model.BuildNumber=$(BUILD_NUMBER)\
|
||||
-X \"github.com/mattermost/mattermost/server/public/model.BuildDate=$(BUILD_DATE)\"\
|
||||
-X github.com/mattermost/mattermost/server/public/model.BuildHash=$(BUILD_HASH)\
|
||||
-X github.com/mattermost/mattermost/server/public/model.BuildHashEnterprise=$(BUILD_HASH_ENTERPRISE)\
|
||||
-X github.com/mattermost/mattermost/server/public/model.BuildEnterpriseReady=$(BUILD_ENTERPRISE_READY)'\
|
||||
-tags '$(BUILD_TAGS)'"
|
||||
|
||||
run-node: export MM_SERVICESETTINGS_SITEURL=http://localhost:8066
|
||||
run-node: export MM_SERVICESETTINGS_LISTENADDRESS=:8066
|
||||
run-node: export MM_SERVICESETTINGS_ENABLELOCALMODE=true
|
||||
run-node: export MM_SERVICESETTINGS_LOCALMODESOCKETLOCATION=/var/tmp/mattermost_local_node.socket
|
||||
run-node: export MM_SQLSETTINGS_DRIVERNAME=postgres
|
||||
run-node: export MM_SQLSETTINGS_DATASOURCE=postgres://mmuser:mostest@localhost/mattermost_node_test?sslmode=disable&sslmode=disable&connect_timeout=10&binary_parameters=yes
|
||||
|
||||
run-node: start-docker ## Runs a shared channel node.
|
||||
@echo Running mattermost node
|
||||
|
||||
$(GO) run $(GOFLAGS) -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' $(PLATFORM_FILES) $(RUN_IN_BACKGROUND)
|
||||
|
||||
run-cli: start-docker ## Runs CLI.
|
||||
@echo Running mattermost for development
|
||||
@echo Example should be like 'make ARGS="-version" run-cli'
|
||||
|
||||
$(GO) run $(GOFLAGS) -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' $(PLATFORM_FILES) ${ARGS}
|
||||
|
||||
run-client: client ## Runs the webapp.
|
||||
@echo Running mattermost client for development
|
||||
|
||||
cd $(BUILD_WEBAPP_DIR) && $(MAKE) run
|
||||
|
||||
client: ## Sets up a symlink to the compiled files generated by the web app
|
||||
@echo Setting up symlink to client directory
|
||||
ln -nfs $(BUILD_WEBAPP_DIR)/channels/dist client
|
||||
|
||||
run-client-fullmap: client ## Legacy alias to run-client
|
||||
@echo Running mattermost client for development
|
||||
|
||||
cd $(BUILD_WEBAPP_DIR) && $(MAKE) run
|
||||
|
||||
run: run-server run-client ## Runs the server and webapp.
|
||||
|
||||
run-pgvector: ## Runs the server and webapp with pgvector PostgreSQL image.
|
||||
@MM_USE_PGVECTOR=true $(MAKE) run
|
||||
|
||||
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-server-pgvector: ## Restarts the server with pgvector PostgreSQL image.
|
||||
@MM_USE_PGVECTOR=true $(MAKE) restart-server
|
||||
|
||||
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) -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' $(PLATFORM_FILES) jobserver &
|
||||
|
||||
config-ldap: ## Configures LDAP.
|
||||
@echo Setting up configuration for local LDAP
|
||||
|
||||
# Check if jq is installed
|
||||
@jq --version > /dev/null 2>&1 || (echo "jq is not installed. Please install jq to continue." && exit 1)
|
||||
|
||||
$(eval TMPDIR := $(shell mktemp -d))
|
||||
jq --slurp '.[0] * .[1]' ${CONFIG_FILE_PATH} build/docker/keycloak/ldap.mmsettings.json > ${TMPDIR}/config.json
|
||||
cp ${TMPDIR}/config.json ${CONFIG_FILE_PATH}
|
||||
rm ${TMPDIR}/config.json
|
||||
|
||||
config-saml: ## Configures SAML.
|
||||
@echo Setting up configuration for local SAML with keycloak, please ensure your keycloak is running on http://localhost:8484
|
||||
|
||||
# Check if jq is installed
|
||||
@jq --version > /dev/null 2>&1 || (echo "jq is not installed. Please install jq to continue." && exit 1)
|
||||
|
||||
@cp build/docker/keycloak/keycloak.crt ./config/saml-idp.crt
|
||||
|
||||
$(eval TMPDIR := $(shell mktemp -d))
|
||||
jq --slurp '.[0] * .[1]' ${CONFIG_FILE_PATH} build/docker/keycloak/saml.mmsettings.json > ${TMPDIR}/config.json
|
||||
cp ${TMPDIR}/config.json ${CONFIG_FILE_PATH}
|
||||
rm ${TMPDIR}/config.json
|
||||
|
||||
config-openid: ## Configures OpenID.
|
||||
@echo Setting up configuration for local OpenID with keycloak, please ensure your keycloak is running on http://localhost:8484
|
||||
|
||||
# Check if jq is installed
|
||||
@jq --version > /dev/null 2>&1 || (echo "jq is not installed. Please install jq to continue." && exit 1)
|
||||
|
||||
$(eval TMPDIR := $(shell mktemp -d))
|
||||
jq --slurp '.[0] * .[1]' ${CONFIG_FILE_PATH} build/docker/keycloak/openid.mmsettings.json > ${TMPDIR}/config.json
|
||||
cp ${TMPDIR}/config.json ${CONFIG_FILE_PATH}
|
||||
rm ${TMPDIR}/config.json
|
||||
|
||||
@echo Finished setting up configuration for local OpenID with keycloak
|
||||
|
||||
config-reset: ## Resets the config/config.json file to the default production values.
|
||||
@echo Resetting configuration to production default
|
||||
rm -f config/config.json
|
||||
OUTPUT_CONFIG=$(PWD)/config/config.json $(GO) run $(GOFLAGS) -tags production ./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 cover.out
|
||||
rm -f ecover.out
|
||||
rm -f *.out
|
||||
rm -f *.test
|
||||
rm -f channels/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 external_imports.go
|
||||
mv enterprise/external_imports.go enterprise/external_imports.go.orig
|
||||
endif
|
||||
|
||||
# Update all dependencies (does not update across major versions)
|
||||
$(GO) get -u ./...
|
||||
|
||||
# Tidy up
|
||||
$(GO) mod tidy
|
||||
|
||||
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
||||
mv enterprise/external_imports.go.orig enterprise/external_imports.go
|
||||
endif
|
||||
|
||||
vet: ## Run mattermost go vet specific checks
|
||||
$(GO) install github.com/mattermost/mattermost-govet/v2@7d8db289e508999dfcac47b97c9490a0fec12d66
|
||||
$(GO) vet -vettool=$(GOBIN)/mattermost-govet -structuredLogging -inconsistentReceiverName -emptyStrCmp -tFatal -configtelemetry -errorAssertions -requestCtxNaming -license -inconsistentReceiverName.ignore=session_serial_gen.go,team_member_serial_gen.go,user_serial_gen.go,utils_serial_gen.go ./...
|
||||
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
||||
ifneq ($(MM_NO_ENTERPRISE_LINT),true)
|
||||
$(GO) vet -vettool=$(GOBIN)/mattermost-govet -structuredLogging -inconsistentReceiverName -emptyStrCmp -tFatal -configtelemetry -errorAssertions -requestCtxNaming -enterpriseLicense $(BUILD_ENTERPRISE_DIR)/...
|
||||
endif
|
||||
endif
|
||||
|
||||
vet-api: export GO := $(GO)
|
||||
vet-api: export GOBIN := $(GOBIN)
|
||||
vet-api: export ROOT := $(ROOT)
|
||||
vet-api: ## Run mattermost go vet to verify api4 documentation, currently not passing
|
||||
$(GO) install github.com/mattermost/mattermost-govet/v2@7d8db289e508999dfcac47b97c9490a0fec12d66
|
||||
make -C ../api build
|
||||
./scripts/vet-api-check.sh
|
||||
|
||||
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=./public/model/utils.go -tests=false -o=./public/model/utils_serial_gen.go
|
||||
@echo "$$LICENSE_HEADER" > tmp.go
|
||||
@cat ./public/model/utils_serial_gen.go >> tmp.go
|
||||
@mv tmp.go ./public/model/utils_serial_gen.go
|
||||
$(GOBIN)/msgp -file=./public/model/session.go -tests=false -o=./public/model/session_serial_gen.go
|
||||
@echo "$$LICENSE_HEADER" > tmp.go
|
||||
@cat ./public/model/session_serial_gen.go >> tmp.go
|
||||
@mv tmp.go ./public/model/session_serial_gen.go
|
||||
$(GOBIN)/msgp -file=./public/model/user.go -tests=false -o=./public/model/user_serial_gen.go
|
||||
@echo "$$LICENSE_HEADER" > tmp.go
|
||||
@cat ./public/model/user_serial_gen.go >> tmp.go
|
||||
@mv tmp.go ./public/model/user_serial_gen.go
|
||||
$(GOBIN)/msgp -file=./public/model/team_member.go -tests=false -o=./public/model/team_member_serial_gen.go
|
||||
@echo "$$LICENSE_HEADER" > tmp.go
|
||||
@cat ./public/model/team_member_serial_gen.go >> tmp.go
|
||||
@mv tmp.go ./public/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")[: ]+' $(BUILD_ENTERPRISE_DIR)/
|
||||
endif
|
||||
|
||||
mmctl-build: ## Compiles and generates the mmctl binary
|
||||
go build -trimpath -ldflags '$(MMCTL_LDFLAGS)' -o bin/mmctl ./cmd/mmctl
|
||||
|
||||
mmctl-docs: ## Generate the mmctl docs
|
||||
rm -rf ./cmd/mmctl/docs
|
||||
cd ./cmd/mmctl && go run mmctl.go docs
|
||||
|
||||
## 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." > channels/db/migrations/migrations.list
|
||||
find channels/db/migrations -maxdepth 2 -mindepth 2 | sort >> channels/db/migrations/migrations.list
|
||||
|
||||
test-local-filestore: # Run tests for local filestore
|
||||
$(GO) test ./platform/shared/filestore -run '^TestLocalFileBackend' -v
|
||||
3
bin/.gitignore
vendored
Normal file
3
bin/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Folder required for build process
|
||||
*
|
||||
!.gitignore
|
||||
3
build/.gitignore
vendored
Normal file
3
build/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
diff.txt
|
||||
latest.sql
|
||||
migrated.sql
|
||||
88
build/Dockerfile
Normal file
88
build/Dockerfile
Normal file
@ -0,0 +1,88 @@
|
||||
# First stage - Ubuntu with document processing dependencies and curl for downloading
|
||||
FROM ubuntu:noble-20250415.1@sha256:dc17125eaac86538c57da886e494a34489122fb6a3ebb6411153d742594c2ddc AS builder
|
||||
# Setting bash as our shell, and enabling pipefail option
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
# Build Arguments
|
||||
ARG PUID=2000
|
||||
ARG PGID=2000
|
||||
# MM_PACKAGE build arguments controls which version of mattermost to install, defaults to latest stable enterprise
|
||||
# e.g. https://releases.mattermost.com/9.7.1/mattermost-9.7.1-linux-amd64.tar.gz
|
||||
ARG MM_PACKAGE="https://latest.mattermost.com/mattermost-enterprise-linux"
|
||||
|
||||
# Install needed packages and indirect dependencies
|
||||
RUN apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
|
||||
ca-certificates \
|
||||
curl \
|
||||
media-types \
|
||||
mailcap \
|
||||
unrtf \
|
||||
wv \
|
||||
poppler-utils \
|
||||
tidy \
|
||||
tzdata \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set mattermost group/user and download Mattermost
|
||||
RUN mkdir -p /mattermost/data /mattermost/plugins /mattermost/client/plugins \
|
||||
&& groupadd --gid ${PGID} mattermost \
|
||||
&& useradd --uid ${PUID} --gid ${PGID} --comment "" --home-dir /mattermost mattermost \
|
||||
&& curl -L $MM_PACKAGE | tar -xvz \
|
||||
&& chown -R mattermost:mattermost /mattermost /mattermost/data /mattermost/plugins /mattermost/client/plugins
|
||||
|
||||
# Create PostgreSQL client SSL directory structure for ssl_mode=require
|
||||
RUN mkdir -p /mattermost/.postgresql \
|
||||
&& chmod 700 /mattermost/.postgresql
|
||||
|
||||
# Final stage using distroless for minimal attack surface
|
||||
FROM gcr.io/distroless/base-debian12
|
||||
|
||||
# Some ENV variables
|
||||
ENV PATH="/mattermost/bin:${PATH}"
|
||||
ENV MM_SERVICESETTINGS_ENABLELOCALMODE="true"
|
||||
ENV MM_INSTALL_TYPE="docker"
|
||||
|
||||
# Copy over metadata files needed by runtime
|
||||
COPY --from=builder /etc/mime.types /etc
|
||||
|
||||
# Copy CA certificates for SSL/TLS validation with proper ownership
|
||||
COPY --from=builder --chown=2000:2000 /etc/ssl/certs /etc/ssl/certs
|
||||
|
||||
# Copy document processing utilities and necessary support files
|
||||
COPY --from=builder /usr/bin/pdftotext /usr/bin/pdftotext
|
||||
COPY --from=builder /usr/bin/wvText /usr/bin/wvText
|
||||
COPY --from=builder /usr/bin/wvWare /usr/bin/wvWare
|
||||
COPY --from=builder /usr/bin/unrtf /usr/bin/unrtf
|
||||
COPY --from=builder /usr/bin/tidy /usr/bin/tidy
|
||||
COPY --from=builder /usr/share/wv /usr/share/wv
|
||||
|
||||
# Copy necessary libraries for document processing utilities
|
||||
COPY --from=builder /usr/lib/libpoppler.so* /usr/lib/
|
||||
COPY --from=builder /usr/lib/libfreetype.so* /usr/lib/
|
||||
COPY --from=builder /usr/lib/libpng.so* /usr/lib/
|
||||
COPY --from=builder /usr/lib/libwv.so* /usr/lib/
|
||||
COPY --from=builder /usr/lib/libtidy.so* /usr/lib/
|
||||
COPY --from=builder /usr/lib/libfontconfig.so* /usr/lib/
|
||||
|
||||
# Copy mattermost from builder stage
|
||||
COPY --from=builder --chown=2000:2000 /mattermost /mattermost
|
||||
|
||||
# Copy passwd including mattermost user
|
||||
COPY passwd /etc/passwd
|
||||
|
||||
# We should refrain from running as privileged user
|
||||
USER mattermost
|
||||
|
||||
# Healthcheck to make sure container is ready - using mmctl instead of curl for distroless compatibility
|
||||
HEALTHCHECK --interval=30s --timeout=10s \
|
||||
CMD ["/mattermost/bin/mmctl", "system", "status", "--local"]
|
||||
|
||||
# Configure entrypoint and command with proper permissions
|
||||
WORKDIR /mattermost
|
||||
CMD ["/mattermost/bin/mattermost"]
|
||||
|
||||
EXPOSE 8065 8067 8074 8075
|
||||
|
||||
# Declare volumes for mount point directories
|
||||
VOLUME ["/mattermost/data", "/mattermost/logs", "/mattermost/config", "/mattermost/plugins", "/mattermost/client/plugins"]
|
||||
17
build/Dockerfile.buildenv
Normal file
17
build/Dockerfile.buildenv
Normal file
@ -0,0 +1,17 @@
|
||||
FROM golang:1.24.6-bullseye@sha256:cf78ce8205287fdb2ca403aac77d68965c75734749e560c577c00e20ecb11954
|
||||
ARG NODE_VERSION=20.11.1
|
||||
|
||||
RUN apt-get update && apt-get install -y make git apt-transport-https ca-certificates curl software-properties-common build-essential zip xmlsec1 jq pgloader gnupg
|
||||
|
||||
# Download and install node via nvm
|
||||
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
|
||||
RUN bash -c "source /root/.nvm/nvm.sh && nvm install $NODE_VERSION && nvm use $NODE_VERSION && nvm alias default $NODE_VERSION"
|
||||
|
||||
# Make node and npm globally available
|
||||
ENV PATH="/root/.nvm/versions/node/v$NODE_VERSION/bin:$PATH"
|
||||
|
||||
RUN git config --global --add safe.directory /mattermost
|
||||
|
||||
# Remove any existing entrypoint, and use bash as the default command
|
||||
ENTRYPOINT []
|
||||
CMD ["bash"]
|
||||
17
build/Dockerfile.buildenv-fips
Normal file
17
build/Dockerfile.buildenv-fips
Normal file
@ -0,0 +1,17 @@
|
||||
FROM cgr.dev/mattermost.com/go-msft-fips:1.24.6-dev@sha256:53d076b1cfa53f8189c4723d813d711d92107c2e8b140805c71e39f4a06dc9cc
|
||||
ARG NODE_VERSION=20.11.1
|
||||
|
||||
RUN apk add curl ca-certificates mailcap unrtf wv poppler-utils tzdata gpg xmlsec
|
||||
|
||||
# Download and install node via nvm
|
||||
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
|
||||
RUN bash -c "source /root/.nvm/nvm.sh && nvm install $NODE_VERSION && nvm use $NODE_VERSION && nvm alias default $NODE_VERSION"
|
||||
|
||||
# Make node and npm globally available
|
||||
ENV PATH="/root/.nvm/versions/node/v$NODE_VERSION/bin:$PATH"
|
||||
|
||||
RUN git config --global --add safe.directory /mattermost
|
||||
|
||||
# Remove any existing entrypoint, and use bash as the default command
|
||||
ENTRYPOINT []
|
||||
CMD ["bash"]
|
||||
91
build/Dockerfile.fips
Normal file
91
build/Dockerfile.fips
Normal file
@ -0,0 +1,91 @@
|
||||
# First stage - FIPS dev image with dependencies for building
|
||||
FROM cgr.dev/mattermost.com/glibc-openssl-fips:15-dev@sha256:9223f9245fb026a3c255ce9b7028a069fe11432aa7710713a331eaa36f44851c AS builder
|
||||
# Setting bash as our shell, and enabling pipefail option
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
# Build Arguments
|
||||
ARG PUID=2000
|
||||
ARG PGID=2000
|
||||
# MM_PACKAGE build arguments controls which version of mattermost to install, defaults to latest stable enterprise
|
||||
# e.g. https://releases.mattermost.com/9.7.1/mattermost-9.7.1-linux-amd64.tar.gz
|
||||
ARG MM_PACKAGE="https://latest.mattermost.com/mattermost-enterprise-linux"
|
||||
|
||||
# Install needed packages and indirect dependencies
|
||||
USER 0:0
|
||||
RUN apk add \
|
||||
curl \
|
||||
ca-certificates \
|
||||
mailcap \
|
||||
unrtf \
|
||||
wv \
|
||||
poppler-utils \
|
||||
tzdata
|
||||
|
||||
# Set mattermost group/user and download Mattermost
|
||||
RUN mkdir -p /mattermost/data /mattermost/plugins /mattermost/client/plugins \
|
||||
&& addgroup -g ${PGID} mattermost \
|
||||
&& adduser -D -u ${PUID} -G mattermost -g "" -s /bin/sh -h /mattermost mattermost \
|
||||
&& curl -L $MM_PACKAGE | tar -xvz \
|
||||
&& chown -R mattermost:mattermost /mattermost /mattermost/data /mattermost/plugins /mattermost/client/plugins
|
||||
|
||||
# Create PostgreSQL client SSL directory structure for ssl_mode=require
|
||||
RUN mkdir -p /mattermost/.postgresql \
|
||||
&& chmod 700 /mattermost/.postgresql
|
||||
|
||||
# Create /var/tmp directory needed for local socket files
|
||||
RUN mkdir -p /var/tmp \
|
||||
&& chmod 755 /var/tmp
|
||||
|
||||
# Final stage using FIPS runtime image
|
||||
FROM cgr.dev/mattermost.com/glibc-openssl-fips:15@sha256:7947eecc0d82fa3bc661aaca039bcd86d55fdf3ee581c8ecdef1b3c6f63fa83a
|
||||
|
||||
# Some ENV variables
|
||||
ENV PATH="/mattermost/bin:${PATH}"
|
||||
ENV MM_SERVICESETTINGS_ENABLELOCALMODE="true"
|
||||
ENV MM_INSTALL_TYPE="docker"
|
||||
|
||||
# Copy over metadata files needed by runtime
|
||||
COPY --from=builder /etc/mime.types /etc
|
||||
|
||||
# Copy CA certificates for SSL/TLS validation with proper ownership
|
||||
COPY --from=builder --chown=2000:2000 /etc/ssl/certs /etc/ssl/certs
|
||||
|
||||
# Copy document processing utilities and necessary support files
|
||||
COPY --from=builder /usr/bin/pdftotext /usr/bin/pdftotext
|
||||
COPY --from=builder /usr/bin/wvText /usr/bin/wvText
|
||||
COPY --from=builder /usr/bin/wvWare /usr/bin/wvWare
|
||||
COPY --from=builder /usr/bin/unrtf /usr/bin/unrtf
|
||||
COPY --from=builder /usr/share/wv /usr/share/wv
|
||||
|
||||
# Copy necessary libraries for document processing utilities
|
||||
COPY --from=builder /usr/lib/libpoppler.so* /usr/lib/
|
||||
COPY --from=builder /usr/lib/libfreetype.so* /usr/lib/
|
||||
COPY --from=builder /usr/lib/libpng16.so* /usr/lib/
|
||||
COPY --from=builder /usr/lib/libwv.so* /usr/lib/
|
||||
COPY --from=builder /usr/lib/libfontconfig.so* /usr/lib/
|
||||
|
||||
# Copy mattermost from builder stage
|
||||
COPY --from=builder --chown=2000:2000 /mattermost /mattermost
|
||||
|
||||
# Copy group and passwd files including mattermost user
|
||||
COPY --from=builder /etc/passwd /etc/passwd
|
||||
COPY --from=builder /etc/group /etc/group
|
||||
|
||||
# Copy /var/tmp directory needed for local socket files
|
||||
COPY --from=builder --chown=2000:2000 /var/tmp /var/tmp
|
||||
|
||||
# We should refrain from running as privileged user
|
||||
USER mattermost
|
||||
|
||||
# Healthcheck to make sure container is ready - using mmctl instead of curl for distroless compatibility
|
||||
HEALTHCHECK --interval=30s --timeout=10s \
|
||||
CMD ["/mattermost/bin/mmctl", "system", "status", "--local"]
|
||||
|
||||
# Configure entrypoint and command with proper permissions
|
||||
WORKDIR /mattermost
|
||||
CMD ["/mattermost/bin/mattermost"]
|
||||
|
||||
EXPOSE 8065 8067 8074 8075
|
||||
|
||||
# Declare volumes for mount point directories
|
||||
VOLUME ["/mattermost/data", "/mattermost/logs", "/mattermost/config", "/mattermost/plugins", "/mattermost/client/plugins"]
|
||||
4
build/Dockerfile.opensearch
Normal file
4
build/Dockerfile.opensearch
Normal file
@ -0,0 +1,4 @@
|
||||
ARG OPENSEARCH_VERSION=2.7.0
|
||||
FROM opensearchproject/opensearch:$OPENSEARCH_VERSION
|
||||
|
||||
RUN /usr/share/opensearch/bin/opensearch-plugin install analysis-icu
|
||||
11
build/MIT-COMPILED-LICENSE.md
Normal file
11
build/MIT-COMPILED-LICENSE.md
Normal file
@ -0,0 +1,11 @@
|
||||
Mattermost MIT Compiled License
|
||||
|
||||
**Note: this license does not cover source code, for information on source code licensing see LICENSE.txt in the Mattermost source code.
|
||||
|
||||
Copyright (c) 2016-present Mattermost, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software;
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
1
build/PRIVACY_POLICY.md
Normal file
1
build/PRIVACY_POLICY.md
Normal file
@ -0,0 +1 @@
|
||||
https://mattermost.com/pl/privacy-policy/
|
||||
21
build/README.md
Normal file
21
build/README.md
Normal file
@ -0,0 +1,21 @@
|
||||
## About this folder
|
||||
|
||||
This folder contains some files that we use to build the `mattermost-server` and other files like privacy policy and licenses.
|
||||
|
||||
The `Dockerfile` in this folder (`Dockerfile.buildenv`) is the build environment for our current builds you can find the docker image to download [here](https://hub.docker.com/r/mattermost/mattermost-build-server/tags/) or build your own.
|
||||
|
||||
|
||||
|
||||
### Docker Image for building the Server
|
||||
|
||||
We have a docker image to build `mattermost-server` and it is based on Go docker image.
|
||||
|
||||
In our Docker Hub Repository we have the following images:
|
||||
|
||||
- `mattermost/mattermost-build-server:dec-7-2018` which is based on Go 1.11 you can use for MM versions <= `5.8.0`
|
||||
- `mattermost/mattermost-build-server:feb-28-2019` which is based on Go 1.12 you can use for MM versions >= `5.9.0` <= `5.15.0`
|
||||
- `mattermost/mattermost-build-server:sep-17-2019` which is based on Go 1.12.9 you can use for MM versions >= `5.16.0`
|
||||
- `mattermost/mattermost-build-server:20200322_golang-1.14.1` which is based on Go 1.14.1 you can use for MM versions >= `5.24.x`
|
||||
- `mattermost/mattermost-build-server:20201023_golang-1.14.6` which is based on Go 1.14.6 you can use for MM versions >= `5.25.x`
|
||||
- `mattermost/mattermost-build-server:20201119_golang-1.15.5` which is based on Go 1.15.5 you can use for MM versions >= `5.26.x` to `5.37.x`
|
||||
- `mattermost/mattermost-build-server:20210810_golang-1.16.7` which is based on Go 1.16.X you can use for MM versions >= `5.38.x`
|
||||
63
build/docker-compose-generator/main.go
Normal file
63
build/docker-compose-generator/main.go
Normal file
@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type DockerCompose struct {
|
||||
Services map[string]*Container `yaml:"services"`
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
Command string `yaml:"command,omitempty"`
|
||||
Image string `yaml:"image,omitempty"`
|
||||
Network []string `yaml:"networks,omitempty"`
|
||||
DependsOn []string `yaml:"depends_on,omitempty"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
validServices := map[string]int{
|
||||
"postgres": 5432,
|
||||
"minio": 9000,
|
||||
"inbucket": 9001,
|
||||
"openldap": 389,
|
||||
"elasticsearch": 9200,
|
||||
"opensearch": 9201,
|
||||
"redis": 6379,
|
||||
"dejavu": 1358,
|
||||
"keycloak": 8080,
|
||||
"prometheus": 9090,
|
||||
"grafana": 3000,
|
||||
"loki": 3100,
|
||||
"promtail": 3180,
|
||||
}
|
||||
command := []string{}
|
||||
for _, arg := range os.Args[1:] {
|
||||
port, ok := validServices[arg]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("Unknown service %s", arg))
|
||||
}
|
||||
command = append(command, fmt.Sprintf("%s:%d", arg, port))
|
||||
}
|
||||
|
||||
var dockerCompose DockerCompose
|
||||
dockerCompose.Services = map[string]*Container{}
|
||||
dockerCompose.Services["start_dependencies"] = &Container{
|
||||
Image: "mattermost/mattermost-wait-for-dep:latest",
|
||||
Network: []string{"mm-test"},
|
||||
DependsOn: os.Args[1:],
|
||||
Command: strings.Join(command, " "),
|
||||
}
|
||||
resultData, err := yaml.Marshal(dockerCompose)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Unable to serialize the docker-compose file: %s.", err.Error()))
|
||||
}
|
||||
fmt.Println(string(resultData))
|
||||
}
|
||||
134
build/docker-compose.common.yml
Normal file
134
build/docker-compose.common.yml
Normal file
@ -0,0 +1,134 @@
|
||||
services:
|
||||
postgres:
|
||||
image: "postgres:14"
|
||||
restart: always
|
||||
networks:
|
||||
- mm-test
|
||||
environment:
|
||||
POSTGRES_USER: mmuser
|
||||
POSTGRES_PASSWORD: mostest
|
||||
POSTGRES_DB: mattermost_test
|
||||
POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256 --auth-local=scram-sha-256"
|
||||
command: postgres -c 'config_file=/etc/postgresql/postgresql.conf'
|
||||
volumes:
|
||||
- "./docker/postgres.conf:/etc/postgresql/postgresql.conf"
|
||||
- "./docker/postgres_node_database.sql:/docker-entrypoint-initdb.d/postgres_node_database.sql"
|
||||
healthcheck:
|
||||
test: [ "CMD", "pg_isready", "-h", "localhost" ]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
minio:
|
||||
image: "minio/minio:RELEASE.2024-06-22T05-26-45Z"
|
||||
command: "server /data --console-address :9002"
|
||||
networks:
|
||||
- mm-test
|
||||
environment:
|
||||
MINIO_ROOT_USER: minioaccesskey
|
||||
MINIO_ROOT_PASSWORD: miniosecretkey
|
||||
MINIO_KMS_SECRET_KEY: my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw=
|
||||
inbucket:
|
||||
image: "inbucket/inbucket:stable"
|
||||
restart: always
|
||||
environment:
|
||||
INBUCKET_WEB_ADDR: "0.0.0.0:9001"
|
||||
INBUCKET_POP3_ADDR: "0.0.0.0:10110"
|
||||
INBUCKET_SMTP_ADDR: "0.0.0.0:10025"
|
||||
networks:
|
||||
- mm-test
|
||||
openldap:
|
||||
image: "osixia/openldap:1.4.0"
|
||||
restart: always
|
||||
networks:
|
||||
- mm-test
|
||||
environment:
|
||||
LDAP_TLS_VERIFY_CLIENT: "never"
|
||||
LDAP_ORGANISATION: "Mattermost Test"
|
||||
LDAP_DOMAIN: "mm.test.com"
|
||||
LDAP_ADMIN_PASSWORD: "mostest"
|
||||
elasticsearch:
|
||||
image: "mattermostdevelopment/mattermost-elasticsearch:8.9.0"
|
||||
networks:
|
||||
- mm-test
|
||||
environment:
|
||||
http.host: "0.0.0.0"
|
||||
http.port: 9200
|
||||
http.cors.enabled: "true"
|
||||
http.cors.allow-origin: "http://localhost:1358,http://127.0.0.1:1358"
|
||||
http.cors.allow-headers: "X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization"
|
||||
http.cors.allow-credentials: "true"
|
||||
transport.host: "127.0.0.1"
|
||||
xpack.security.enabled: "false"
|
||||
action.destructive_requires_name: "false"
|
||||
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
|
||||
opensearch:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile.opensearch
|
||||
networks:
|
||||
- mm-test
|
||||
environment:
|
||||
http.host: "0.0.0.0"
|
||||
http.port: 9201
|
||||
http.cors.enabled: "true"
|
||||
http.cors.allow-origin: "http://localhost:1358,http://127.0.0.1:1358"
|
||||
http.cors.allow-headers: "X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization"
|
||||
http.cors.allow-credentials: "true"
|
||||
transport.host: "127.0.0.1"
|
||||
discovery.type: single-node
|
||||
plugins.security.disabled: "true"
|
||||
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
|
||||
redis:
|
||||
image: "redis:7.4.0"
|
||||
networks:
|
||||
- mm-test
|
||||
dejavu:
|
||||
image: "appbaseio/dejavu:3.4.2"
|
||||
networks:
|
||||
- mm-test
|
||||
keycloak:
|
||||
image: "quay.io/keycloak/keycloak:23.0.7"
|
||||
restart: always
|
||||
entrypoint: /opt/keycloak/bin/kc.sh start --import-realm
|
||||
networks:
|
||||
- mm-test
|
||||
environment:
|
||||
KEYCLOAK_ADMIN: admin
|
||||
KEYCLOAK_ADMIN_PASSWORD: admin
|
||||
KC_HOSTNAME_STRICT: 'false'
|
||||
KC_HOSTNAME_STRICT_HTTPS: 'false'
|
||||
KC_HTTP_ENABLED: 'true'
|
||||
volumes:
|
||||
- "./docker/keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json"
|
||||
prometheus:
|
||||
image: "prom/prometheus:v2.46.0"
|
||||
user: root
|
||||
volumes:
|
||||
- "./docker/prometheus.yml:/etc/prometheus/prometheus.yml"
|
||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||
networks:
|
||||
- mm-test
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
grafana:
|
||||
image: "grafana/grafana:10.4.2"
|
||||
volumes:
|
||||
- "./docker/grafana/grafana.ini:/etc/grafana/grafana.ini"
|
||||
- "./docker/grafana/provisioning:/etc/grafana/provisioning"
|
||||
- "./docker/grafana/dashboards:/var/lib/grafana/dashboards"
|
||||
networks:
|
||||
- mm-test
|
||||
loki:
|
||||
image: "grafana/loki:3.0.0"
|
||||
networks:
|
||||
- mm-test
|
||||
promtail:
|
||||
image: "grafana/promtail:3.0.0"
|
||||
volumes:
|
||||
- "./docker/promtail/promtail-local-config.yaml:/etc/promtail/docker-config.yaml"
|
||||
- "/var/lib/docker/containers:/var/lib/docker/containers:ro"
|
||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||
- "../logs:/logs"
|
||||
command: -config.file=/etc/promtail/docker-config.yaml
|
||||
networks:
|
||||
- mm-test
|
||||
72
build/docker-compose.yml
Normal file
72
build/docker-compose.yml
Normal file
@ -0,0 +1,72 @@
|
||||
services:
|
||||
postgres:
|
||||
extends:
|
||||
file: docker-compose.common.yml
|
||||
service: postgres
|
||||
tmpfs: /var/lib/postgresql/data
|
||||
minio:
|
||||
extends:
|
||||
file: docker-compose.common.yml
|
||||
service: minio
|
||||
inbucket:
|
||||
extends:
|
||||
file: docker-compose.common.yml
|
||||
service: inbucket
|
||||
openldap:
|
||||
extends:
|
||||
file: docker-compose.common.yml
|
||||
service: openldap
|
||||
elasticsearch:
|
||||
extends:
|
||||
file: docker-compose.common.yml
|
||||
service: elasticsearch
|
||||
opensearch:
|
||||
extends:
|
||||
file: docker-compose.common.yml
|
||||
service: opensearch
|
||||
redis:
|
||||
extends:
|
||||
file: docker-compose.common.yml
|
||||
service: redis
|
||||
dejavu:
|
||||
extends:
|
||||
file: docker-compose.common.yml
|
||||
service: dejavu
|
||||
keycloak:
|
||||
extends:
|
||||
file: docker-compose.common.yml
|
||||
service: keycloak
|
||||
prometheus:
|
||||
extends:
|
||||
file: docker-compose.common.yml
|
||||
service: prometheus
|
||||
grafana:
|
||||
extends:
|
||||
file: docker-compose.common.yml
|
||||
service: grafana
|
||||
loki:
|
||||
extends:
|
||||
file: docker-compose.common.yml
|
||||
service: loki
|
||||
promtail:
|
||||
extends:
|
||||
file: docker-compose.common.yml
|
||||
service: promtail
|
||||
|
||||
start_dependencies:
|
||||
image: mattermost/mattermost-wait-for-dep:latest
|
||||
networks:
|
||||
- mm-test
|
||||
depends_on:
|
||||
- postgres
|
||||
- minio
|
||||
- inbucket
|
||||
- openldap
|
||||
- elasticsearch
|
||||
- opensearch
|
||||
- redis
|
||||
command: postgres:5432 minio:9000 inbucket:9001 openldap:389 elasticsearch:9200 opensearch:9201 redis:6379
|
||||
|
||||
networks:
|
||||
mm-test:
|
||||
driver: bridge
|
||||
38
build/docker-preview/Dockerfile
Normal file
38
build/docker-preview/Dockerfile
Normal file
@ -0,0 +1,38 @@
|
||||
# Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
# See License.txt for license information.
|
||||
FROM postgres:14
|
||||
|
||||
RUN apt-get update && apt-get install -y ca-certificates
|
||||
|
||||
ARG MATTERMOST_VERSION
|
||||
#
|
||||
# Configure PostgreSQL
|
||||
#
|
||||
|
||||
ENV POSTGRES_USER=mmuser
|
||||
ENV POSTGRES_PASSWORD=mostest
|
||||
ENV POSTGRES_DB=mattermost_test
|
||||
|
||||
#
|
||||
# Configure Mattermost
|
||||
#
|
||||
WORKDIR /mm
|
||||
|
||||
# Copy over files
|
||||
ADD https://releases.mattermost.com/${MATTERMOST_VERSION}/mattermost-team-${MATTERMOST_VERSION}-linux-amd64.tar.gz .
|
||||
RUN tar -zxvf mattermost-team-${MATTERMOST_VERSION}-linux-amd64.tar.gz
|
||||
ADD config_docker.json ./mattermost/config/config_docker.json
|
||||
ADD docker-entry.sh .
|
||||
|
||||
RUN chmod +x ./docker-entry.sh
|
||||
ENTRYPOINT ./docker-entry.sh
|
||||
|
||||
# Mattermost environment variables
|
||||
ENV PATH="/mm/mattermost/bin:${PATH}"
|
||||
|
||||
# Create default storage directory
|
||||
RUN mkdir ./mattermost-data
|
||||
VOLUME /mm/mattermost-data
|
||||
|
||||
# Ports
|
||||
EXPOSE 8065
|
||||
19
build/docker-preview/README.md
Normal file
19
build/docker-preview/README.md
Normal file
@ -0,0 +1,19 @@
|
||||
# Mattermost Docker Preview Image
|
||||
|
||||
This is a Docker image to install Mattermost in *Preview Mode* for exploring product functionality on a single machine using Docker.
|
||||
|
||||
Note: This configuration should not be used in production, as it’s using a known password string and contains other non-production configuration settings, and it does not support upgrade. If you’re looking for a production installation with Docker, please see the [Mattermost Production Docker Deployment Guide](https://docs.mattermost.com/install/install-docker.html#deploy-mattermost-on-docker-for-production-use).
|
||||
|
||||
To contribute, please see [Contribution Guidelines](https://developers.mattermost.com/contribute/more-info/getting-started/).
|
||||
|
||||
To file issues, [search for existing bugs and file a GitHub issue if your bug is new](https://developers.mattermost.com/contribute/why-contribute/#youve-found-a-bug).
|
||||
|
||||
## Usage
|
||||
|
||||
Please see [documentation for usage](http://docs.mattermost.com/install/docker-local-machine.html).
|
||||
|
||||
If you have Docker already set up, you can run this image in one line:
|
||||
|
||||
```
|
||||
docker run --name mattermost-preview -d --publish 8065:8065 --add-host dockerhost:127.0.0.1 mattermost/mattermost-preview
|
||||
```
|
||||
29
build/docker-preview/config_docker.json
Normal file
29
build/docker-preview/config_docker.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"ServiceSettings": {
|
||||
"EnableOAuthServiceProvider": true,
|
||||
"EnableDeveloper": false,
|
||||
"EnableGifPicker": true
|
||||
},
|
||||
"SqlSettings": {
|
||||
"DriverName": "postgres",
|
||||
"DataSource": "postgres://mmuser:mostest@localhost/mattermost_test?sslmode=disable\u0026connect_timeout=10\u0026binary_parameters=yes",
|
||||
"AtRestEncryptKey": ""
|
||||
},
|
||||
"FileSettings": {
|
||||
"DriverName": "local",
|
||||
"Directory": "/mm/mattermost-data",
|
||||
"PublicLinkSalt": ""
|
||||
},
|
||||
"EmailSettings": {
|
||||
"SMTPServer": "",
|
||||
"SMTPPort": "",
|
||||
"PushNotificationContents": "generic"
|
||||
},
|
||||
"ElasticsearchSettings": {
|
||||
"ConnectionUrl": ""
|
||||
},
|
||||
"PluginSettings": {
|
||||
"EnableUploads": true,
|
||||
"PluginStates": {}
|
||||
}
|
||||
}
|
||||
18
build/docker-preview/docker-entry.sh
Normal file
18
build/docker-preview/docker-entry.sh
Normal file
@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
# Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
# See License.txt for license information.
|
||||
|
||||
echo "Starting PostgreSQL"
|
||||
docker-entrypoint.sh -c 'shared_buffers=256MB' -c 'max_connections=300' &
|
||||
|
||||
until pg_isready -hlocalhost -p 5432 -U "$POSTGRES_USER" &> /dev/null; do
|
||||
echo "postgres still not ready, sleeping"
|
||||
sleep 5
|
||||
done
|
||||
|
||||
echo "Updating CA certificates"
|
||||
update-ca-certificates --fresh >/dev/null
|
||||
|
||||
echo "Starting platform"
|
||||
cd mattermost
|
||||
exec ./bin/mattermost --config=config/config_docker.json
|
||||
15
build/docker/grafana/dashboards/README.md
Normal file
15
build/docker/grafana/dashboards/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# Developer Dashboards
|
||||
|
||||
This folder contains a modified form of the following Grafana dashboards:
|
||||
* [Mattermost Performance Monitoring](https://grafana.com/grafana/dashboards/2542)
|
||||
* [Mattermost Performance Monitoring (Bonus Metrics)](https://grafana.com/grafana/dashboards/2545)
|
||||
* [Mattermost Performance KPI Metrics](https://grafana.com/grafana/dashboards/2539)
|
||||
|
||||
The dashboards are modified from the version available on grafana.com since [Grafana doesn't currently support variables](https://github.com/grafana/grafana/issues/10786) (i.e. `${DS_MATTERMOST}`) and has no way of binding the datasource with the dashboards at the time of provisioning. Instead of falling back to the REST API to effect these changes, the following in-place changes were made to the exported dashboards available above:
|
||||
* Remove the top-level `__inputs` key
|
||||
* Remove the top-level `__requires` key
|
||||
* Replace all instances of `${DS_MATTERMOST}` with `Prometheus`, matching the name of the provisioned datasource.
|
||||
|
||||
Upon using the dashboards within Grafana however, it immediately adds various missing fields, presumably due to some internal migration. This results in a spurious prompt to "save" the dashboard. To avoid confusion, these changes were subsequently exported and written back into the provisioned dashboards above.
|
||||
|
||||
This entire process above will need to be repeated in the event newer versions of these dashboards are published.
|
||||
428
build/docker/grafana/dashboards/home.json
Normal file
428
build/docker/grafana/dashboards/home.json
Normal file
@ -0,0 +1,428 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "grafana"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": 5,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 3,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 1,
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"type": "welcome"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 3
|
||||
},
|
||||
"id": 3,
|
||||
"options": {
|
||||
"includeVars": false,
|
||||
"keepTime": false,
|
||||
"maxItems": 30,
|
||||
"query": "Mattermost",
|
||||
"showHeadings": true,
|
||||
"showRecentlyViewed": false,
|
||||
"showSearch": true,
|
||||
"showStarred": false,
|
||||
"tags": []
|
||||
},
|
||||
"pluginVersion": "10.4.2",
|
||||
"tags": [],
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Dashboards",
|
||||
"type": "dashlist"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Mixed --"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"options": {
|
||||
"0": {
|
||||
"color": "red",
|
||||
"index": 0,
|
||||
"text": "Offline"
|
||||
}
|
||||
},
|
||||
"type": "value"
|
||||
},
|
||||
{
|
||||
"options": {
|
||||
"from": 0,
|
||||
"result": {
|
||||
"color": "green",
|
||||
"index": 1,
|
||||
"text": "Online"
|
||||
},
|
||||
"to": 99999999999999
|
||||
},
|
||||
"type": "range"
|
||||
}
|
||||
],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 12,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 3
|
||||
},
|
||||
"id": 7,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "/^(?!Time)/",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "value_and_name",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "10.4.2",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "loki",
|
||||
"uid": "P8E80F9AEF21F6940"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum(count_over_time({container=\"mattermost-postgres\"}[5m])) or vector(0)",
|
||||
"legendFormat": "Postgres",
|
||||
"queryType": "range",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "loki",
|
||||
"uid": "P8E80F9AEF21F6940"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum(count_over_time({container=\"mattermost-inbucket\"}[5m])) or vector(0)",
|
||||
"legendFormat": "Inbucket",
|
||||
"queryType": "range",
|
||||
"refId": "C"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "loki",
|
||||
"uid": "P8E80F9AEF21F6940"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum(count_over_time({container=\"mattermost-minio\"}[5m])) or vector(0)",
|
||||
"legendFormat": "MinIO",
|
||||
"queryType": "range",
|
||||
"refId": "D"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "loki",
|
||||
"uid": "P8E80F9AEF21F6940"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum(count_over_time({container=\"mattermost-openldap\"}[5m])) or vector(0)",
|
||||
"legendFormat": "OpenLDAP",
|
||||
"queryType": "range",
|
||||
"refId": "E"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "loki",
|
||||
"uid": "P8E80F9AEF21F6940"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum(count_over_time({container=\"mattermost-dejavu\"}[5m])) or vector(0)",
|
||||
"legendFormat": "DejaVu",
|
||||
"queryType": "range",
|
||||
"refId": "F"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "loki",
|
||||
"uid": "P8E80F9AEF21F6940"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum(count_over_time({container=\"mattermost-keycloak\"}[5m])) or vector(0)",
|
||||
"legendFormat": "Keycloak",
|
||||
"queryType": "range",
|
||||
"refId": "G"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "loki",
|
||||
"uid": "P8E80F9AEF21F6940"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum(count_over_time({container=\"mattermost-elasticsearch\"}[5m])) or vector(0)",
|
||||
"legendFormat": "Elasticsearch",
|
||||
"queryType": "range",
|
||||
"refId": "H"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum(up{container=\"mattermost-prometheus\"}) or vector(0)",
|
||||
"hide": false,
|
||||
"legendFormat": "Prometheus",
|
||||
"range": true,
|
||||
"refId": "I"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum(up{container=\"mattermost-grafana\"}) or vector(0)",
|
||||
"hide": false,
|
||||
"legendFormat": "Grafana",
|
||||
"range": true,
|
||||
"refId": "J"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum(up{container=\"mattermost-loki\"}) or vector(0)",
|
||||
"hide": false,
|
||||
"legendFormat": "Loki",
|
||||
"range": true,
|
||||
"refId": "K"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum(up{container=\"mattermost-promtail\"}) or vector(0)",
|
||||
"hide": false,
|
||||
"legendFormat": "Promtail",
|
||||
"range": true,
|
||||
"refId": "L"
|
||||
}
|
||||
],
|
||||
"title": "Panel Title",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "loki",
|
||||
"uid": "P8E80F9AEF21F6940"
|
||||
},
|
||||
"description": "Links to common queries.",
|
||||
"gridPos": {
|
||||
"h": 5,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 10
|
||||
},
|
||||
"id": 6,
|
||||
"options": {
|
||||
"code": {
|
||||
"language": "plaintext",
|
||||
"showLineNumbers": false,
|
||||
"showMiniMap": false
|
||||
},
|
||||
"content": "* [Mattermost Error Logs](/explore?schemaVersion=1&panes=%7B%22z8g%22:%7B%22datasource%22:%22P8E80F9AEF21F6940%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22expr%22:%22%7Bapp%3D%5C%22mattermost%5C%22%7D%20%7C%20level%3D%5C%22error%5C%22%22,%22queryType%22:%22range%22,%22datasource%22:%7B%22type%22:%22loki%22,%22uid%22:%22P8E80F9AEF21F6940%22%7D,%22editorMode%22:%22code%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1)\n* [Plugin Logs](/explore?schemaVersion=1&panes=%7B%22z8g%22:%7B%22datasource%22:%22P8E80F9AEF21F6940%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22expr%22:%22%7Bapp%3D%5C%22mattermost%5C%22%7D%20%7C%20json%20%7C%20plugin_id%20%21%3D%20%5C%22%5C%22%22,%22queryType%22:%22range%22,%22datasource%22:%7B%22type%22:%22loki%22,%22uid%22:%22P8E80F9AEF21F6940%22%7D,%22editorMode%22:%22code%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1)",
|
||||
"mode": "markdown"
|
||||
},
|
||||
"pluginVersion": "10.4.2",
|
||||
"title": "Links",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "loki",
|
||||
"uid": "P8E80F9AEF21F6940"
|
||||
},
|
||||
"description": "Logs from the localhost Mattermost instance.",
|
||||
"gridPos": {
|
||||
"h": 10,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 15
|
||||
},
|
||||
"id": 4,
|
||||
"options": {
|
||||
"dedupStrategy": "none",
|
||||
"enableLogDetails": true,
|
||||
"prettifyLogMessage": false,
|
||||
"showCommonLabels": false,
|
||||
"showLabels": false,
|
||||
"showTime": false,
|
||||
"sortOrder": "Descending",
|
||||
"wrapLogMessage": false
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "loki",
|
||||
"uid": "P8E80F9AEF21F6940"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "{app=\"mattermost\"}",
|
||||
"queryType": "range",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Mattermost Logs",
|
||||
"type": "logs"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "loki",
|
||||
"uid": "P8E80F9AEF21F6940"
|
||||
},
|
||||
"description": "Logs from the Docker services running to support Mattermost.",
|
||||
"gridPos": {
|
||||
"h": 10,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 25
|
||||
},
|
||||
"id": 5,
|
||||
"options": {
|
||||
"dedupStrategy": "none",
|
||||
"enableLogDetails": true,
|
||||
"prettifyLogMessage": false,
|
||||
"showCommonLabels": false,
|
||||
"showLabels": false,
|
||||
"showTime": false,
|
||||
"sortOrder": "Descending",
|
||||
"wrapLogMessage": false
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "loki",
|
||||
"uid": "P8E80F9AEF21F6940"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "{job=\"docker\"}",
|
||||
"queryType": "range",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Docker Logs",
|
||||
"type": "logs"
|
||||
}
|
||||
],
|
||||
"schemaVersion": 39,
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"hidden": true,
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
],
|
||||
"type": "timepicker"
|
||||
},
|
||||
"timezone": "browser",
|
||||
"title": "Home",
|
||||
"uid": "4yRNKfaGz",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
||||
9
build/docker/grafana/grafana.ini
Normal file
9
build/docker/grafana/grafana.ini
Normal file
@ -0,0 +1,9 @@
|
||||
[auth]
|
||||
disable_login_form = false
|
||||
|
||||
[auth.anonymous]
|
||||
enabled = true
|
||||
org_role = Editor
|
||||
|
||||
[dashboards]
|
||||
default_home_dashboard_path = /var/lib/grafana/dashboards/home.json
|
||||
14
build/docker/grafana/provisioning/dashboards/mattermost.yaml
Normal file
14
build/docker/grafana/provisioning/dashboards/mattermost.yaml
Normal file
@ -0,0 +1,14 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: 'mattermost'
|
||||
orgId: 1
|
||||
folder: 'mattermost'
|
||||
type: file
|
||||
disableDeletion: true
|
||||
updateIntervalSeconds: 300
|
||||
allowUiUpdates: true
|
||||
options:
|
||||
# <string, required> path to dashboard files on disk. Required when using the 'file' type
|
||||
path: /var/lib/grafana/dashboards
|
||||
foldersFromFilesStructure: true
|
||||
11
build/docker/grafana/provisioning/datasources/loki.yml
Normal file
11
build/docker/grafana/provisioning/datasources/loki.yml
Normal file
@ -0,0 +1,11 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Loki
|
||||
type: 'loki'
|
||||
access: 'proxy'
|
||||
orgId: 1
|
||||
url: 'http://loki:3100'
|
||||
isDefault: false
|
||||
version: 1
|
||||
editable: false
|
||||
11
build/docker/grafana/provisioning/datasources/prometheus.yml
Normal file
11
build/docker/grafana/provisioning/datasources/prometheus.yml
Normal file
@ -0,0 +1,11 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: 'prometheus'
|
||||
access: 'proxy'
|
||||
orgId: 1
|
||||
url: 'http://prometheus:9090'
|
||||
isDefault: true
|
||||
version: 1
|
||||
editable: false
|
||||
39
build/docker/keycloak/README.md
Normal file
39
build/docker/keycloak/README.md
Normal file
@ -0,0 +1,39 @@
|
||||
# Keycloak development environment
|
||||
|
||||
## Setting up
|
||||
|
||||
### OpenID
|
||||
|
||||
Overwrite your `OpenIdSettings` section in your config.json file by running `make config-openid` and restarting your server.
|
||||
|
||||
- [Official OpenID with Keycloak documentation](https://docs.mattermost.com/onboard/sso-openidconnect.html)
|
||||
|
||||
### SAML
|
||||
|
||||
Overwrite your `SamlSettings` section in your config.json file by running `make config-saml` and restarting your server.
|
||||
|
||||
- [Official SAML with Keycloak documentation](https://docs.mattermost.com/onboard/sso-saml-keycloak.html)
|
||||
|
||||
### LDAP
|
||||
|
||||
Overwrite your `LdapSettings` section in your config.json file by running `make config-ldap` and restarting your server.
|
||||
|
||||
- [Official LDAP with Keycloak documentation](https://docs.mattermost.com/onboard/ad-ldap.html)
|
||||
|
||||
## Credentials to log in
|
||||
|
||||
- **Admin account**, used to log in to the Keycloak Admin UI:
|
||||
- `admin`/`admin`
|
||||
|
||||
- **User accounts**, used to log in to Mattermost:
|
||||
- `homer`/`password`
|
||||
- `marge`/`password`
|
||||
- `lisa`/`password`
|
||||
|
||||
## Updating the `realm-export.json`
|
||||
|
||||
The `realm-export.json` file is automatically imported by the keycloak development container. If you make any modifications to this file or to the base configuration, export it by running a terminal in the container and running:
|
||||
|
||||
```bash
|
||||
/opt/keycloak/bin/kc.sh export --realm mattermost --users realm_file --file /opt/keycloak/data/import/realm-export.json
|
||||
```
|
||||
5
build/docker/keycloak/kc-healthcheck.sh
Executable file
5
build/docker/keycloak/kc-healthcheck.sh
Executable file
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -eu -o pipefail
|
||||
|
||||
/opt/keycloak/bin/kcadm.sh config credentials -x --server http://localhost:8080 --realm master --user "$KEYCLOAK_ADMIN" --password "$KEYCLOAK_ADMIN_PASSWORD"
|
||||
/opt/keycloak/bin/kcadm.sh get realms/mattermost >/dev/null
|
||||
3
build/docker/keycloak/keycloak.crt
Normal file
3
build/docker/keycloak/keycloak.crt
Normal file
@ -0,0 +1,3 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICozCCAYsCBgGNzWfMwjANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDDAptYXR0ZXJtb3N0MB4XDTI0MDIyMTIwNDA0OFoXDTM0MDIyMTIwNDIyOFowFTETMBEGA1UEAwwKbWF0dGVybW9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOnsgNexkO5tbKkFXN+SdMUuLHbqdjZ9/JSnKrYPHLarf8801YDDzV8wI9jjdCCgq+xtKFKWlwU2rGpjPbefDLV1m7CSu0Iq+hNxDiBdX3wkEIK98piDpx+xYGL0aAbXn3nAlqFOWQJLKLM1I65ZmK31YZeVj4Kn01W4WfsvKHoxPjLPwPTug4HB6vaQXqEpzYYYHyuJKvIYNuVwo0WQdaPRXb0poZoYzOnoB6tOFrim6B7/chqtZeXQc7h6/FejBsV59aO5uATI0aAJw1twzjCNIiOeJLB2jlLuIMR3/Yaqr8IRpRXzcRPETpisWNilhV07ZBW0YL9ZwuU4sHWy+iMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAW4I1egm+czdnnZxTtth3cjCmLg/UsalUDKSfFOLAlnbe6TtVhP4DpAl+OaQO4+kdEKemLENPmh4ddaHUjSSbbCQZo8B7IjByEe7x3kQdj2ucQpA4bh0vGZ11pVhk5HfkGqAO+UVNQsyLpTmWXQ8SEbxcw6mlTM4SjuybqaGOva1LBscI158Uq5FOVT6TJaxCt3dQkBH0tK+vhRtIM13pNZ/+SFgecn16AuVdBfjjqXynefrSihQ20BZ3NTyjs/N5J2qvSwQ95JARZrlhfiS++L81u2N/0WWni9cXmHsdTLxRrDZjz2CXBNeFOBRio74klSx8tMK27/2lxMsEC7R+DA==
|
||||
-----END CERTIFICATE-----
|
||||
39
build/docker/keycloak/ldap.mmsettings.json
Normal file
39
build/docker/keycloak/ldap.mmsettings.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"LdapSettings": {
|
||||
"Enable": true,
|
||||
"EnableSync": false,
|
||||
"LdapServer": "localhost",
|
||||
"LdapPort": 389,
|
||||
"ConnectionSecurity": "",
|
||||
"BaseDN": "dc=mm,dc=test,dc=com",
|
||||
"BindUsername": "cn=admin,dc=mm,dc=test,dc=com",
|
||||
"BindPassword": "mostest",
|
||||
"UserFilter": "",
|
||||
"GroupFilter": "",
|
||||
"GuestFilter": "",
|
||||
"EnableAdminFilter": false,
|
||||
"AdminFilter": "",
|
||||
"GroupDisplayNameAttribute": "cn",
|
||||
"GroupIdAttribute": "entryUUID",
|
||||
"FirstNameAttribute": "cn",
|
||||
"LastNameAttribute": "sn",
|
||||
"EmailAttribute": "mail",
|
||||
"UsernameAttribute": "uid",
|
||||
"NicknameAttribute": "cn",
|
||||
"IdAttribute": "uid",
|
||||
"PositionAttribute": "title",
|
||||
"LoginIdAttribute": "uid",
|
||||
"PictureAttribute": "",
|
||||
"SyncIntervalMinutes": 60,
|
||||
"SkipCertificateVerification": false,
|
||||
"PublicCertificateFile": "",
|
||||
"PrivateKeyFile": "",
|
||||
"QueryTimeout": 60,
|
||||
"MaxPageSize": 0,
|
||||
"LoginFieldName": "",
|
||||
"LoginButtonColor": "#0000",
|
||||
"LoginButtonBorderColor": "#2389D7",
|
||||
"LoginButtonTextColor": "#2389D7",
|
||||
"Trace": false
|
||||
}
|
||||
}
|
||||
14
build/docker/keycloak/openid.mmsettings.json
Normal file
14
build/docker/keycloak/openid.mmsettings.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"OpenIdSettings": {
|
||||
"Enable": true,
|
||||
"Secret": "9Y7dykcoA9luTC77XtXxOu9UbNx3rhj6",
|
||||
"Id": "mattermost-openid",
|
||||
"Scope": "profile openid email",
|
||||
"AuthEndpoint": "",
|
||||
"TokenEndpoint": "",
|
||||
"UserAPIEndpoint": "",
|
||||
"DiscoveryEndpoint": "http://localhost:8484/realms/mattermost/.well-known/openid-configuration",
|
||||
"ButtonText": "Login using OpenID",
|
||||
"ButtonColor": "#ffaa4c"
|
||||
}
|
||||
}
|
||||
2057
build/docker/keycloak/realm-export.json
Normal file
2057
build/docker/keycloak/realm-export.json
Normal file
File diff suppressed because it is too large
Load Diff
38
build/docker/keycloak/saml.mmsettings.json
Normal file
38
build/docker/keycloak/saml.mmsettings.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"SamlSettings": {
|
||||
"Enable": true,
|
||||
"EnableSyncWithLdap": false,
|
||||
"EnableSyncWithLdapIncludeAuth": false,
|
||||
"IgnoreGuestsLdapSync": false,
|
||||
"Verify": false,
|
||||
"Encrypt": false,
|
||||
"SignRequest": false,
|
||||
"IdpURL": "http://localhost:8484/realms/mattermost/protocol/saml",
|
||||
"IdpDescriptorURL": "http://localhost:8484/realms/mattermost",
|
||||
"IdpMetadataURL": "http://localhost:8484/realms/mattermost/protocol/saml/descriptor",
|
||||
"ServiceProviderIdentifier": "mattermost",
|
||||
"AssertionConsumerServiceURL": "http://localhost:8065/login/sso/saml",
|
||||
"SignatureAlgorithm": "RSAwithSHA256",
|
||||
"CanonicalAlgorithm": "Canonical1.0",
|
||||
"ScopingIDPProviderId": "",
|
||||
"ScopingIDPName": "",
|
||||
"IdpCertificateFile": "saml-idp.crt",
|
||||
"PublicCertificateFile": "",
|
||||
"PrivateKeyFile": "",
|
||||
"IdAttribute": "id",
|
||||
"GuestAttribute": "",
|
||||
"EnableAdminAttribute": false,
|
||||
"AdminAttribute": "",
|
||||
"FirstNameAttribute": "givenName",
|
||||
"LastNameAttribute": "surname",
|
||||
"EmailAttribute": "email",
|
||||
"UsernameAttribute": "username",
|
||||
"NicknameAttribute": "",
|
||||
"LocaleAttribute": "",
|
||||
"PositionAttribute": "",
|
||||
"LoginButtonText": "SAML",
|
||||
"LoginButtonColor": "#34a28b",
|
||||
"LoginButtonBorderColor": "#2389D7",
|
||||
"LoginButtonTextColor": "#ffffff"
|
||||
}
|
||||
}
|
||||
38
build/docker/nginx/default.conf
Normal file
38
build/docker/nginx/default.conf
Normal file
@ -0,0 +1,38 @@
|
||||
upstream app_cluster {
|
||||
server leader:8065 fail_timeout=10s max_fails=10;
|
||||
server follower:8065 fail_timeout=10s max_fails=10;
|
||||
server follower2:8065 fail_timeout=10s max_fails=10;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 8065;
|
||||
|
||||
location ~ /api/v[0-9]+/(users/)?websocket$ {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_http_version 1.1;
|
||||
client_max_body_size 50M;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Frame-Options SAMEORIGIN;
|
||||
proxy_buffers 256 16k;
|
||||
proxy_buffer_size 16k;
|
||||
proxy_read_timeout 600s;
|
||||
proxy_pass http://app_cluster;
|
||||
}
|
||||
|
||||
location / {
|
||||
client_max_body_size 100M;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Frame-Options SAMEORIGIN;
|
||||
proxy_pass http://app_cluster;
|
||||
}
|
||||
}
|
||||
8
build/docker/postgres.conf
Normal file
8
build/docker/postgres.conf
Normal file
@ -0,0 +1,8 @@
|
||||
max_connections = 500
|
||||
listen_addresses = '*'
|
||||
fsync = off
|
||||
full_page_writes = off
|
||||
default_text_search_config = 'pg_catalog.english'
|
||||
commit_delay=1000
|
||||
logging_collector=off
|
||||
password_encryption = 'scram-sha-256'
|
||||
2
build/docker/postgres_node_database.sql
Normal file
2
build/docker/postgres_node_database.sql
Normal file
@ -0,0 +1,2 @@
|
||||
CREATE DATABASE mattermost_node_test;
|
||||
GRANT ALL PRIVILEGES ON DATABASE mattermost_node_test TO mmuser;
|
||||
18
build/docker/prometheus.yml
Normal file
18
build/docker/prometheus.yml
Normal file
@ -0,0 +1,18 @@
|
||||
global:
|
||||
scrape_interval: 5s
|
||||
evaluation_interval: 60s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: 'mattermost'
|
||||
static_configs:
|
||||
- targets: ['host.docker.internal:8067']
|
||||
- job_name: fmetrics_scrape
|
||||
docker_sd_configs:
|
||||
- host: unix:///var/run/docker.sock
|
||||
refresh_interval: 5s
|
||||
relabel_configs:
|
||||
- source_labels: ['__meta_docker_container_name']
|
||||
regex: '/(.*)'
|
||||
target_label: 'container'
|
||||
- source_labels: ['__meta_docker_container_log_stream']
|
||||
target_label: 'logstream'
|
||||
46
build/docker/promtail/promtail-local-config.yaml
Normal file
46
build/docker/promtail/promtail-local-config.yaml
Normal file
@ -0,0 +1,46 @@
|
||||
server:
|
||||
http_listen_port: 3180
|
||||
grpc_listen_port: 0
|
||||
|
||||
positions:
|
||||
filename: /tmp/positions.yaml
|
||||
|
||||
clients:
|
||||
- url: http://loki:3100/loki/api/v1/push
|
||||
|
||||
scrape_configs:
|
||||
- job_name: mattermost
|
||||
pipeline_stages:
|
||||
- match:
|
||||
selector: '{job="mattermost"}'
|
||||
stages:
|
||||
- json:
|
||||
expressions:
|
||||
timestamp: timestamp
|
||||
level: level
|
||||
- labels:
|
||||
level:
|
||||
- timestamp:
|
||||
format: '2006-01-02 15:04:05.999 -07:00'
|
||||
source: timestamp
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost
|
||||
labels:
|
||||
job: mattermost
|
||||
app: mattermost
|
||||
__path__: /logs/*.log
|
||||
# https://ruanbekker.medium.com/logging-with-docker-promtail-and-grafana-loki-d920fd790ca8
|
||||
- job_name: docker
|
||||
docker_sd_configs:
|
||||
- host: unix:///var/run/docker.sock
|
||||
refresh_interval: 5s
|
||||
relabel_configs:
|
||||
- source_labels: ['__meta_docker_container_name']
|
||||
regex: '/(.*)'
|
||||
target_label: 'container'
|
||||
- source_labels: ['__meta_docker_container_log_stream']
|
||||
target_label: 'logstream'
|
||||
pipeline_stages:
|
||||
- static_labels:
|
||||
job: docker
|
||||
1
build/dotenv/test-schema-validation.env
Normal file
1
build/dotenv/test-schema-validation.env
Normal file
@ -0,0 +1 @@
|
||||
IS_CI=true
|
||||
12
build/dotenv/test.env
Normal file
12
build/dotenv/test.env
Normal file
@ -0,0 +1,12 @@
|
||||
TEST_DATABASE_POSTGRESQL_DSN=postgres://mmuser:mostest@postgres:5432/mattermost_test?sslmode=disable&connect_timeout=10
|
||||
GOBIN=/mattermost/server/bin
|
||||
|
||||
CI_INBUCKET_HOST=inbucket
|
||||
CI_MINIO_HOST=minio
|
||||
CI_INBUCKET_PORT=9001
|
||||
CI_MINIO_PORT=9000
|
||||
CI_INBUCKET_SMTP_PORT=10025
|
||||
CI_LDAP_HOST=openldap
|
||||
IS_CI=true
|
||||
MM_EMAILSETTINGS_SMTPSERVER=inbucket
|
||||
MM_ELASTICSEARCHSETTINGS_CONNECTIONURL=http://elasticsearch:9200
|
||||
7
build/entrypoint.sh
Executable file
7
build/entrypoint.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "${1:0:1}" = '-' ]; then
|
||||
set -- mattermost "$@"
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
72
build/local-test-env.sh
Executable file
72
build/local-test-env.sh
Executable file
@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
|
||||
export COMPOSE_PROJECT_NAME=localdev
|
||||
local_cmdname=${0##*/}
|
||||
|
||||
usage()
|
||||
{
|
||||
cat << USAGE >&2
|
||||
Usage:
|
||||
$local_cmdname up/down
|
||||
USAGE
|
||||
exit 1
|
||||
}
|
||||
|
||||
up()
|
||||
{
|
||||
docker compose run --rm start_dependencies
|
||||
|
||||
docker compose exec openldap bash -c 'echo -e "dn: ou=testusers,dc=mm,dc=test,dc=com\nobjectclass: organizationalunit" | ldapadd -x -D "cn=admin,dc=mm,dc=test,dc=com" -w mostest'
|
||||
docker compose exec openldap bash -c 'echo -e "dn: uid=test.one,ou=testusers,dc=mm,dc=test,dc=com\nobjectclass: iNetOrgPerson\nsn: User\ncn: Test1\nmail: success+testone@simulator.amazonses.com" | ldapadd -x -D "cn=admin,dc=mm,dc=test,dc=com" -w mostest'
|
||||
docker compose exec openldap bash -c 'ldappasswd -s Password1 -D "cn=admin,dc=mm,dc=test,dc=com" -x "uid=test.one,ou=testusers,dc=mm,dc=test,dc=com" -w mostest'
|
||||
docker compose exec openldap bash -c 'echo -e "dn: uid=test.two,ou=testusers,dc=mm,dc=test,dc=com\nobjectclass: iNetOrgPerson\nsn: User\ncn: Test2\nmail: success+testtwo@simulator.amazonses.com" | ldapadd -x -D "cn=admin,dc=mm,dc=test,dc=com" -w mostest'
|
||||
docker compose exec openldap bash -c 'ldappasswd -s Password1 -D "cn=admin,dc=mm,dc=test,dc=com" -x "uid=test.two,ou=testusers,dc=mm,dc=test,dc=com" -w mostest'
|
||||
docker compose exec openldap bash -c 'echo -e "dn: cn=tgroup,ou=testusers,dc=mm,dc=test,dc=com\nobjectclass: groupOfUniqueNames\nuniqueMember: uid=test.one,ou=testusers,dc=mm,dc=test,dc=com" | ldapadd -x -D "cn=admin,dc=mm,dc=test,dc=com" -w mostest'
|
||||
|
||||
docker run -it -u root \
|
||||
--privileged \
|
||||
-v $GOPATH:/go \
|
||||
-w /go/src/github.com/mattermost/mattermost-server/ \
|
||||
--net ${COMPOSE_PROJECT_NAME}_mm-test \
|
||||
--env-file=dotenv/test.env
|
||||
-e GOPATH="/go" \
|
||||
-e MM_SQLSETTINGS_DATASOURCE="postgres://mmuser:mostest@postgres:5432/mattermost_test?sslmode=disable&connect_timeout=10" \
|
||||
-e MM_SQLSETTINGS_DRIVERNAME=postgres
|
||||
mattermost/mattermost-build-server:20210810_golang-1.16.7 bash
|
||||
}
|
||||
|
||||
down()
|
||||
{
|
||||
docker compose down
|
||||
}
|
||||
|
||||
# process arguments
|
||||
while [[ $# -gt 0 ]]
|
||||
do
|
||||
case "$1" in
|
||||
up)
|
||||
echo "Starting Containers"
|
||||
up
|
||||
break
|
||||
;;
|
||||
|
||||
down)
|
||||
echo "Stopping Containers"
|
||||
down
|
||||
break
|
||||
;;
|
||||
|
||||
--help)
|
||||
usage
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$1" == "" ]]; then
|
||||
usage
|
||||
fi
|
||||
28
build/notice-file/Readme.md
Normal file
28
build/notice-file/Readme.md
Normal file
@ -0,0 +1,28 @@
|
||||
# Notice.txt File Configuration
|
||||
|
||||
We are automatically generating Notice.txt by using first-level dependencies of the project. The related pipeline uses `config.yaml` stored in this folder.
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
Sample:
|
||||
|
||||
```
|
||||
title: "Mattermost Playbooks"
|
||||
copyright: "©2015-present Mattermost, Inc. All Rights Reserved. See LICENSE for license information."
|
||||
description: "This document includes a list of open source components used in Mattermost Playbooks, including those that have been modified."
|
||||
search:
|
||||
- "go.mod"
|
||||
- "client/go.mod"
|
||||
dependencies: []
|
||||
devDependencies: []
|
||||
```
|
||||
|
||||
| Field | Type | Purpose |
|
||||
| :-- | :-- | :-- |
|
||||
| title | string | Field content will be used as a title of the application. See first line of `NOTICE.txt` file. |
|
||||
| copyright | string | Field content will be used as a copyright message. See second line of `NOTICE.txt` file. |
|
||||
| description | string | Field content will be used as notice file description. See third line of `NOTICE.txt` file. |
|
||||
| dependencies | array | If any dependency name mentioned, it will be automatically added even if it is not a first-level dependency. |
|
||||
| devDependencies | array | If any dependency name mentioned, it will be added when it is referenced in devDependency section. |
|
||||
| search | array | Pipeline will search for package.json/go.mod files mentioned here. Globstar format is supported ie. `x/**/go.mod`. |
|
||||
11
build/notice-file/config.yaml
Normal file
11
build/notice-file/config.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
title: "Mattermost Server"
|
||||
copyright: "©2015-present Mattermost,Inc. All Rights Reserved. See LICENSE for license information."
|
||||
description: "This document includes a list of open source components used in Mattermost Server, including those that have been modified."
|
||||
search:
|
||||
- "server/go.mod"
|
||||
- "webapp/package.json"
|
||||
- "webapp/channels/package.json"
|
||||
ignoreDependencies:
|
||||
- "@mattermost/dynamic-virtualized-list"
|
||||
includeDevDependencies: false
|
||||
4
build/passwd
Normal file
4
build/passwd
Normal file
@ -0,0 +1,4 @@
|
||||
root:x:0:0:root:/root:/sbin/nologin
|
||||
nobody:x:65534:65534:nobody:/nonexistent:/sbin/nologin
|
||||
nonroot:x:65532:65532:nonroot:/home/nonroot:/sbin/nologin
|
||||
mattermost:x:2000:2000:mattermost:/mattermost:/sbin/nologin
|
||||
52
build/plugin-production-public-key.gpg
Normal file
52
build/plugin-production-public-key.gpg
Normal file
@ -0,0 +1,52 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBF3YTiEBEACgkhnZ5+xylKZhLVj193b6d/rSQuCU/zwWeZJnqyR8wRsPotXO
|
||||
CMXOUM9bTTaGfItCP9KlPPcyrshNEIgqcqhB6TSKkWSyrV5XS95Opd9Esbjw1VZq
|
||||
PMezLG3m36cytEmM82+Zpagm20/1PSQS2cQxEfieUDVkM7o4iE5TpifD6rgSaObQ
|
||||
njbV1z+yljWfK+1OvM4VLAQ4dC5U0NS9SwRcencGyTnw9Vynoc+tRyQn5R+RST8h
|
||||
rFEjpY11BsVJ+vAdZ7NoNH6P3gOTXI8lpcSn1G0lXJrJGyd45fGR8IT1imDIdmHN
|
||||
B/nIg9J+PtMmH3BuybtYzxIgZF3SKan1avirpZCKkiW/93UK02Z/aHgoth27fJLf
|
||||
kKNgO+5Rvf9TOdWujAgOnGHMedEslGzFv74tlnrGYvqo/hWhHuV6jFFEtZvI5LuL
|
||||
RD5lzZePEgN9XQ80In6LD1TAIOs3LPLvg7+Wxd9foc0qAMLUtmYjKG0/hmXPjrPY
|
||||
shuXUVCj0VD0RATdhWGKV8cWlOnh6elnUr9/BBZ32SP1ID76oLXjoPgy/jWxAvJf
|
||||
UqxYkwcGpEJqxywE5BAw4v29mz4nbzxLcpgK30soeIIu7A+tYtgMaJ89TPA53Hwt
|
||||
h+8+pPsVG2uB5Iz2SFiHQ1GymGpwxsqPRJosC5/6xsoKIqfvLFNCx83FhQARAQAB
|
||||
tClNYXR0ZXJtb3N0LCBJbmMuIDxzdXBwb3J0QG1hdHRlcm1vc3QuY29tPokCTgQT
|
||||
AQoAOBYhBMVYgbgPaehjuFrV0dG1S0elzv7EBQJd2E4hAhsDBQsJCAcDBRUKCQgL
|
||||
BRYCAwEAAh4BAheAAAoJENG1S0elzv7ETDsQAIthyS2tHHmZKVnWrwTuhcls88GI
|
||||
gTOzGzlUCyMH7mAQzv9PgtUz9LVDN1wiIRzqdVUc0ljKFF5urjLcg1t1ni3Ocy87
|
||||
ysE+bpOtBJcVK7fLV/XVAGmxtGblkWaCHaYFCXSqrXialXm588Bnsbfd+2HNPX63
|
||||
8mlfYgCIO5Kowlk2OPCACiB4TM8vOi0q6xV740hWfDor7BQo7jK/LL24EPGY4m30
|
||||
nMXHsZnyLCDvfJ8y3clb7ac0qCyF2g8npU2EVdE01IpYuOVCIZ8zenXKCHeTKisl
|
||||
ahUpm+WbhFGrys1cXLad1TaBVjw1aPmBA1PGh9yDCIxI4WBX5l0qqr3rxT7DdrBh
|
||||
YD7biejekWYlw/MDkK1bp6tryiR2H7kK4Jp8zUcrsBT0cIr7m2UdP/qChK7Z9jBA
|
||||
gUEMM0gmZYyaK7dqOESifg+67tm1D35qrD8lAb1xXjk76PdzkTleMJfpVgONs870
|
||||
GLsqbik2ZAWX/dP7g6k3TRMOkDW2TvnzKjK7K6ETMWFObB+k8FgOibdgmLyXfMAq
|
||||
IAEmCqhGv17qd+99xru76TCRIisnzG0/kSALBKny9w/nuUDx6xS4w7E8szEe5geg
|
||||
VJ3CxBNSOlA8YJo2BpCG9LH7O6l4gg65jGQZ//8LSJel7EXsFJQizwPTdh5T61xb
|
||||
ktH5ucon5W99mvpuuQINBF3YTiEBEAC3ottuAdWtXjZEQg1k9c3enb+aYlqcElY1
|
||||
5izMqha/t+ySZn4ZRjux/6soPDUEEH1kAasrewvAcG4dcFEhRUSv67AJ+Jc0dpEd
|
||||
RNcZBCYGVqo/PxHwSKdxQduKSd6Qwm19UPpRvw5ETV83mOcpUCiupS3Chx8pjQB0
|
||||
vxCf9L545dLOFppWKcAjB96Snhl9IuwluzoPjO2zuY5qCHfWtyw4n5XVfvMbkL3v
|
||||
KRaVHPheT6L4Hrr12D+BycYUE1yztWoX2wqMpRole3qpvkCQ46pPok/XLMIlCKgp
|
||||
Hc/RbnKKFCYjIhQEQLR9WhnBBnhhXS6lglgyHzTVOIMEAe6jXDfSb0pk29g6NTjW
|
||||
7YycWKNO6Cwzpnbu/fHOzeP4oBURKAzPBgGBRzTZiBtMoViQy3l6HoCsWhMeyez7
|
||||
5jvk3ZrRvijNenE6gzuMsN6LnKgLGRMRYerDP466uZmp6qrVQbAgHSWZzK7j2fv8
|
||||
NsJw+Abf/sN+UrDPZqxLnSvMy2qtr+ZXAEjfVEerN9OSyC5wnEpRoPbV3vSsjImo
|
||||
JWeVtxPUBubTGns92/9CjV0jHPQofpKnWqFPh0OzNCZWtFdv5J1FrMnZo+sg1vTb
|
||||
4Vkx3SXg69JriicmbD5Ohd0cfcDfhAB/Ip5W/uNOUNWDHfFdInCywMzILrNJNPYU
|
||||
aqj2uxdKlwARAQABiQI2BBgBCgAgFiEExViBuA9p6GO4WtXR0bVLR6XO/sQFAl3Y
|
||||
TiECGwwACgkQ0bVLR6XO/sRHIw//WwLthCOZABhq1H+wvF8MPmkKE/JOBaLCYgCz
|
||||
vIZ2aOeDilKEfQdhjv4257VuKet+VjC7dnWafVi7S51PIgmrOh3VUT9+RDB3dDTZ
|
||||
OT3uD4xpNdbkgfIzRo0FgOGYpnE6SBxF9Tr9U9ENjyfqrt1w0odvuVp1WK1jJhDz
|
||||
HWRqDyNrdMYLS/VsgXv5Pz8XoFpKryyOYqG/x3rU62cZ5McFBu/b9dVyyZuxfmKV
|
||||
rhe5zXKIaBVnv9G7U9+1Zqbkhbfj5irne4hwuGT9RHRkxfuM0+YW0nopIniKYDFI
|
||||
F7mX2QHf7HN4jip8N+vht6fsRe1UY9Ktp6jOlNkCJEAhhoLI/bOenop3i07xAMry
|
||||
gHk/FGNbpM9M7oCn7XDRTr4afw0JUIFQQ9/NUPv5Ktjf3CAuY1NCivzSIqRwEnsc
|
||||
MO/8PtKxycbLAxfZe3QIp/bD9czU3F00rM4JIc+sBS5l373BJY3gqqpKd6/Oe1uK
|
||||
Pl1UP/KBMTTBnpcwvRnAXzg7petoNojBnbtmRzsHoNwNYVRu7Gpscb1WU/+IgBe6
|
||||
VbpFWCb4naPYMMhl/2afOKtmASJnw3MQkMXlTyf59+NoYRtAEh7mQ2AzBqYcsYbs
|
||||
ue8bSAG/lgJ7XKl4JyfhvC/63CTi/xMklsW5BGQn77vGjKo0bv5Oi1XfqhEujFqY
|
||||
d+eYY64=
|
||||
=UePK
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
257
build/release.mk
Normal file
257
build/release.mk
Normal file
@ -0,0 +1,257 @@
|
||||
dist: | check-style test package
|
||||
|
||||
build-linux: build-linux-amd64 build-linux-arm64
|
||||
|
||||
build-linux-amd64:
|
||||
@echo Build Linux amd64
|
||||
ifeq ($(BUILDER_GOOS_GOARCH),"linux_amd64")
|
||||
env GOOS=linux GOARCH=amd64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./...
|
||||
else
|
||||
mkdir -p $(GOBIN)/linux_amd64
|
||||
env GOOS=linux GOARCH=amd64 $(GO) build -o $(GOBIN)/linux_amd64 $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./...
|
||||
endif
|
||||
ifeq ($(FIPS_ENABLED),true)
|
||||
@echo Verifying Build Linux amd64 for FIPS
|
||||
$(GO) version -m $(GOBIN)/$(MM_BIN_NAME) | grep -q "GOEXPERIMENT=systemcrypto" || (echo "ERROR: FIPS mattermost binary missing GOEXPERIMENT=systemcrypto" && exit 1)
|
||||
$(GO) version -m $(GOBIN)/$(MM_BIN_NAME) | grep "\-tags" | grep -q "requirefips" || (echo "ERROR: FIPS mattermost binary missing -tags=requirefips" && exit 1)
|
||||
$(GO) tool nm $(GOBIN)/$(MM_BIN_NAME) | grep -q "func_go_openssl_OpenSSL_version" || (echo "ERROR: FIPS mattermost binary missing OpenSSL integration" && exit 1)
|
||||
$(GO) version -m $(GOBIN)/$(MMCTL_BIN_NAME) | grep -q "GOEXPERIMENT=systemcrypto" || (echo "ERROR: FIPS mmctl binary missing GOEXPERIMENT=systemcrypto" && exit 1)
|
||||
$(GO) version -m $(GOBIN)/$(MMCTL_BIN_NAME) | grep "\-tags" | grep -q "requirefips" || (echo "ERROR: FIPS mmctl binary missing -tags=requirefips" && exit 1)
|
||||
$(GO) tool nm $(GOBIN)/$(MMCTL_BIN_NAME) | grep -q "func_go_openssl_OpenSSL_version" || (echo "ERROR: FIPS mmctl binary missing OpenSSL integration" && exit 1)
|
||||
endif
|
||||
|
||||
build-linux-arm64:
|
||||
ifeq ($(FIPS_ENABLED),true)
|
||||
@echo Skipping Build Linux arm64 for FIPS
|
||||
else
|
||||
@echo Build Linux arm64
|
||||
ifeq ($(BUILDER_GOOS_GOARCH),"linux_arm64")
|
||||
env GOOS=linux GOARCH=arm64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./...
|
||||
else
|
||||
mkdir -p $(GOBIN)/linux_arm64
|
||||
env GOOS=linux GOARCH=arm64 $(GO) build -o $(GOBIN)/linux_arm64 $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./...
|
||||
endif
|
||||
endif
|
||||
|
||||
build-osx:
|
||||
@echo Build OSX amd64
|
||||
ifeq ($(BUILDER_GOOS_GOARCH),"darwin_amd64")
|
||||
env GOOS=darwin GOARCH=amd64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./...
|
||||
else
|
||||
mkdir -p $(GOBIN)/darwin_amd64
|
||||
env GOOS=darwin GOARCH=amd64 $(GO) build -o $(GOBIN)/darwin_amd64 $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./...
|
||||
endif
|
||||
@echo Build OSX arm64
|
||||
ifeq ($(BUILDER_GOOS_GOARCH),"darwin_arm64")
|
||||
env GOOS=darwin GOARCH=arm64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./...
|
||||
else
|
||||
mkdir -p $(GOBIN)/darwin_arm64
|
||||
env GOOS=darwin GOARCH=arm64 $(GO) build -o $(GOBIN)/darwin_arm64 $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./...
|
||||
endif
|
||||
|
||||
build-windows:
|
||||
@echo Build Windows amd64
|
||||
ifeq ($(BUILDER_GOOS_GOARCH),"windows_amd64")
|
||||
env GOOS=windows GOARCH=amd64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./...
|
||||
else
|
||||
mkdir -p $(GOBIN)/windows_amd64
|
||||
env GOOS=windows GOARCH=amd64 $(GO) build -o $(GOBIN)/windows_amd64 $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./...
|
||||
endif
|
||||
|
||||
build-cmd-linux:
|
||||
@echo Build CMD Linux amd64
|
||||
ifeq ($(BUILDER_GOOS_GOARCH),"linux_amd64")
|
||||
env GOOS=linux GOARCH=amd64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./cmd/...
|
||||
else
|
||||
mkdir -p $(GOBIN)/linux_amd64
|
||||
env GOOS=linux GOARCH=amd64 $(GO) build -o $(GOBIN)/linux_amd64 $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./cmd/...
|
||||
endif
|
||||
ifeq ($(FIPS_ENABLED),true)
|
||||
@echo Verifying Build Linux amd64 for FIPS
|
||||
$(GO) version -m $(GOBIN)/mattermost | grep -q "GOEXPERIMENT=systemcrypto" || (echo "ERROR: FIPS mattermost binary missing GOEXPERIMENT=systemcrypto" && exit 1)
|
||||
$(GO) version -m $(GOBIN)/mattermost | grep "\-tags" | grep -q "requirefips" || (echo "ERROR: FIPS mattermost binary missing -tags=requirefips" && exit 1)
|
||||
$(GO) tool nm $(GOBIN)/mattermost | grep -q "func_go_openssl_OpenSSL_version" || (echo "ERROR: FIPS mattermost binary missing OpenSSL integration" && exit 1)
|
||||
$(GO) version -m $(GOBIN)/mmctl | grep -q "GOEXPERIMENT=systemcrypto" || (echo "ERROR: FIPS mmctl binary missing GOEXPERIMENT=systemcrypto" && exit 1)
|
||||
$(GO) version -m $(GOBIN)/mmctl | grep "\-tags" | grep -q "requirefips" || (echo "ERROR: FIPS mmctl binary missing -tags=requirefips" && exit 1)
|
||||
$(GO) tool nm $(GOBIN)/mmctl | grep -q "func_go_openssl_OpenSSL_version" || (echo "ERROR: FIPS mmctl binary missing OpenSSL integration" && exit 1)
|
||||
endif
|
||||
ifeq ($(FIPS_ENABLED),true)
|
||||
@echo Skipping Build Linux arm64 for FIPS
|
||||
else
|
||||
@echo Build CMD Linux arm64
|
||||
ifeq ($(BUILDER_GOOS_GOARCH),"linux_arm64")
|
||||
env GOOS=linux GOARCH=arm64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./cmd/...
|
||||
else
|
||||
mkdir -p $(GOBIN)/linux_arm64
|
||||
env GOOS=linux GOARCH=arm64 $(GO) build -o $(GOBIN)/linux_arm64 $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./cmd/...
|
||||
endif
|
||||
endif
|
||||
|
||||
build-cmd-osx:
|
||||
@echo Build CMD OSX amd64
|
||||
ifeq ($(BUILDER_GOOS_GOARCH),"darwin_amd64")
|
||||
env GOOS=darwin GOARCH=amd64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./cmd/...
|
||||
else
|
||||
mkdir -p $(GOBIN)/darwin_amd64
|
||||
env GOOS=darwin GOARCH=amd64 $(GO) build -o $(GOBIN)/darwin_amd64 $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./cmd/...
|
||||
endif
|
||||
@echo Build CMD OSX arm64
|
||||
ifeq ($(BUILDER_GOOS_GOARCH),"darwin_arm64")
|
||||
env GOOS=darwin GOARCH=arm64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./cmd/...
|
||||
else
|
||||
mkdir -p $(GOBIN)/darwin_arm64
|
||||
env GOOS=darwin GOARCH=arm64 $(GO) build -o $(GOBIN)/darwin_arm64 $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./cmd/...
|
||||
endif
|
||||
|
||||
build-cmd-windows:
|
||||
@echo Build CMD Windows amd64
|
||||
ifeq ($(BUILDER_GOOS_GOARCH),"windows_amd64")
|
||||
env GOOS=windows GOARCH=amd64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./cmd/...
|
||||
else
|
||||
mkdir -p $(GOBIN)/windows_amd64
|
||||
env GOOS=windows GOARCH=amd64 $(GO) build -o $(GOBIN)/windows_amd64 $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./cmd/...
|
||||
endif
|
||||
|
||||
# Only build linux by default. Other platforms can be built by specifying the platform.
|
||||
build: setup-go-work build-client build-linux
|
||||
|
||||
# Only build linux by default. Other platforms can be built by specifying the platform.
|
||||
build-cmd: setup-go-work build-client build-cmd-linux
|
||||
|
||||
build-client:
|
||||
@echo Building mattermost web app
|
||||
|
||||
cd $(BUILD_WEBAPP_DIR) && $(MAKE) dist
|
||||
|
||||
package-prep:
|
||||
@ echo Packaging mattermost
|
||||
@# Remove any old files
|
||||
rm -Rf $(DIST_ROOT)
|
||||
|
||||
@# Resource directories
|
||||
mkdir -p $(DIST_PATH)/config
|
||||
cp -L config/README.md $(DIST_PATH)/config
|
||||
OUTPUT_CONFIG=$(PWD)/$(DIST_PATH)/config/config.json go run ./scripts/config_generator
|
||||
cp -RL fonts $(DIST_PATH)
|
||||
cp -RL templates $(DIST_PATH)
|
||||
rm -rf $(DIST_PATH)/templates/*.mjml $(DIST_PATH)/templates/partials/
|
||||
cp -RL i18n $(DIST_PATH)
|
||||
|
||||
@# Disable developer settings
|
||||
sed -i'' -e 's|"ConsoleLevel": "DEBUG"|"ConsoleLevel": "INFO"|g' $(DIST_PATH)/config/config.json
|
||||
sed -i'' -e 's|"SiteURL": "http://localhost:8065"|"SiteURL": ""|g' $(DIST_PATH)/config/config.json
|
||||
|
||||
@# Reset email sending to original configuration
|
||||
sed -i'' -e 's|"SendEmailNotifications": true,|"SendEmailNotifications": false,|g' $(DIST_PATH)/config/config.json
|
||||
sed -i'' -e 's|"FeedbackEmail": "test@example.com",|"FeedbackEmail": "",|g' $(DIST_PATH)/config/config.json
|
||||
sed -i'' -e 's|"ReplyToAddress": "test@example.com",|"ReplyToAddress": "",|g' $(DIST_PATH)/config/config.json
|
||||
sed -i'' -e 's|"SMTPServer": "localhost",|"SMTPServer": "",|g' $(DIST_PATH)/config/config.json
|
||||
sed -i'' -e 's|"SMTPPort": "2500",|"SMTPPort": "",|g' $(DIST_PATH)/config/config.json
|
||||
chmod 600 $(DIST_PATH)/config/config.json
|
||||
|
||||
@# Package web app
|
||||
mkdir -p $(DIST_PATH)/client
|
||||
cp -RL $(BUILD_WEBAPP_DIR)/channels/dist/* $(DIST_PATH)/client
|
||||
|
||||
@# Help files
|
||||
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
||||
cp $(BUILD_ENTERPRISE_DIR)/ENTERPRISE-EDITION-LICENSE.txt $(DIST_PATH)
|
||||
cp -L $(BUILD_ENTERPRISE_DIR)/cloud/config/cloud_defaults.json $(DIST_PATH)/config
|
||||
else
|
||||
cp build/MIT-COMPILED-LICENSE.md $(DIST_PATH)
|
||||
endif
|
||||
cp ../NOTICE.txt $(DIST_PATH)
|
||||
cp ../README.md $(DIST_PATH)
|
||||
if [ -f bin/manifest.txt ]; then \
|
||||
cp bin/manifest.txt $(DIST_PATH); \
|
||||
fi
|
||||
|
||||
fetch-prepackaged-plugins:
|
||||
@# Import Mattermost plugin public key, ignoring errors. In FIPS mode, GPG fails to start
|
||||
@# the gpg-agent, but still imports the key. If it really fails, it will fail validation later.
|
||||
-gpg --import build/plugin-production-public-key.gpg
|
||||
@# Download prepackaged plugins
|
||||
mkdir -p tmpprepackaged
|
||||
@echo "Downloading prepackaged plugins ... "
|
||||
@cd tmpprepackaged && for plugin_package in $(PLUGIN_PACKAGES) ; do \
|
||||
curl -f -O -L https://plugins.releases.mattermost.com/release/$$plugin_package-$(PLUGIN_ARCH).tar.gz; \
|
||||
curl -f -O -L https://plugins.releases.mattermost.com/release/$$plugin_package-$(PLUGIN_ARCH).tar.gz.sig; \
|
||||
done
|
||||
@echo "Done"
|
||||
|
||||
package-general:
|
||||
@# Create needed directories
|
||||
mkdir -p $(DIST_PATH_GENERIC)/bin
|
||||
mkdir -p $(DIST_PATH_GENERIC)/logs
|
||||
|
||||
@# Copy binaries
|
||||
ifeq ($(BUILDER_GOOS_GOARCH),"$(CURRENT_PACKAGE_ARCH)")
|
||||
cp $(GOBIN)/$(MM_BIN_NAME) $(GOBIN)/$(MMCTL_BIN_NAME) $(DIST_PATH_GENERIC)/bin # from native bin dir, not cross-compiled
|
||||
else
|
||||
cp $(GOBIN)/$(CURRENT_PACKAGE_ARCH)/$(MM_BIN_NAME) $(GOBIN)/$(CURRENT_PACKAGE_ARCH)/$(MMCTL_BIN_NAME) $(DIST_PATH_GENERIC)/bin # from cross-compiled bin dir
|
||||
endif
|
||||
|
||||
package-plugins: fetch-prepackaged-plugins
|
||||
@# Create needed directories
|
||||
mkdir -p $(DIST_PATH_GENERIC)/prepackaged_plugins
|
||||
|
||||
@# Prepackage plugins
|
||||
@for plugin_package in $(PLUGIN_PACKAGES) ; do \
|
||||
ARCH=$(PLUGIN_ARCH); \
|
||||
cp tmpprepackaged/$$plugin_package-$$ARCH.tar.gz $(DIST_PATH_GENERIC)/prepackaged_plugins; \
|
||||
cp tmpprepackaged/$$plugin_package-$$ARCH.tar.gz.sig $(DIST_PATH_GENERIC)/prepackaged_plugins; \
|
||||
gpg --verify $(DIST_PATH_GENERIC)/prepackaged_plugins/$$plugin_package-$$ARCH.tar.gz.sig $(DIST_PATH_GENERIC)/prepackaged_plugins/$$plugin_package-$$ARCH.tar.gz; \
|
||||
if [ $$? -ne 0 ]; then \
|
||||
echo "Failed to verify $$plugin_package-$$ARCH.tar.gz|$$plugin_package-$$ARCH.tar.gz.sig"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
done
|
||||
|
||||
package-osx-amd64: package-prep
|
||||
DIST_PATH_GENERIC=$(DIST_PATH_OSX_AMD64) CURRENT_PACKAGE_ARCH=darwin_amd64 MM_BIN_NAME=mattermost MMCTL_BIN_NAME=mmctl $(MAKE) package-general
|
||||
@# Package
|
||||
tar -C $(DIST_PATH_OSX_AMD64)/.. -czf $(DIST_PATH)-$(BUILD_TYPE_NAME)-darwin-amd64.tar.gz mattermost ../mattermost
|
||||
@# Cleanup
|
||||
rm -rf $(DIST_ROOT)/darwin_amd64
|
||||
|
||||
package-osx-arm64: package-prep
|
||||
DIST_PATH_GENERIC=$(DIST_PATH_OSX_ARM64) CURRENT_PACKAGE_ARCH=darwin_arm64 MM_BIN_NAME=mattermost MMCTL_BIN_NAME=mmctl $(MAKE) package-general
|
||||
@# Package
|
||||
tar -C $(DIST_PATH_OSX_ARM64)/.. -czf $(DIST_PATH)-$(BUILD_TYPE_NAME)-darwin-arm64.tar.gz mattermost ../mattermost
|
||||
@# Cleanup
|
||||
rm -rf $(DIST_ROOT)/darwin_arm64
|
||||
|
||||
package-osx: package-osx-amd64 package-osx-arm64
|
||||
|
||||
package-linux-amd64: package-prep
|
||||
DIST_PATH_GENERIC=$(DIST_PATH_LIN_AMD64) PLUGIN_ARCH=linux-amd64 $(MAKE) package-plugins
|
||||
DIST_PATH_GENERIC=$(DIST_PATH_LIN_AMD64) CURRENT_PACKAGE_ARCH=linux_amd64 MM_BIN_NAME=mattermost MMCTL_BIN_NAME=mmctl $(MAKE) package-general
|
||||
@# Package
|
||||
tar -C $(DIST_PATH_LIN_AMD64)/.. -czf $(DIST_PATH)-$(BUILD_TYPE_NAME)-linux-amd64.tar.gz mattermost ../mattermost
|
||||
@# Cleanup
|
||||
rm -rf $(DIST_ROOT)/linux_amd64
|
||||
|
||||
package-linux-arm64: package-prep
|
||||
ifeq ($(FIPS_ENABLED),true)
|
||||
@echo Skipping package linux arm64 for FIPS
|
||||
else
|
||||
DIST_PATH_GENERIC=$(DIST_PATH_LIN_ARM64) CURRENT_PACKAGE_ARCH=linux_arm64 MM_BIN_NAME=mattermost MMCTL_BIN_NAME=mmctl $(MAKE) package-general
|
||||
@# Package
|
||||
tar -C $(DIST_PATH_LIN_ARM64)/.. -czf $(DIST_PATH)-$(BUILD_TYPE_NAME)-linux-arm64.tar.gz mattermost ../mattermost
|
||||
@# Cleanup
|
||||
rm -rf $(DIST_ROOT)/linux_arm64
|
||||
endif
|
||||
|
||||
package-linux: package-linux-amd64 package-linux-arm64
|
||||
|
||||
package-windows: package-prep
|
||||
DIST_PATH_GENERIC=$(DIST_PATH_WIN) CURRENT_PACKAGE_ARCH=windows_amd64 MM_BIN_NAME=mattermost.exe MMCTL_BIN_NAME=mmctl.exe $(MAKE) package-general
|
||||
@# Package
|
||||
cd $(DIST_PATH_WIN)/.. && zip -9 -r -q -l ../mattermost-$(BUILD_TYPE_NAME)-windows-amd64.zip mattermost ../mattermost && cd ../..
|
||||
@# Cleanup
|
||||
rm -rf $(DIST_ROOT)/windows
|
||||
|
||||
# Only package linux by default. Other platforms can be packaged by specifying the platform.
|
||||
package: package-linux
|
||||
rm -rf tmpprepackaged
|
||||
rm -rf $(DIST_PATH)
|
||||
721
channels/api4/access_control.go
Normal file
721
channels/api4/access_control.go
Normal file
@ -0,0 +1,721 @@
|
||||
// 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/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitAccessControlPolicy() {
|
||||
if !api.srv.Config().FeatureFlags.AttributeBasedAccessControl {
|
||||
return
|
||||
}
|
||||
api.BaseRoutes.AccessControlPolicies.Handle("", api.APISessionRequired(createAccessControlPolicy)).Methods(http.MethodPut)
|
||||
api.BaseRoutes.AccessControlPolicies.Handle("/search", api.APISessionRequired(searchAccessControlPolicies)).Methods(http.MethodPost)
|
||||
|
||||
api.BaseRoutes.AccessControlPolicies.Handle("/cel/check", api.APISessionRequired(checkExpression)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.AccessControlPolicies.Handle("/cel/test", api.APISessionRequired(testExpression)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.AccessControlPolicies.Handle("/cel/validate_requester", api.APISessionRequired(validateExpressionAgainstRequester)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.AccessControlPolicies.Handle("/cel/autocomplete/fields", api.APISessionRequired(getFieldsAutocomplete)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.AccessControlPolicies.Handle("/cel/visual_ast", api.APISessionRequired(convertToVisualAST)).Methods(http.MethodPost)
|
||||
|
||||
api.BaseRoutes.AccessControlPolicy.Handle("", api.APISessionRequired(getAccessControlPolicy)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.AccessControlPolicy.Handle("", api.APISessionRequired(deleteAccessControlPolicy)).Methods(http.MethodDelete)
|
||||
api.BaseRoutes.AccessControlPolicy.Handle("/activate", api.APISessionRequired(updateActiveStatus)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.AccessControlPolicy.Handle("/assign", api.APISessionRequired(assignAccessPolicy)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.AccessControlPolicy.Handle("/unassign", api.APISessionRequired(unassignAccessPolicy)).Methods(http.MethodDelete)
|
||||
api.BaseRoutes.AccessControlPolicy.Handle("/resources/channels", api.APISessionRequired(getChannelsForAccessControlPolicy)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.AccessControlPolicy.Handle("/resources/channels/search", api.APISessionRequired(searchChannelsForAccessControlPolicy)).Methods(http.MethodPost)
|
||||
}
|
||||
|
||||
func createAccessControlPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var policy model.AccessControlPolicy
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&policy); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("policy", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventCreateAccessControlPolicy, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "requested", &policy)
|
||||
|
||||
switch policy.Type {
|
||||
case model.AccessControlPolicyTypeParent:
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
case model.AccessControlPolicyTypeChannel:
|
||||
// Check if user has system admin permission first
|
||||
hasManageSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem)
|
||||
|
||||
if !hasManageSystemPermission {
|
||||
// For non-system admins, check channel-specific permission
|
||||
if !model.IsValidId(policy.ID) {
|
||||
c.SetInvalidParam("policy.id")
|
||||
return
|
||||
}
|
||||
|
||||
hasChannelPermission := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, policy.ID, model.PermissionManageChannelAccessRules)
|
||||
if !hasChannelPermission {
|
||||
c.SetPermissionError(model.PermissionManageChannelAccessRules)
|
||||
return
|
||||
}
|
||||
|
||||
// Now do the full validation (channel exists, is private, etc.)
|
||||
if appErr := c.App.ValidateChannelAccessControlPolicyCreation(c.AppContext, c.AppContext.Session().UserId, &policy); appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
}
|
||||
default:
|
||||
c.SetInvalidParam("type")
|
||||
return
|
||||
}
|
||||
|
||||
np, appErr := c.App.CreateOrUpdateAccessControlPolicy(c.AppContext, &policy)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventObjectType("access_control_policy")
|
||||
auditRec.AddEventResultState(np)
|
||||
|
||||
js, err := json.Marshal(np)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("createAccessControlPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := w.Write(js); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getAccessControlPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequirePolicyId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
policyID := c.Params.PolicyId
|
||||
|
||||
// Extract optional channelId from query parameters for context
|
||||
channelID := r.URL.Query().Get("channelId")
|
||||
|
||||
// Check if user has system admin permission OR channel-specific permission
|
||||
hasManageSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem)
|
||||
if !hasManageSystemPermission {
|
||||
// For non-system admins, validate policy access permission (read-only access for GET requests)
|
||||
if appErr := c.App.ValidateAccessControlPolicyPermissionWithChannelContext(c.AppContext, c.AppContext.Session().UserId, policyID, true, channelID); appErr != nil {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
policy, appErr := c.App.GetAccessControlPolicy(c.AppContext, policyID)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(policy)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getAccessControlPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := w.Write(js); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func deleteAccessControlPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequirePolicyId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
policyID := c.Params.PolicyId
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventDeleteAccessControlPolicy, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "id", policyID)
|
||||
|
||||
// Check if user has system admin permission OR channel-specific permission
|
||||
hasManageSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem)
|
||||
if !hasManageSystemPermission {
|
||||
// For non-system admins, validate policy access permission
|
||||
if appErr := c.App.ValidateAccessControlPolicyPermission(c.AppContext, c.AppContext.Session().UserId, policyID); appErr != nil {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
appErr := c.App.DeleteAccessControlPolicy(c.AppContext, policyID)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
func checkExpression(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// request type reserved for future expansion
|
||||
// for now, we only support the expression check
|
||||
checkExpressionRequest := struct {
|
||||
Expression string `json:"expression"`
|
||||
ChannelId string `json:"channelId,omitempty"`
|
||||
}{}
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&checkExpressionRequest); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("user", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
// Get channelId from request body (required for channel-specific permission check)
|
||||
channelId := checkExpressionRequest.ChannelId
|
||||
if channelId != "" && !model.IsValidId(channelId) {
|
||||
c.SetInvalidParam("channelId")
|
||||
return
|
||||
}
|
||||
|
||||
// Check permissions: system admin OR channel-specific permission
|
||||
hasSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem)
|
||||
if !hasSystemPermission {
|
||||
// For channel admins, channelId is required
|
||||
if channelId == "" {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
// SECURE: Check specific channel permission
|
||||
hasChannelPermission := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, channelId, model.PermissionManageChannelAccessRules)
|
||||
if !hasChannelPermission {
|
||||
c.SetPermissionError(model.PermissionManageChannelAccessRules)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
errs, appErr := c.App.CheckExpression(c.AppContext, checkExpressionRequest.Expression)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(errs)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("checkExpression", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := w.Write(js); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func testExpression(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var checkExpressionRequest model.QueryExpressionParams
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&checkExpressionRequest); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("user", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
// Get channelId from request body (required for channel-specific permission check)
|
||||
channelId := checkExpressionRequest.ChannelId
|
||||
if channelId != "" && !model.IsValidId(channelId) {
|
||||
c.SetInvalidParam("channelId")
|
||||
return
|
||||
}
|
||||
|
||||
// Check permissions: system admin OR channel-specific permission
|
||||
hasSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem)
|
||||
if !hasSystemPermission {
|
||||
// For channel admins, channelId is required
|
||||
if channelId == "" {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
// SECURE: Check specific channel permission
|
||||
hasChannelPermission := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, channelId, model.PermissionManageChannelAccessRules)
|
||||
if !hasChannelPermission {
|
||||
c.SetPermissionError(model.PermissionManageChannelAccessRules)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var users []*model.User
|
||||
var count int64
|
||||
var appErr *model.AppError
|
||||
|
||||
searchOpts := model.SubjectSearchOptions{
|
||||
Term: checkExpressionRequest.Term,
|
||||
Limit: checkExpressionRequest.Limit,
|
||||
Cursor: model.SubjectCursor{
|
||||
TargetID: checkExpressionRequest.After,
|
||||
},
|
||||
}
|
||||
|
||||
if hasSystemPermission {
|
||||
// SYSTEM ADMIN: Can see ALL users (no restrictions)
|
||||
users, count, appErr = c.App.TestExpression(c.AppContext, checkExpressionRequest.Expression, searchOpts)
|
||||
} else {
|
||||
// CHANNEL ADMIN: Only see users matching expressions with attributes they possess
|
||||
users, count, appErr = c.App.TestExpressionWithChannelContext(c.AppContext, checkExpressionRequest.Expression, searchOpts)
|
||||
}
|
||||
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
resp := model.AccessControlPolicyTestResponse{
|
||||
Users: users,
|
||||
Total: count,
|
||||
}
|
||||
|
||||
js, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("checkExpression", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := w.Write(js); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func validateExpressionAgainstRequester(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var request struct {
|
||||
Expression string `json:"expression"`
|
||||
ChannelId string `json:"channelId,omitempty"`
|
||||
}
|
||||
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&request); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("request", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
// Get channelId from request body (required for channel-specific permission check)
|
||||
channelId := request.ChannelId
|
||||
if channelId != "" && !model.IsValidId(channelId) {
|
||||
c.SetInvalidParam("channelId")
|
||||
return
|
||||
}
|
||||
|
||||
// Check permissions: system admin OR channel-specific permission
|
||||
hasSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem)
|
||||
if !hasSystemPermission {
|
||||
// For channel admins, channelId is required
|
||||
if channelId == "" {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
// SECURE: Check specific channel permission
|
||||
hasChannelPermission := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, channelId, model.PermissionManageChannelAccessRules)
|
||||
if !hasChannelPermission {
|
||||
c.SetPermissionError(model.PermissionManageChannelAccessRules)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Direct validation against requester
|
||||
matches, appErr := c.App.ValidateExpressionAgainstRequester(c.AppContext, request.Expression, c.AppContext.Session().UserId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
response := struct {
|
||||
RequesterMatches bool `json:"requester_matches"`
|
||||
}{
|
||||
RequesterMatches: matches,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func searchAccessControlPolicies(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
var props *model.AccessControlPolicySearch
|
||||
err := json.NewDecoder(r.Body).Decode(&props)
|
||||
if err != nil || props == nil {
|
||||
c.SetInvalidParamWithErr("access_control_policy_search", err)
|
||||
return
|
||||
}
|
||||
|
||||
policies, total, appErr := c.App.SearchAccessControlPolicies(c.AppContext, *props)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
result := model.AccessControlPoliciesWithCount{
|
||||
Policies: policies,
|
||||
Total: total,
|
||||
}
|
||||
|
||||
js, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("searchAccessControlPolicies", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := w.Write(js); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func updateActiveStatus(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequirePolicyId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
policyID := c.Params.PolicyId
|
||||
|
||||
// Check if user has system admin permission OR channel-specific permission for this policy
|
||||
hasManageSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem)
|
||||
if !hasManageSystemPermission {
|
||||
// For non-system admins, validate policy access permission
|
||||
if appErr := c.App.ValidateAccessControlPolicyPermission(c.AppContext, c.AppContext.Session().UserId, policyID); appErr != nil {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventUpdateActiveStatus, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "id", policyID)
|
||||
|
||||
active := r.URL.Query().Get("active")
|
||||
if active != "true" && active != "false" {
|
||||
c.SetInvalidParam("active")
|
||||
return
|
||||
}
|
||||
activeBool, err := strconv.ParseBool(active)
|
||||
if err != nil {
|
||||
c.SetInvalidParamWithErr("active", err)
|
||||
return
|
||||
}
|
||||
model.AddEventParameterToAuditRec(auditRec, "active", activeBool)
|
||||
|
||||
appErr := c.App.UpdateAccessControlPolicyActive(c.AppContext, policyID, activeBool)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
// Return success response
|
||||
response := map[string]any{
|
||||
"status": "OK",
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func assignAccessPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
c.RequirePolicyId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
policyID := c.Params.PolicyId
|
||||
|
||||
var assignments struct {
|
||||
ChannelIds []string `json:"channel_ids"`
|
||||
}
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&assignments)
|
||||
if err != nil {
|
||||
c.SetInvalidParamWithErr("assignments", err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventAssignAccessPolicy, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "id", policyID)
|
||||
model.AddEventParameterToAuditRec(auditRec, "channel_ids", assignments.ChannelIds)
|
||||
|
||||
if len(assignments.ChannelIds) != 0 {
|
||||
_, appErr := c.App.AssignAccessControlPolicyToChannels(c.AppContext, policyID, assignments.ChannelIds)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
func unassignAccessPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
c.RequirePolicyId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
policyID := c.Params.PolicyId
|
||||
|
||||
var assignments struct {
|
||||
ChannelIds []string `json:"channel_ids"`
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventUnassignAccessPolicy, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "id", policyID)
|
||||
model.AddEventParameterToAuditRec(auditRec, "channel_ids", assignments.ChannelIds)
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&assignments)
|
||||
if err != nil {
|
||||
c.SetInvalidParamWithErr("assignments", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(assignments.ChannelIds) != 0 {
|
||||
appErr := c.App.UnassignPoliciesFromChannels(c.AppContext, policyID, assignments.ChannelIds)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
func getChannelsForAccessControlPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
c.RequirePolicyId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
policyID := c.Params.PolicyId
|
||||
|
||||
afterID := r.URL.Query().Get("after")
|
||||
if afterID != "" && !model.IsValidId(afterID) {
|
||||
c.SetInvalidParam("after")
|
||||
return
|
||||
}
|
||||
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
limit, err := strconv.Atoi(limitStr)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getChannelsForAccessControlPolicy", "api.access_control_policy.get_channels.limit.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
channels, total, appErr := c.App.GetChannelsForPolicy(c.AppContext, policyID, model.AccessControlPolicyCursor{
|
||||
ID: afterID,
|
||||
}, limit)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
data := model.ChannelsWithCount{Channels: channels, TotalCount: total}
|
||||
|
||||
js, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getChannelsForAccessControlPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := w.Write(js); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func searchChannelsForAccessControlPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
c.RequirePolicyId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var props *model.ChannelSearch
|
||||
err := json.NewDecoder(r.Body).Decode(&props)
|
||||
if err != nil || props == nil {
|
||||
c.SetInvalidParamWithErr("channel_search", err)
|
||||
return
|
||||
}
|
||||
|
||||
policyID := c.Params.PolicyId
|
||||
|
||||
c.RequirePolicyId()
|
||||
|
||||
opts := model.ChannelSearchOpts{
|
||||
Deleted: props.Deleted,
|
||||
IncludeDeleted: props.IncludeDeleted,
|
||||
Private: true,
|
||||
ExcludeGroupConstrained: true,
|
||||
TeamIds: props.TeamIds,
|
||||
ParentAccessControlPolicyId: policyID,
|
||||
}
|
||||
|
||||
channels, total, appErr := c.App.SearchAllChannels(c.AppContext, props.Term, opts)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
data := model.ChannelsWithCount{Channels: channels, TotalCount: total}
|
||||
|
||||
channelsJSON, jsonErr := json.Marshal(data)
|
||||
if jsonErr != nil {
|
||||
c.Err = model.NewAppError("searchChannelsInPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := w.Write(channelsJSON); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getFieldsAutocomplete(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// Get channelId from query parameters (required for channel-specific permission check)
|
||||
channelId := r.URL.Query().Get("channelId")
|
||||
if channelId != "" && !model.IsValidId(channelId) {
|
||||
c.SetInvalidParam("channelId")
|
||||
return
|
||||
}
|
||||
|
||||
// Check permissions: system admin OR channel-specific permission
|
||||
hasSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem)
|
||||
if !hasSystemPermission {
|
||||
// For channel admins, channelId is required
|
||||
if channelId == "" {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
// SECURE: Check specific channel permission
|
||||
hasChannelPermission := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, channelId, model.PermissionManageChannelAccessRules)
|
||||
if !hasChannelPermission {
|
||||
c.SetPermissionError(model.PermissionManageChannelAccessRules)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
after := r.URL.Query().Get("after")
|
||||
if after != "" && !model.IsValidId(after) {
|
||||
c.SetInvalidParam("after")
|
||||
return
|
||||
} else if after == "" {
|
||||
after = strings.Repeat("0", 26)
|
||||
}
|
||||
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
limit, err := strconv.Atoi(limitStr)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getFieldsAutocomplete", "api.access_control_policy.get_fields.limit.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
if limit <= 0 || limit > 100 {
|
||||
c.Err = model.NewAppError("getFieldsAutocomplete", "api.access_control_policy.get_fields.limit.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var ac []*model.PropertyField
|
||||
var appErr *model.AppError
|
||||
|
||||
ac, appErr = c.App.GetAccessControlFieldsAutocomplete(c.AppContext, after, limit)
|
||||
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(ac)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getExpressionAutocomplete", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := w.Write(js); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func convertToVisualAST(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var cel struct {
|
||||
Expression string `json:"expression"`
|
||||
ChannelId string `json:"channelId,omitempty"`
|
||||
}
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&cel); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("user", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
// Get channelId from request body (required for channel-specific permission check)
|
||||
channelId := cel.ChannelId
|
||||
if channelId != "" && !model.IsValidId(channelId) {
|
||||
c.SetInvalidParam("channelId")
|
||||
return
|
||||
}
|
||||
|
||||
// Check permissions: system admin OR channel-specific permission
|
||||
hasSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem)
|
||||
if !hasSystemPermission {
|
||||
// For channel admins, channelId is required
|
||||
if channelId == "" {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
// SECURE: Check specific channel permission
|
||||
hasChannelPermission := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, channelId, model.PermissionManageChannelAccessRules)
|
||||
if !hasChannelPermission {
|
||||
c.SetPermissionError(model.PermissionManageChannelAccessRules)
|
||||
return
|
||||
}
|
||||
}
|
||||
visualAST, appErr := c.App.ExpressionToVisualAST(c.AppContext, cel.Expression)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(visualAST)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("convertToVisualAST", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
if _, err := w.Write(b); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
28
channels/api4/access_control_local.go
Normal file
28
channels/api4/access_control_local.go
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import "net/http"
|
||||
|
||||
func (api *API) InitAccessControlPolicyLocal() {
|
||||
if !api.srv.Config().FeatureFlags.AttributeBasedAccessControl {
|
||||
return
|
||||
}
|
||||
api.BaseRoutes.AccessControlPolicies.Handle("", api.APILocal(createAccessControlPolicy)).Methods(http.MethodPut)
|
||||
api.BaseRoutes.AccessControlPolicies.Handle("/search", api.APILocal(searchAccessControlPolicies)).Methods(http.MethodPost)
|
||||
|
||||
api.BaseRoutes.AccessControlPolicies.Handle("/cel/check", api.APILocal(checkExpression)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.AccessControlPolicies.Handle("/cel/test", api.APILocal(testExpression)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.AccessControlPolicies.Handle("/cel/validate_requester", api.APILocal(validateExpressionAgainstRequester)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.AccessControlPolicies.Handle("/cel/autocomplete/fields", api.APILocal(getFieldsAutocomplete)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.AccessControlPolicies.Handle("/cel/visual_ast", api.APILocal(convertToVisualAST)).Methods(http.MethodPost)
|
||||
|
||||
api.BaseRoutes.AccessControlPolicy.Handle("", api.APILocal(getAccessControlPolicy)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.AccessControlPolicy.Handle("", api.APILocal(deleteAccessControlPolicy)).Methods(http.MethodDelete)
|
||||
api.BaseRoutes.AccessControlPolicy.Handle("/activate", api.APILocal(updateActiveStatus)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.AccessControlPolicy.Handle("/assign", api.APILocal(assignAccessPolicy)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.AccessControlPolicy.Handle("/unassign", api.APILocal(unassignAccessPolicy)).Methods(http.MethodDelete)
|
||||
api.BaseRoutes.AccessControlPolicy.Handle("/resources/channels", api.APILocal(getChannelsForAccessControlPolicy)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.AccessControlPolicy.Handle("/resources/channels/search", api.APILocal(searchChannelsForAccessControlPolicy)).Methods(http.MethodPost)
|
||||
}
|
||||
867
channels/api4/access_control_test.go
Normal file
867
channels/api4/access_control_test.go
Normal file
@ -0,0 +1,867 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/plugin/plugintest/mock"
|
||||
"github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCreateAccessControlPolicy(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL", "true")
|
||||
th := Setup(t).InitBasic()
|
||||
t.Cleanup(func() {
|
||||
th.TearDown()
|
||||
os.Unsetenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL")
|
||||
})
|
||||
|
||||
samplePolicy := &model.AccessControlPolicy{
|
||||
ID: th.BasicChannel.Id,
|
||||
Type: model.AccessControlPolicyTypeChannel,
|
||||
Version: model.AccessControlPolicyVersionV0_2,
|
||||
Revision: 1,
|
||||
Rules: []model.AccessControlPolicyRule{
|
||||
{
|
||||
Expression: "user.attributes.team == 'engineering'",
|
||||
Actions: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("CreateAccessControlPolicy without license", func(t *testing.T) {
|
||||
_, resp, err := th.SystemAdminClient.CreateAccessControlPolicy(context.Background(), samplePolicy)
|
||||
require.Error(t, err)
|
||||
CheckNotImplementedStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("CreateAccessControlPolicy with regular user", func(t *testing.T) {
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
// Create another user who will create the channel
|
||||
channelCreator := th.CreateUser()
|
||||
th.LinkUserToTeam(channelCreator, th.BasicTeam)
|
||||
channelCreatorClient := th.CreateClient()
|
||||
_, _, err := channelCreatorClient.Login(context.Background(), channelCreator.Email, channelCreator.Password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a private channel with the other user (not th.BasicUser)
|
||||
privateChannel, _, err := channelCreatorClient.CreateChannel(context.Background(), &model.Channel{
|
||||
TeamId: th.BasicTeam.Id,
|
||||
Name: "private-channel-" + model.NewId(),
|
||||
DisplayName: "Private Channel",
|
||||
Type: model.ChannelTypePrivate,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create channel-specific policy (regular user should not have permission)
|
||||
channelPolicy := &model.AccessControlPolicy{
|
||||
ID: privateChannel.Id, // Set to actual channel ID
|
||||
Type: model.AccessControlPolicyTypeChannel,
|
||||
Version: model.AccessControlPolicyVersionV0_2,
|
||||
Revision: 1,
|
||||
Rules: []model.AccessControlPolicyRule{
|
||||
{
|
||||
Expression: "user.attributes.team == 'engineering'",
|
||||
Actions: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create and set up the mock
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
_, resp, err := th.Client.CreateAccessControlPolicy(context.Background(), channelPolicy)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("CreateAccessControlPolicy with channel admin for their channel", func(t *testing.T) {
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
// Add the permission to channel admin role
|
||||
th.AddPermissionToRole(model.PermissionManageChannelAccessRules.Id, model.ChannelAdminRoleId)
|
||||
|
||||
// Create a private channel and make user channel admin
|
||||
privateChannel := th.CreatePrivateChannel()
|
||||
channelAdmin := th.CreateUser()
|
||||
th.LinkUserToTeam(channelAdmin, th.BasicTeam)
|
||||
th.AddUserToChannel(channelAdmin, privateChannel)
|
||||
th.MakeUserChannelAdmin(channelAdmin, privateChannel)
|
||||
channelAdminClient := th.CreateClient()
|
||||
th.LoginBasicWithClient(channelAdminClient)
|
||||
_, _, err := channelAdminClient.Login(context.Background(), channelAdmin.Email, channelAdmin.Password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create channel-specific policy
|
||||
channelPolicy := &model.AccessControlPolicy{
|
||||
ID: privateChannel.Id,
|
||||
Type: model.AccessControlPolicyTypeChannel,
|
||||
Version: model.AccessControlPolicyVersionV0_2,
|
||||
Revision: 1,
|
||||
Rules: []model.AccessControlPolicyRule{
|
||||
{
|
||||
Expression: "user.attributes.team == 'engineering'",
|
||||
Actions: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create and set up the mock
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
mockAccessControlService.On("SavePolicy", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("*model.AccessControlPolicy")).Return(channelPolicy, nil).Times(1)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
_, resp, err := channelAdminClient.CreateAccessControlPolicy(context.Background(), channelPolicy)
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("CreateAccessControlPolicy with channel admin for another channel should fail", func(t *testing.T) {
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
// Create two private channels
|
||||
privateChannel1 := th.CreatePrivateChannel()
|
||||
privateChannel2 := th.CreatePrivateChannel()
|
||||
channelAdmin := th.CreateUser()
|
||||
th.LinkUserToTeam(channelAdmin, th.BasicTeam)
|
||||
th.AddUserToChannel(channelAdmin, privateChannel1)
|
||||
th.MakeUserChannelAdmin(channelAdmin, privateChannel1)
|
||||
channelAdminClient := th.CreateClient()
|
||||
th.LoginBasicWithClient(channelAdminClient)
|
||||
_, _, err := channelAdminClient.Login(context.Background(), channelAdmin.Email, channelAdmin.Password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to create policy for different channel
|
||||
channelPolicy := &model.AccessControlPolicy{
|
||||
ID: privateChannel2.Id,
|
||||
Type: model.AccessControlPolicyTypeChannel,
|
||||
Version: model.AccessControlPolicyVersionV0_2,
|
||||
Revision: 1,
|
||||
Rules: []model.AccessControlPolicyRule{
|
||||
{
|
||||
Expression: "user.attributes.team == 'engineering'",
|
||||
Actions: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
_, resp, err := channelAdminClient.CreateAccessControlPolicy(context.Background(), channelPolicy)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("CreateAccessControlPolicy with channel admin creating parent policy should fail", func(t *testing.T) {
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
// Create a private channel and make user channel admin
|
||||
privateChannel := th.CreatePrivateChannel()
|
||||
channelAdmin := th.CreateUser()
|
||||
th.LinkUserToTeam(channelAdmin, th.BasicTeam)
|
||||
th.AddUserToChannel(channelAdmin, privateChannel)
|
||||
th.MakeUserChannelAdmin(channelAdmin, privateChannel)
|
||||
channelAdminClient := th.CreateClient()
|
||||
th.LoginBasicWithClient(channelAdminClient)
|
||||
_, _, err := channelAdminClient.Login(context.Background(), channelAdmin.Email, channelAdmin.Password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to create parent-type policy
|
||||
parentPolicy := &model.AccessControlPolicy{
|
||||
ID: model.NewId(),
|
||||
Type: model.AccessControlPolicyTypeParent,
|
||||
Version: model.AccessControlPolicyVersionV0_2,
|
||||
Revision: 1,
|
||||
Rules: []model.AccessControlPolicyRule{
|
||||
{
|
||||
Expression: "user.attributes.team == 'engineering'",
|
||||
Actions: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
_, resp, err := channelAdminClient.CreateAccessControlPolicy(context.Background(), parentPolicy)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||
// Set up a test license with Data Retention enabled
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
// Create and set up the mock
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
|
||||
// Set up mock expectations
|
||||
mockAccessControlService.On("SavePolicy", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("*model.AccessControlPolicy")).Return(samplePolicy, nil).Times(1)
|
||||
|
||||
// Set the mock on the app
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
_, resp, err := client.CreateAccessControlPolicy(context.Background(), samplePolicy)
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
}, "CreateAccessControlPolicy with system admin")
|
||||
|
||||
t.Run("CreateAccessControlPolicy with channel scope permissions", func(t *testing.T) {
|
||||
// Set up a test license with Data Retention enabled
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
// Create and set up the mock
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
|
||||
ch := th.CreatePrivateChannel()
|
||||
|
||||
// Set up mock expectations
|
||||
mockAccessControlService.On("SavePolicy", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("*model.AccessControlPolicy")).Return(samplePolicy, nil).Times(1)
|
||||
|
||||
// Set the mock on the app
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
th.AddPermissionToRole(model.PermissionManageChannelAccessRules.Id, model.ChannelAdminRoleId)
|
||||
|
||||
channelPolicy := &model.AccessControlPolicy{
|
||||
Type: model.AccessControlPolicyTypeChannel,
|
||||
Version: model.AccessControlPolicyVersionV0_2,
|
||||
Revision: 1,
|
||||
Rules: []model.AccessControlPolicyRule{
|
||||
{
|
||||
Expression: "user.attributes.team == 'engineering'",
|
||||
Actions: []string{"*"},
|
||||
},
|
||||
},
|
||||
ID: ch.Id,
|
||||
}
|
||||
|
||||
_, resp, err := th.Client.CreateAccessControlPolicy(context.Background(), channelPolicy)
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetAccessControlPolicy(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL", "true")
|
||||
th := Setup(t).InitBasic()
|
||||
t.Cleanup(func() {
|
||||
th.TearDown()
|
||||
os.Unsetenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL")
|
||||
})
|
||||
|
||||
samplePolicy := &model.AccessControlPolicy{
|
||||
ID: model.NewId(),
|
||||
Type: model.AccessControlPolicyTypeChannel,
|
||||
Version: model.AccessControlPolicyVersionV0_2,
|
||||
Revision: 1,
|
||||
Rules: []model.AccessControlPolicyRule{
|
||||
{
|
||||
Expression: "user.attributes.team == 'engineering'",
|
||||
Actions: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("GetAccessControlPolicy without license", func(t *testing.T) {
|
||||
_, resp, err := th.SystemAdminClient.GetAccessControlPolicy(context.Background(), samplePolicy.ID)
|
||||
require.Error(t, err)
|
||||
CheckNotImplementedStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("GetAccessControlPolicy with regular user", func(t *testing.T) {
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
// Create and set up the mock
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
mockAccessControlService.On("GetPolicy", mock.AnythingOfType("*request.Context"), samplePolicy.ID).Return(samplePolicy, nil).Times(1)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
_, resp, err := th.Client.GetAccessControlPolicy(context.Background(), samplePolicy.ID)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
// Create and set up the mock
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
mockAccessControlService.On("GetPolicy", mock.AnythingOfType("*request.Context"), samplePolicy.ID).Return(samplePolicy, nil).Times(1)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
_, resp, err := client.GetAccessControlPolicy(context.Background(), samplePolicy.ID)
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
}, "GetAccessControlPolicy with system admin")
|
||||
}
|
||||
|
||||
func TestDeleteAccessControlPolicy(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL", "true")
|
||||
th := Setup(t).InitBasic()
|
||||
t.Cleanup(func() {
|
||||
th.TearDown()
|
||||
os.Unsetenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL")
|
||||
})
|
||||
|
||||
samplePolicyID := model.NewId()
|
||||
|
||||
t.Run("DeleteAccessControlPolicy without license", func(t *testing.T) {
|
||||
resp, err := th.SystemAdminClient.DeleteAccessControlPolicy(context.Background(), samplePolicyID)
|
||||
require.Error(t, err)
|
||||
CheckNotImplementedStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("DeleteAccessControlPolicy with regular user", func(t *testing.T) {
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
|
||||
// Mock the GetPolicy call that happens in ValidateAccessControlPolicyPermission
|
||||
channelPolicy := &model.AccessControlPolicy{
|
||||
ID: samplePolicyID,
|
||||
Type: model.AccessControlPolicyTypeChannel,
|
||||
Version: model.AccessControlPolicyVersionV0_2,
|
||||
Revision: 1,
|
||||
Rules: []model.AccessControlPolicyRule{
|
||||
{
|
||||
Expression: "user.attributes.team == 'engineering'",
|
||||
Actions: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
mockAccessControlService.On("GetPolicy", mock.AnythingOfType("*request.Context"), samplePolicyID).Return(channelPolicy, nil)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
resp, err := th.Client.DeleteAccessControlPolicy(context.Background(), samplePolicyID)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
mockAccessControlService.On("DeletePolicy", mock.AnythingOfType("*request.Context"), samplePolicyID).Return(nil).Times(1)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
resp, err := client.DeleteAccessControlPolicy(context.Background(), samplePolicyID)
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCheckExpression(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL", "true")
|
||||
th := Setup(t).InitBasic()
|
||||
t.Cleanup(func() {
|
||||
th.TearDown()
|
||||
os.Unsetenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL")
|
||||
})
|
||||
|
||||
t.Run("CheckExpression without license", func(t *testing.T) {
|
||||
_, resp, err := th.SystemAdminClient.CheckExpression(context.Background(), "true")
|
||||
require.Error(t, err)
|
||||
CheckNotImplementedStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("CheckExpression with regular user", func(t *testing.T) {
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
_, resp, err := th.Client.CheckExpression(context.Background(), "true")
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
mockAccessControlService.On("CheckExpression", mock.AnythingOfType("*request.Context"), "true").Return([]model.CELExpressionError{}, nil).Times(1)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
errors, resp, err := client.CheckExpression(context.Background(), "true")
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
require.Empty(t, errors, "expected no errors")
|
||||
}, "CheckExpression with system admin")
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
mockAccessControlService.On("CheckExpression", mock.AnythingOfType("*request.Context"), "true").Return([]model.CELExpressionError{
|
||||
{
|
||||
Line: 1,
|
||||
Column: 1,
|
||||
Message: "Syntax error",
|
||||
},
|
||||
}, nil).Times(1)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
errors, resp, err := client.CheckExpression(context.Background(), "true")
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
require.NotEmpty(t, errors, "expected errors")
|
||||
}, "CheckExpression with system admin errors returned")
|
||||
|
||||
t.Run("CheckExpression with channel admin for their channel", func(t *testing.T) {
|
||||
// Reload config to pick up the feature flag
|
||||
err := th.App.ReloadConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
// Add permission to channel admin role
|
||||
th.AddPermissionToRole(model.PermissionManageChannelAccessRules.Id, model.ChannelAdminRoleId)
|
||||
|
||||
// Create private channel and make user channel admin
|
||||
privateChannel := th.CreatePrivateChannel()
|
||||
channelAdmin := th.CreateUser()
|
||||
th.LinkUserToTeam(channelAdmin, th.BasicTeam)
|
||||
th.AddUserToChannel(channelAdmin, privateChannel)
|
||||
th.MakeUserChannelAdmin(channelAdmin, privateChannel)
|
||||
channelAdminClient := th.CreateClient()
|
||||
_, _, err = channelAdminClient.Login(context.Background(), channelAdmin.Email, channelAdmin.Password)
|
||||
require.NoError(t, err)
|
||||
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
mockAccessControlService.On("CheckExpression", mock.AnythingOfType("*request.Context"), "true").Return([]model.CELExpressionError{}, nil).Times(1)
|
||||
|
||||
// Channel admin should be able to check expressions for their channel
|
||||
errors, resp, err := channelAdminClient.CheckExpression(context.Background(), "true", privateChannel.Id)
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
require.Empty(t, errors, "expected no errors")
|
||||
})
|
||||
}
|
||||
|
||||
func TestTestExpression(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL", "true")
|
||||
th := Setup(t).InitBasic()
|
||||
t.Cleanup(func() {
|
||||
th.TearDown()
|
||||
os.Unsetenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL")
|
||||
})
|
||||
|
||||
t.Run("TestExpression without license", func(t *testing.T) {
|
||||
_, resp, err := th.SystemAdminClient.TestExpression(context.Background(), model.QueryExpressionParams{})
|
||||
require.Error(t, err)
|
||||
CheckNotImplementedStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("TestExpression with regular user", func(t *testing.T) {
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
_, resp, err := th.Client.TestExpression(context.Background(), model.QueryExpressionParams{})
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
mockAccessControlService.On("QueryUsersForExpression", mock.AnythingOfType("*request.Context"), "true", model.SubjectSearchOptions{}).Return([]*model.User{}, int64(0), nil).Times(1)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
usersResp, resp, err := client.TestExpression(context.Background(), model.QueryExpressionParams{
|
||||
Expression: "true",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
require.Empty(t, usersResp.Users, "expected no users")
|
||||
require.Equal(t, int64(0), usersResp.Total, "expected count 0 users")
|
||||
}, "TestExpression with system admin")
|
||||
}
|
||||
|
||||
func TestSearchAccessControlPolicies(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL", "true")
|
||||
th := Setup(t).InitBasic()
|
||||
t.Cleanup(func() {
|
||||
th.TearDown()
|
||||
os.Unsetenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL")
|
||||
})
|
||||
|
||||
t.Run("SearchAccessControlPolicies without license", func(t *testing.T) {
|
||||
_, resp, err := th.SystemAdminClient.SearchAccessControlPolicies(context.Background(), model.AccessControlPolicySearch{})
|
||||
require.Error(t, err)
|
||||
CheckNotImplementedStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("SearchAccessControlPolicies with regular user", func(t *testing.T) {
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
_, resp, err := th.Client.SearchAccessControlPolicies(context.Background(), model.AccessControlPolicySearch{})
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
mockAccessControlService.On("SearchPolicies", mock.AnythingOfType("*request.Context"), model.AccessControlPolicySearch{
|
||||
Term: "engineering",
|
||||
}).Return([]*model.AccessControlPolicy{}, int64(0), nil).Times(1)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
policiesResp, resp, err := client.SearchAccessControlPolicies(context.Background(), model.AccessControlPolicySearch{
|
||||
Term: "engineering",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
require.Empty(t, policiesResp.Policies, "expected no policies")
|
||||
require.Equal(t, int64(0), policiesResp.Total, "expected count 0 policies")
|
||||
}, "SearchAccessControlPolicies with system admin")
|
||||
}
|
||||
|
||||
func TestAssignAccessPolicy(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL", "true")
|
||||
th := Setup(t).InitBasic()
|
||||
t.Cleanup(func() {
|
||||
th.TearDown()
|
||||
os.Unsetenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL")
|
||||
})
|
||||
|
||||
samplePolicy := &model.AccessControlPolicy{
|
||||
ID: model.NewId(),
|
||||
Type: model.AccessControlPolicyTypeParent,
|
||||
Version: model.AccessControlPolicyVersionV0_2,
|
||||
Revision: 1,
|
||||
Rules: []model.AccessControlPolicyRule{
|
||||
{
|
||||
Expression: "user.attributes.team == 'engineering'",
|
||||
Actions: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("AssignAccessPolicy without license", func(t *testing.T) {
|
||||
resp, err := th.SystemAdminClient.AssignAccessControlPolicies(context.Background(), model.NewId(), []string{model.NewId()})
|
||||
require.Error(t, err)
|
||||
CheckNotImplementedStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("AssignAccessPolicy with regular user", func(t *testing.T) {
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
resp, err := th.Client.AssignAccessControlPolicies(context.Background(), model.NewId(), []string{model.NewId()})
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||
resourceID := model.NewId()
|
||||
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
child := model.AccessControlPolicy{
|
||||
ID: resourceID,
|
||||
Type: model.AccessControlPolicyTypeChannel,
|
||||
Version: model.AccessControlPolicyVersionV0_2,
|
||||
Revision: 1,
|
||||
}
|
||||
|
||||
appErr := child.Inherit(samplePolicy)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
mockAccessControlService.On("GetPolicy", mock.AnythingOfType("*request.Context"), samplePolicy.ID).Return(samplePolicy, nil).Times(1)
|
||||
mockAccessControlService.On("SavePolicy", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("*model.AccessControlPolicy")).Return(child, nil).Times(1)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
resp, err := client.AssignAccessControlPolicies(context.Background(), samplePolicy.ID, []string{resourceID})
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
}, "AssignAccessPolicy with system admin")
|
||||
}
|
||||
|
||||
func TestUnassignAccessPolicy(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL", "true")
|
||||
th := Setup(t).InitBasic()
|
||||
t.Cleanup(func() {
|
||||
th.TearDown()
|
||||
os.Unsetenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL")
|
||||
})
|
||||
|
||||
samplePolicy := &model.AccessControlPolicy{
|
||||
ID: model.NewId(),
|
||||
Type: model.AccessControlPolicyTypeParent,
|
||||
Version: model.AccessControlPolicyVersionV0_2,
|
||||
Revision: 1,
|
||||
Rules: []model.AccessControlPolicyRule{
|
||||
{
|
||||
Expression: "user.attributes.team == 'engineering'",
|
||||
Actions: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("UnassignAccessPolicy without license", func(t *testing.T) {
|
||||
resp, err := th.SystemAdminClient.UnassignAccessControlPolicies(context.Background(), samplePolicy.ID, []string{model.NewId()})
|
||||
require.Error(t, err)
|
||||
CheckNotImplementedStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("UnassignAccessPolicy with regular user", func(t *testing.T) {
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
resp, err := th.Client.UnassignAccessControlPolicies(context.Background(), samplePolicy.ID, []string{model.NewId()})
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||
resourceID := model.NewId()
|
||||
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
child := &model.AccessControlPolicy{
|
||||
ID: resourceID,
|
||||
Type: model.AccessControlPolicyTypeChannel,
|
||||
Version: model.AccessControlPolicyVersionV0_2,
|
||||
Revision: 1,
|
||||
}
|
||||
|
||||
appErr := child.Inherit(samplePolicy)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
mockAccessControlService.On("GetPolicy", mock.AnythingOfType("*request.Context"), samplePolicy.ID).Return(samplePolicy, nil).Times(1)
|
||||
mockAccessControlService.On("SearchPolicies", mock.AnythingOfType("*request.Context"), model.AccessControlPolicySearch{
|
||||
Type: model.AccessControlPolicyTypeChannel,
|
||||
ParentID: samplePolicy.ID,
|
||||
}).Return([]*model.AccessControlPolicy{child}, nil).Times(1)
|
||||
mockAccessControlService.On("DeletePolicy", mock.AnythingOfType("*request.Context"), child.ID).Return(nil).Times(1)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
resp, err := client.UnassignAccessControlPolicies(context.Background(), samplePolicy.ID, []string{child.ID})
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
}, "UnassignAccessPolicy with system admin")
|
||||
}
|
||||
|
||||
func TestGetChannelsForAccessControlPolicy(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL", "true")
|
||||
th := Setup(t).InitBasic()
|
||||
t.Cleanup(func() {
|
||||
th.TearDown()
|
||||
os.Unsetenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL")
|
||||
})
|
||||
|
||||
samplePolicy := &model.AccessControlPolicy{
|
||||
ID: model.NewId(),
|
||||
Type: model.AccessControlPolicyTypeParent,
|
||||
Version: model.AccessControlPolicyVersionV0_2,
|
||||
Revision: 1,
|
||||
Rules: []model.AccessControlPolicyRule{
|
||||
{
|
||||
Expression: "user.attributes.team == 'engineering'",
|
||||
Actions: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("GetChannelsForAccessControlPolicy without license", func(t *testing.T) {
|
||||
_, resp, err := th.SystemAdminClient.GetChannelsForAccessControlPolicy(context.Background(), samplePolicy.ID, "", 1000)
|
||||
require.Error(t, err)
|
||||
CheckNotImplementedStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("GetChannelsForAccessControlPolicy with regular user", func(t *testing.T) {
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
_, resp, err := th.Client.GetChannelsForAccessControlPolicy(context.Background(), samplePolicy.ID, "", 1000)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
mockAccessControlService.On("GetPolicy", mock.AnythingOfType("*request.Context"), samplePolicy.ID).Return(samplePolicy, nil).Times(1)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
channelsResp, resp, err := client.GetChannelsForAccessControlPolicy(context.Background(), samplePolicy.ID, "", 1000)
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
require.Empty(t, channelsResp.Channels, "expected no channels")
|
||||
require.Equal(t, int64(0), channelsResp.TotalCount, "expected count 0 channels")
|
||||
}, "GetChannelsForAccessControlPolicy with system admin")
|
||||
}
|
||||
|
||||
func TestSearchChannelsForAccessControlPolicy(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL", "true")
|
||||
th := Setup(t).InitBasic()
|
||||
t.Cleanup(func() {
|
||||
th.TearDown()
|
||||
os.Unsetenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL")
|
||||
})
|
||||
|
||||
samplePolicy := &model.AccessControlPolicy{
|
||||
ID: model.NewId(),
|
||||
Type: model.AccessControlPolicyTypeParent,
|
||||
Version: model.AccessControlPolicyVersionV0_2,
|
||||
Revision: 1,
|
||||
Rules: []model.AccessControlPolicyRule{
|
||||
{
|
||||
Expression: "user.attributes.team == 'engineering'",
|
||||
Actions: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("SearchChannelsForAccessControlPolicy with regular user", func(t *testing.T) {
|
||||
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.True(t, ok, "SetLicense should return true")
|
||||
|
||||
mockAccessControlService := &mocks.AccessControlServiceInterface{}
|
||||
th.App.Srv().Channels().AccessControl = mockAccessControlService
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
|
||||
})
|
||||
|
||||
_, resp, err := th.Client.SearchChannelsForAccessControlPolicy(context.Background(), samplePolicy.ID, model.ChannelSearch{})
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
}
|
||||
55
channels/api4/agents.go
Normal file
55
channels/api4/agents.go
Normal file
@ -0,0 +1,55 @@
|
||||
// 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/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitAgents() {
|
||||
// GET /api/v4/agents
|
||||
api.BaseRoutes.Agents.Handle("", api.APISessionRequired(getAgents)).Methods(http.MethodGet)
|
||||
// GET /api/v4/llmservices
|
||||
api.BaseRoutes.LLMServices.Handle("", api.APISessionRequired(getLLMServices)).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
func getAgents(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
agents, appErr := c.App.GetAgents(c.AppContext, c.AppContext.Session().UserId)
|
||||
if appErr != nil {
|
||||
c.Err = model.NewAppError("Api4.getAgents", "app.agents.get_agents.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
||||
return
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(agents)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getAgents", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := w.Write(jsonData); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getLLMServices(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
services, appErr := c.App.GetLLMServices(c.AppContext, c.AppContext.Session().UserId)
|
||||
if appErr != nil {
|
||||
c.Err = model.NewAppError("Api4.getLLMServices", "app.agents.get_services.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
||||
return
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(services)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getLLMServices", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := w.Write(jsonData); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
498
channels/api4/api.go
Normal file
498
channels/api4/api.go
Normal file
@ -0,0 +1,498 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
_ "github.com/mattermost/go-i18n/i18n"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/manualtesting"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/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'
|
||||
ChannelBookmarks *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/bookmarks'
|
||||
ChannelBookmark *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/bookmarks/{bookmark_id:[A-Za-z0-9]+}'
|
||||
|
||||
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]+}'
|
||||
|
||||
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'
|
||||
|
||||
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'
|
||||
Import *mux.Router // 'api/v4/imports/{import_name:.+\\.zip}'
|
||||
|
||||
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'
|
||||
ChannelForRemote *mux.Router // 'api/v4/remotecluster/{remote_id:[A-Za-z0-9]+}/channels/{channel_id:[A-Za-z0-9]+}'
|
||||
SharedChannelRemotes *mux.Router // 'api/v4/remotecluster/{remote_id:[A-Za-z0-9]+}/sharedchannelremotes'
|
||||
|
||||
Permissions *mux.Router // 'api/v4/permissions'
|
||||
|
||||
Usage *mux.Router // 'api/v4/usage'
|
||||
|
||||
HostedCustomer *mux.Router // 'api/v4/hosted_customer'
|
||||
|
||||
Drafts *mux.Router // 'api/v4/drafts'
|
||||
|
||||
IPFiltering *mux.Router // 'api/v4/ip_filtering'
|
||||
|
||||
Reports *mux.Router // 'api/v4/reports'
|
||||
|
||||
Limits *mux.Router // 'api/v4/limits'
|
||||
|
||||
OutgoingOAuthConnections *mux.Router // 'api/v4/oauth/outgoing_connections'
|
||||
OutgoingOAuthConnection *mux.Router // 'api/v4/oauth/outgoing_connections/{outgoing_oauth_connection_id:[A-Za-z0-9]+}'
|
||||
|
||||
CustomProfileAttributes *mux.Router // 'api/v4/custom_profile_attributes'
|
||||
CustomProfileAttributesFields *mux.Router // 'api/v4/custom_profile_attributes/fields'
|
||||
CustomProfileAttributesField *mux.Router // 'api/v4/custom_profile_attributes/fields/{field_id:[A-Za-z0-9]+}'
|
||||
CustomProfileAttributesValues *mux.Router // 'api/v4/custom_profile_attributes/values'
|
||||
|
||||
AuditLogs *mux.Router // 'api/v4/audit_logs'
|
||||
|
||||
AccessControlPolicies *mux.Router // 'api/v4/access_control_policies'
|
||||
AccessControlPolicy *mux.Router // 'api/v4/access_control_policies/{policy_id:[A-Za-z0-9]+}'
|
||||
|
||||
ContentFlagging *mux.Router // 'api/v4/content_flagging'
|
||||
|
||||
Agents *mux.Router // 'api/v4/agents'
|
||||
LLMServices *mux.Router // 'api/v4/llmservices'
|
||||
}
|
||||
|
||||
type API struct {
|
||||
srv *app.Server
|
||||
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.ChannelBookmarks = api.BaseRoutes.Channel.PathPrefix("/bookmarks").Subrouter()
|
||||
api.BaseRoutes.ChannelBookmark = api.BaseRoutes.ChannelBookmarks.PathPrefix("/{bookmark_id:[A-Za-z0-9]+}").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.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.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.Import = api.BaseRoutes.Imports.PathPrefix("/{import_name:.+\\.zip}").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.SharedChannelRemotes = api.BaseRoutes.RemoteCluster.PathPrefix("/{remote_id:[A-Za-z0-9]+}/sharedchannelremotes").Subrouter()
|
||||
api.BaseRoutes.ChannelForRemote = api.BaseRoutes.RemoteCluster.PathPrefix("/{remote_id:[A-Za-z0-9]+}/channels/{channel_id:[A-Za-z0-9]+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.Permissions = api.BaseRoutes.APIRoot.PathPrefix("/permissions").Subrouter()
|
||||
|
||||
api.BaseRoutes.Usage = api.BaseRoutes.APIRoot.PathPrefix("/usage").Subrouter()
|
||||
|
||||
api.BaseRoutes.HostedCustomer = api.BaseRoutes.APIRoot.PathPrefix("/hosted_customer").Subrouter()
|
||||
|
||||
api.BaseRoutes.Drafts = api.BaseRoutes.APIRoot.PathPrefix("/drafts").Subrouter()
|
||||
|
||||
api.BaseRoutes.IPFiltering = api.BaseRoutes.APIRoot.PathPrefix("/ip_filtering").Subrouter()
|
||||
|
||||
api.BaseRoutes.Reports = api.BaseRoutes.APIRoot.PathPrefix("/reports").Subrouter()
|
||||
|
||||
api.BaseRoutes.Limits = api.BaseRoutes.APIRoot.PathPrefix("/limits").Subrouter()
|
||||
|
||||
api.BaseRoutes.OutgoingOAuthConnections = api.BaseRoutes.APIRoot.PathPrefix("/oauth/outgoing_connections").Subrouter()
|
||||
api.BaseRoutes.OutgoingOAuthConnection = api.BaseRoutes.OutgoingOAuthConnections.PathPrefix("/{outgoing_oauth_connection_id:[A-Za-z0-9]+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.CustomProfileAttributes = api.BaseRoutes.APIRoot.PathPrefix("/custom_profile_attributes").Subrouter()
|
||||
api.BaseRoutes.CustomProfileAttributesFields = api.BaseRoutes.CustomProfileAttributes.PathPrefix("/fields").Subrouter()
|
||||
api.BaseRoutes.CustomProfileAttributesField = api.BaseRoutes.CustomProfileAttributesFields.PathPrefix("/{field_id:[A-Za-z0-9]+}").Subrouter()
|
||||
api.BaseRoutes.CustomProfileAttributesValues = api.BaseRoutes.CustomProfileAttributes.PathPrefix("/values").Subrouter()
|
||||
|
||||
api.BaseRoutes.AuditLogs = api.BaseRoutes.APIRoot.PathPrefix("/audit_logs").Subrouter()
|
||||
|
||||
api.BaseRoutes.AccessControlPolicies = api.BaseRoutes.APIRoot.PathPrefix("/access_control_policies").Subrouter()
|
||||
api.BaseRoutes.AccessControlPolicy = api.BaseRoutes.APIRoot.PathPrefix("/access_control_policies/{policy_id:[A-Za-z0-9]+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.ContentFlagging = api.BaseRoutes.APIRoot.PathPrefix("/content_flagging").Subrouter()
|
||||
|
||||
api.BaseRoutes.Agents = api.BaseRoutes.APIRoot.PathPrefix("/agents").Subrouter()
|
||||
api.BaseRoutes.LLMServices = api.BaseRoutes.APIRoot.PathPrefix("/llmservices").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.InitDataRetention()
|
||||
api.InitBrand()
|
||||
api.InitJob()
|
||||
api.InitCommand()
|
||||
api.InitStatus()
|
||||
api.InitWebSocket()
|
||||
api.InitEmoji()
|
||||
api.InitOAuth()
|
||||
api.InitReaction()
|
||||
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.InitUsage()
|
||||
api.InitHostedCustomer()
|
||||
api.InitDrafts()
|
||||
api.InitIPFiltering()
|
||||
api.InitChannelBookmarks()
|
||||
api.InitReports()
|
||||
api.InitLimits()
|
||||
api.InitOutgoingOAuthConnection()
|
||||
api.InitClientPerformanceMetrics()
|
||||
api.InitScheduledPost()
|
||||
api.InitCustomProfileAttributes()
|
||||
api.InitAuditLogging()
|
||||
api.InitAccessControlPolicy()
|
||||
api.InitContentFlagging()
|
||||
api.InitAgents()
|
||||
|
||||
// If we allow testing then listen for manual testing URL hits
|
||||
if *srv.Config().ServiceSettings.EnableTesting {
|
||||
api.BaseRoutes.Root.Handle("/manualtest", api.APIHandler(manualtesting.ManualTest)).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
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.Preferences = api.BaseRoutes.User.PathPrefix("/preferences").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.Import = api.BaseRoutes.Imports.PathPrefix("/{import_name:.+\\.zip}").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.BaseRoutes.CustomProfileAttributes = api.BaseRoutes.APIRoot.PathPrefix("/custom_profile_attributes").Subrouter()
|
||||
api.BaseRoutes.CustomProfileAttributesFields = api.BaseRoutes.CustomProfileAttributes.PathPrefix("/fields").Subrouter()
|
||||
api.BaseRoutes.CustomProfileAttributesField = api.BaseRoutes.CustomProfileAttributesFields.PathPrefix("/{field_id:[A-Za-z0-9]+}").Subrouter()
|
||||
api.BaseRoutes.CustomProfileAttributesValues = api.BaseRoutes.CustomProfileAttributes.PathPrefix("/values").Subrouter()
|
||||
|
||||
api.BaseRoutes.AccessControlPolicies = api.BaseRoutes.APIRoot.PathPrefix("/access_control_policies").Subrouter()
|
||||
api.BaseRoutes.AccessControlPolicy = api.BaseRoutes.APIRoot.PathPrefix("/access_control_policies/{policy_id:[A-Za-z0-9]+}").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.InitPreferenceLocal()
|
||||
api.InitRoleLocal()
|
||||
api.InitUploadLocal()
|
||||
api.InitImportLocal()
|
||||
api.InitExportLocal()
|
||||
api.InitJobLocal()
|
||||
api.InitSamlLocal()
|
||||
api.InitCustomProfileAttributesLocal()
|
||||
api.InitAccessControlPolicyLocal()
|
||||
|
||||
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
|
||||
1480
channels/api4/apitestlib.go
Normal file
1480
channels/api4/apitestlib.go
Normal file
File diff suppressed because it is too large
Load Diff
70
channels/api4/apitestlib_test.go
Normal file
70
channels/api4/apitestlib_test.go
Normal file
@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEnvironmentVariableHandling(t *testing.T) {
|
||||
// TestEnvironmentVariableHandling should NEVER be run with t.Parallel()
|
||||
|
||||
originalConsoleLevel := os.Getenv("MM_LOGSETTINGS_CONSOLELEVEL")
|
||||
defer func() {
|
||||
// Restore original environment variables
|
||||
if originalConsoleLevel != "" {
|
||||
os.Setenv("MM_LOGSETTINGS_CONSOLELEVEL", originalConsoleLevel)
|
||||
} else {
|
||||
os.Unsetenv("MM_LOGSETTINGS_CONSOLELEVEL")
|
||||
}
|
||||
}()
|
||||
|
||||
t.Run("MM_LOGSETTINGS_CONSOLELEVEL should be respected when set", func(t *testing.T) {
|
||||
// never run with t.Parallel()
|
||||
|
||||
// Set the console level environment variable
|
||||
os.Setenv("MM_LOGSETTINGS_CONSOLELEVEL", "ERROR")
|
||||
defer os.Unsetenv("MM_LOGSETTINGS_CONSOLELEVEL")
|
||||
|
||||
th := SetupEnterprise(t)
|
||||
defer th.TearDown()
|
||||
|
||||
// Verify the console level was set from the environment variable
|
||||
config := th.App.Config()
|
||||
assert.Equal(t, "ERROR", *config.LogSettings.ConsoleLevel)
|
||||
})
|
||||
|
||||
t.Run("Only MM_LOGSETTINGS_CONSOLELEVEL is manually processed", func(t *testing.T) {
|
||||
// never run with t.Parallel()
|
||||
|
||||
// This test verifies that we haven't accidentally enabled general environment
|
||||
// variable processing - we only manually handle MM_LOGSETTINGS_CONSOLELEVEL
|
||||
|
||||
// First, test without MM_LOGSETTINGS_CONSOLELEVEL set
|
||||
os.Unsetenv("MM_LOGSETTINGS_CONSOLELEVEL")
|
||||
|
||||
th1 := SetupEnterprise(t)
|
||||
config1 := th1.App.Config()
|
||||
defaultConsoleLevel := *config1.LogSettings.ConsoleLevel
|
||||
th1.TearDown()
|
||||
|
||||
// Now test with MM_LOGSETTINGS_CONSOLELEVEL set
|
||||
os.Setenv("MM_LOGSETTINGS_CONSOLELEVEL", "DEBUG")
|
||||
defer os.Unsetenv("MM_LOGSETTINGS_CONSOLELEVEL")
|
||||
|
||||
th2 := SetupEnterprise(t)
|
||||
config2 := th2.App.Config()
|
||||
customConsoleLevel := *config2.LogSettings.ConsoleLevel
|
||||
th2.TearDown()
|
||||
|
||||
// Verify our manual implementation works
|
||||
assert.Equal(t, mlog.LvlStdLog.Name, defaultConsoleLevel, "Default should be stdlog")
|
||||
assert.Equal(t, "DEBUG", customConsoleLevel, "Environment variable should be respected")
|
||||
assert.NotEqual(t, defaultConsoleLevel, customConsoleLevel, "Values should be different")
|
||||
})
|
||||
}
|
||||
83
channels/api4/audit_logging.go
Normal file
83
channels/api4/audit_logging.go
Normal file
@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func (api *API) InitAuditLogging() {
|
||||
api.BaseRoutes.AuditLogs.Handle("/certificate", api.APISessionRequired(addAuditLogCertificate)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.AuditLogs.Handle("/certificate", api.APISessionRequired(removeAuditLogCertificate)).Methods(http.MethodDelete)
|
||||
}
|
||||
|
||||
func parseAuditLogCertificateRequest(r *http.Request, maxFileSize int64) (*multipart.FileHeader, *model.AppError) {
|
||||
err := r.ParseMultipartForm(maxFileSize)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("addAuditLogCertificate", "api.admin.add_certificate.no_file.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
}
|
||||
|
||||
m := r.MultipartForm
|
||||
|
||||
fileArray, ok := m.File["certificate"]
|
||||
if !ok || len(fileArray) == 0 {
|
||||
return nil, model.NewAppError("addAuditLogCertificate", "api.admin.add_certificate.no_file.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if len(fileArray) > 1 {
|
||||
return nil, model.NewAppError("addAuditLogCertificate", "api.admin.add_certificate.multiple_files.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return fileArray[0], nil
|
||||
}
|
||||
|
||||
func addAuditLogCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.Logger.Debug("addAuditLogCertificate")
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteExperimentalFeatures) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteExperimentalFeatures)
|
||||
return
|
||||
}
|
||||
|
||||
fileData, err := parseAuditLogCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventAddAuditLogCertificate, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "filename", fileData.Filename)
|
||||
|
||||
if err := c.App.AddAuditLogCertificate(c.AppContext, fileData); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func removeAuditLogCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.Logger.Debug("removeAuditLogCertificate")
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteExperimentalFeatures) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteExperimentalFeatures)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventRemoveAuditLogCertificate, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if err := c.App.RemoveAuditLogCertificate(c.AppContext); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
319
channels/api4/bot.go
Normal file
319
channels/api4/bot.go
Normal file
@ -0,0 +1,319 @@
|
||||
// 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/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitBot() {
|
||||
api.BaseRoutes.Bots.Handle("", api.APISessionRequired(createBot)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Bot.Handle("", api.APISessionRequired(patchBot)).Methods(http.MethodPut)
|
||||
api.BaseRoutes.Bot.Handle("", api.APISessionRequired(getBot)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Bots.Handle("", api.APISessionRequired(getBots)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Bot.Handle("/disable", api.APISessionRequired(disableBot)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Bot.Handle("/enable", api.APISessionRequired(enableBot)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Bot.Handle("/convert_to_user", api.APISessionRequired(convertBotToUser)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Bot.Handle("/assign/{user_id:[A-Za-z0-9]+}", api.APISessionRequired(assignBot)).Methods(http.MethodPost)
|
||||
}
|
||||
|
||||
func createBot(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var botPatch *model.BotPatch
|
||||
err := json.NewDecoder(r.Body).Decode(&botPatch)
|
||||
if err != nil || botPatch == nil {
|
||||
c.SetInvalidParamWithErr("bot", err)
|
||||
return
|
||||
}
|
||||
|
||||
bot := &model.Bot{
|
||||
OwnerId: c.AppContext.Session().UserId,
|
||||
}
|
||||
bot.Patch(botPatch)
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventCreateBot, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterAuditableToAuditRec(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 || botPatch == nil {
|
||||
c.SetInvalidParamWithErr("bot", err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventPatchBot, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "id", botUserId)
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "bot", botPatch)
|
||||
|
||||
if err := c.App.SessionHasPermissionToManageBot(c.AppContext, *c.AppContext.Session(), botUserId); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
updatedBot, appErr := c.App.PatchBot(c.AppContext, 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(c.AppContext, 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("permissions", 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("permissions", 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(c.AppContext, &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(model.AuditEventUpdateBotActive, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "id", botUserId)
|
||||
model.AddEventParameterToAuditRec(auditRec, "enable", active)
|
||||
|
||||
if err := c.App.SessionHasPermissionToManageBot(c.AppContext, *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(model.AuditEventAssignBot, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "id", botUserId)
|
||||
model.AddEventParameterToAuditRec(auditRec, "user_id", userId)
|
||||
|
||||
if err := c.App.SessionHasPermissionToManageBot(c.AppContext, *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(c.AppContext, 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.AppContext, 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(model.AuditEventConvertBotToUser, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "bot", bot)
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "user_patch", &userPatch)
|
||||
model.AddEventParameterToAuditRec(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))
|
||||
}
|
||||
}
|
||||
17
channels/api4/bot_local.go
Normal file
17
channels/api4/bot_local.go
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import "net/http"
|
||||
|
||||
func (api *API) InitBotLocal() {
|
||||
api.BaseRoutes.Bot.Handle("", api.APILocal(getBot)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Bot.Handle("", api.APILocal(patchBot)).Methods(http.MethodPut)
|
||||
api.BaseRoutes.Bot.Handle("/disable", api.APILocal(disableBot)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Bot.Handle("/enable", api.APILocal(enableBot)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Bot.Handle("/convert_to_user", api.APILocal(convertBotToUser)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Bot.Handle("/assign/{user_id:[A-Za-z0-9]+}", api.APILocal(assignBot)).Methods(http.MethodPost)
|
||||
|
||||
api.BaseRoutes.Bots.Handle("", api.APILocal(getBots)).Methods(http.MethodGet)
|
||||
}
|
||||
1597
channels/api4/bot_test.go
Normal file
1597
channels/api4/bot_test.go
Normal file
File diff suppressed because it is too large
Load Diff
105
channels/api4/brand.go
Normal file
105
channels/api4/brand.go
Normal file
@ -0,0 +1,105 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitBrand() {
|
||||
api.BaseRoutes.Brand.Handle("/image", api.APIHandlerTrustRequester(getBrandImage)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Brand.Handle("/image", api.APISessionRequired(uploadBrandImage, handlerParamFileAPI)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Brand.Handle("/image", api.APISessionRequired(deleteBrandImage)).Methods(http.MethodDelete)
|
||||
}
|
||||
|
||||
func getBrandImage(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// No permission check required
|
||||
|
||||
img, err := c.App.GetBrandImage(c.AppContext)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
if _, err := w.Write(nil); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
if _, err := w.Write(img); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func uploadBrandImage(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if _, err := io.Copy(io.Discard, r.Body); err != nil {
|
||||
c.Logger.Warn("Error discarding request body", mlog.Err(err))
|
||||
}
|
||||
}()
|
||||
|
||||
if r.ContentLength > *c.App.Config().FileSettings.MaxFileSize {
|
||||
c.Err = model.NewAppError("uploadBrandImage", "api.admin.upload_brand_image.too_large.app_error", nil, "", http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize); err != nil {
|
||||
c.Err = model.NewAppError("uploadBrandImage", "api.admin.upload_brand_image.parse.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
m := r.MultipartForm
|
||||
|
||||
imageArray, ok := m.File["image"]
|
||||
if !ok {
|
||||
c.Err = model.NewAppError("uploadBrandImage", "api.admin.upload_brand_image.no_file.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(imageArray) <= 0 {
|
||||
c.Err = model.NewAppError("uploadBrandImage", "api.admin.upload_brand_image.array.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventUploadBrandImage, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionEditBrand) {
|
||||
c.SetPermissionError(model.PermissionEditBrand)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.SaveBrandImage(c.AppContext, imageArray[0]); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
c.LogAudit("")
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func deleteBrandImage(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventDeleteBrandImage, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionEditBrand) {
|
||||
c.SetPermissionError(model.PermissionEditBrand)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.DeleteBrandImage(c.AppContext); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
170
channels/api4/brand_test.go
Normal file
170
channels/api4/brand_test.go
Normal file
@ -0,0 +1,170 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils/testutils"
|
||||
)
|
||||
|
||||
func TestGetBrandImage(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
client := th.Client
|
||||
|
||||
_, resp, err := client.GetBrandImage(context.Background())
|
||||
require.Error(t, err)
|
||||
CheckNotFoundStatus(t, resp)
|
||||
|
||||
_, err = client.Logout(context.Background())
|
||||
require.NoError(t, err)
|
||||
_, resp, err = client.GetBrandImage(context.Background())
|
||||
require.Error(t, err)
|
||||
CheckNotFoundStatus(t, resp)
|
||||
|
||||
_, resp, err = th.SystemAdminClient.GetBrandImage(context.Background())
|
||||
require.Error(t, err)
|
||||
CheckNotFoundStatus(t, resp)
|
||||
}
|
||||
|
||||
func TestUploadBrandImage(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
client := th.Client
|
||||
|
||||
data, err := testutils.ReadTestFile("test.png")
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := client.UploadBrandImage(context.Background(), data)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
// status code returns either forbidden or unauthorized
|
||||
// note: forbidden is set as default at Client4.SetProfileImage when request is terminated early by server
|
||||
_, err = client.Logout(context.Background())
|
||||
require.NoError(t, err)
|
||||
resp, err = client.UploadBrandImage(context.Background(), data)
|
||||
require.Error(t, err)
|
||||
if resp.StatusCode == http.StatusForbidden {
|
||||
CheckForbiddenStatus(t, resp)
|
||||
} else if resp.StatusCode == http.StatusUnauthorized {
|
||||
CheckUnauthorizedStatus(t, resp)
|
||||
} else {
|
||||
require.Fail(t, "Should have failed either forbidden or unauthorized")
|
||||
}
|
||||
|
||||
resp, err = th.SystemAdminClient.UploadBrandImage(context.Background(), data)
|
||||
require.NoError(t, err)
|
||||
CheckCreatedStatus(t, resp)
|
||||
}
|
||||
|
||||
func TestUploadBrandImageTwice(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
data, err := testutils.ReadTestFile("test.png")
|
||||
require.NoError(t, err)
|
||||
|
||||
// First upload as system admin
|
||||
resp, err := th.SystemAdminClient.UploadBrandImage(context.Background(), data)
|
||||
require.NoError(t, err)
|
||||
CheckCreatedStatus(t, resp)
|
||||
|
||||
// Verify the image exists and contents match what was uploaded
|
||||
receivedImg, resp, err := th.SystemAdminClient.GetBrandImage(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, receivedImg)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.NotEmpty(t, receivedImg, "Received image data should not be empty")
|
||||
|
||||
// Get the list of files in the brand directory
|
||||
files, err := th.App.FileBackend().ListDirectory("brand/")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, files, 1, "Expected only the original image file")
|
||||
|
||||
// ListDirectory returns paths with the directory prefix included
|
||||
fileName := files[0]
|
||||
fileName = strings.TrimPrefix(fileName, "brand/")
|
||||
require.Equal(t, "image.png", fileName, "Expected the original image file")
|
||||
|
||||
// Second upload (which should back up the previous one)
|
||||
data2, err := testutils.ReadTestFile("test.tiff")
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err = th.SystemAdminClient.UploadBrandImage(context.Background(), data2)
|
||||
require.NoError(t, err)
|
||||
CheckCreatedStatus(t, resp)
|
||||
|
||||
// Get the list of files in the brand directory again
|
||||
files, err = th.App.FileBackend().ListDirectory("brand/")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should now have the new image.png and a backup with timestamp
|
||||
require.Len(t, files, 2, "Expected the original and backup files")
|
||||
|
||||
// Check that one of the files is image.png
|
||||
hasOriginal := false
|
||||
hasBackup := false
|
||||
for _, file := range files {
|
||||
// ListDirectory returns paths with the directory prefix included
|
||||
fileName := strings.TrimPrefix(file, "brand/")
|
||||
|
||||
if fileName == "image.png" {
|
||||
hasOriginal = true
|
||||
} else if strings.HasSuffix(fileName, ".png") && strings.Contains(fileName, "-") {
|
||||
// Backup file should have a timestamp format like 2006-01-02T15:04:05.png
|
||||
hasBackup = true
|
||||
}
|
||||
}
|
||||
|
||||
require.True(t, hasOriginal, "Original image.png file should exist")
|
||||
require.True(t, hasBackup, "Backup image file should exist")
|
||||
|
||||
// Verify the new image is available through the API and matches what was uploaded
|
||||
receivedImg2, resp, err := th.SystemAdminClient.GetBrandImage(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, receivedImg2)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.NotEmpty(t, receivedImg2, "Received image data should not be empty")
|
||||
}
|
||||
|
||||
func TestDeleteBrandImage(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
data, err := testutils.ReadTestFile("test.png")
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := th.SystemAdminClient.UploadBrandImage(context.Background(), data)
|
||||
require.NoError(t, err)
|
||||
CheckCreatedStatus(t, resp)
|
||||
|
||||
resp, err = th.Client.DeleteBrandImage(context.Background())
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
_, err = th.Client.Logout(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err = th.Client.DeleteBrandImage(context.Background())
|
||||
require.Error(t, err)
|
||||
CheckUnauthorizedStatus(t, resp)
|
||||
|
||||
resp, err = th.SystemAdminClient.DeleteBrandImage(context.Background())
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
|
||||
resp, err = th.SystemAdminClient.DeleteBrandImage(context.Background())
|
||||
require.Error(t, err)
|
||||
CheckNotFoundStatus(t, resp)
|
||||
}
|
||||
2568
channels/api4/channel.go
Normal file
2568
channels/api4/channel.go
Normal file
File diff suppressed because it is too large
Load Diff
433
channels/api4/channel_bookmark.go
Normal file
433
channels/api4/channel_bookmark.go
Normal file
@ -0,0 +1,433 @@
|
||||
// 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/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitChannelBookmarks() {
|
||||
if api.srv.Config().FeatureFlags.ChannelBookmarks {
|
||||
api.BaseRoutes.ChannelBookmarks.Handle("", api.APISessionRequired(createChannelBookmark)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.ChannelBookmark.Handle("", api.APISessionRequired(updateChannelBookmark)).Methods(http.MethodPatch)
|
||||
api.BaseRoutes.ChannelBookmark.Handle("/sort_order", api.APISessionRequired(updateChannelBookmarkSortOrder)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.ChannelBookmark.Handle("", api.APISessionRequired(deleteChannelBookmark)).Methods(http.MethodDelete)
|
||||
api.BaseRoutes.ChannelBookmarks.Handle("", api.APISessionRequired(listChannelBookmarksForChannel)).Methods(http.MethodGet)
|
||||
}
|
||||
}
|
||||
|
||||
func createChannelBookmark(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if c.App.Channels().License() == nil {
|
||||
c.Err = model.NewAppError("createChannelBookmark", "api.channel.bookmark.channel_bookmark.license.error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
connectionID := r.Header.Get(model.ConnectionId)
|
||||
|
||||
c.RequireChannelId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if channel.DeleteAt != 0 {
|
||||
c.Err = model.NewAppError("createChannelBookmark", "api.channel.bookmark.create_channel_bookmark.deleted_channel.forbidden.app_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
var channelBookmark *model.ChannelBookmark
|
||||
err := json.NewDecoder(r.Body).Decode(&channelBookmark)
|
||||
if err != nil || channelBookmark == nil {
|
||||
c.SetInvalidParamWithErr("channelBookmark", err)
|
||||
return
|
||||
}
|
||||
channelBookmark.ChannelId = c.Params.ChannelId
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventCreateChannelBookmark, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "channelBookmark", channelBookmark)
|
||||
|
||||
switch channel.Type {
|
||||
case model.ChannelTypeOpen:
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionAddBookmarkPublicChannel) {
|
||||
c.SetPermissionError(model.PermissionAddBookmarkPublicChannel)
|
||||
return
|
||||
}
|
||||
|
||||
case model.ChannelTypePrivate:
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionAddBookmarkPrivateChannel) {
|
||||
c.SetPermissionError(model.PermissionAddBookmarkPrivateChannel)
|
||||
return
|
||||
}
|
||||
|
||||
case model.ChannelTypeGroup, model.ChannelTypeDirect:
|
||||
// Any member of DM/GMs but guests can manage channel bookmarks
|
||||
if _, errGet := c.App.GetChannelMember(c.AppContext, channel.Id, c.AppContext.Session().UserId); errGet != nil {
|
||||
c.Err = model.NewAppError("createChannelBookmark", "api.channel.bookmark.create_channel_bookmark.direct_or_group_channels.forbidden.app_error", nil, errGet.Message, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
user, gAppErr := c.App.GetUser(c.AppContext.Session().UserId)
|
||||
if gAppErr != nil {
|
||||
c.Err = gAppErr
|
||||
return
|
||||
}
|
||||
|
||||
if user.IsGuest() {
|
||||
c.Err = model.NewAppError("createChannelBookmark", "api.channel.bookmark.create_channel_bookmark.direct_or_group_channels_by_guests.forbidden.app_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
c.Err = model.NewAppError("createChannelBookmark", "api.channel.bookmark.create_channel_bookmark.forbidden.app_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
newChannelBookmark, appErr := c.App.CreateChannelBookmark(c.AppContext, channelBookmark, connectionID)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(newChannelBookmark)
|
||||
auditRec.AddEventObjectType("channelBookmarkWithFileInfo")
|
||||
c.LogAudit("display_name=" + newChannelBookmark.DisplayName)
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(newChannelBookmark); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func updateChannelBookmark(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if c.App.Channels().License() == nil {
|
||||
c.Err = model.NewAppError("updateChannelBookmark", "api.channel.bookmark.channel_bookmark.license.error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
connectionID := r.Header.Get(model.ConnectionId)
|
||||
|
||||
c.RequireChannelId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var patch *model.ChannelBookmarkPatch
|
||||
if err := json.NewDecoder(r.Body).Decode(&patch); err != nil || patch == nil {
|
||||
c.SetInvalidParamWithErr("channelBookmarkPatch", err)
|
||||
return
|
||||
}
|
||||
|
||||
originalChannelBookmark, appErr := c.App.GetBookmark(c.Params.ChannelBookmarkId, false)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
patchedBookmark := originalChannelBookmark.Clone()
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventUpdateChannelBookmark, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "channelBookmark", patch)
|
||||
|
||||
// The channel bookmark should belong to the same channel specified in the URL
|
||||
if patchedBookmark.ChannelId != c.Params.ChannelId {
|
||||
c.SetInvalidParam("channel_id")
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventPriorState(originalChannelBookmark)
|
||||
|
||||
channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if channel.DeleteAt != 0 {
|
||||
c.Err = model.NewAppError("updateChannelBookmark", "api.channel.bookmark.update_channel_bookmark.deleted_channel.forbidden.app_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
switch channel.Type {
|
||||
case model.ChannelTypeOpen:
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionEditBookmarkPublicChannel) {
|
||||
c.SetPermissionError(model.PermissionEditBookmarkPublicChannel)
|
||||
return
|
||||
}
|
||||
|
||||
case model.ChannelTypePrivate:
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionEditBookmarkPrivateChannel) {
|
||||
c.SetPermissionError(model.PermissionEditBookmarkPrivateChannel)
|
||||
return
|
||||
}
|
||||
|
||||
case model.ChannelTypeGroup, model.ChannelTypeDirect:
|
||||
// Any member of DM/GMs but guests can manage channel bookmarks
|
||||
if _, errGet := c.App.GetChannelMember(c.AppContext, channel.Id, c.AppContext.Session().UserId); errGet != nil {
|
||||
c.Err = model.NewAppError("updateChannelBookmark", "api.channel.bookmark.update_channel_bookmark.direct_or_group_channels.forbidden.app_error", nil, errGet.Message, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
user, gAppErr := c.App.GetUser(c.AppContext.Session().UserId)
|
||||
if gAppErr != nil {
|
||||
c.Err = gAppErr
|
||||
return
|
||||
}
|
||||
|
||||
if user.IsGuest() {
|
||||
c.Err = model.NewAppError("updateChannelBookmark", "api.channel.bookmark.update_channel_bookmark.direct_or_group_channels_by_guests.forbidden.app_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
c.Err = model.NewAppError("updateChannelBookmark", "api.channel.bookmark.update_channel_bookmark.forbidden.app_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
patchedBookmark.Patch(patch)
|
||||
updateChannelBookmarkResponse, appErr := c.App.UpdateChannelBookmark(c.AppContext, patchedBookmark, connectionID)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(updateChannelBookmarkResponse)
|
||||
auditRec.AddEventObjectType("updateChannelBookmarkResponse")
|
||||
c.LogAudit("")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(updateChannelBookmarkResponse); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func updateChannelBookmarkSortOrder(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if c.App.Channels().License() == nil {
|
||||
c.Err = model.NewAppError("updateChannelBookmarkSortOrder", "api.channel.bookmark.channel_bookmark.license.error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
connectionID := r.Header.Get(model.ConnectionId)
|
||||
|
||||
c.RequireChannelId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var newSortOrder int64
|
||||
if err := json.NewDecoder(r.Body).Decode(&newSortOrder); err != nil {
|
||||
c.SetInvalidParamWithErr("channelBookmarkSortOrder", err)
|
||||
return
|
||||
}
|
||||
|
||||
if newSortOrder < 0 {
|
||||
c.SetInvalidParam("channelBookmarkSortOrder")
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventUpdateChannelBookmarkSortOrder, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "id", c.Params.ChannelBookmarkId)
|
||||
|
||||
channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if channel.DeleteAt != 0 {
|
||||
c.Err = model.NewAppError("updateChannelBookmarkSortOrder", "api.channel.bookmark.update_channel_bookmark_sort_order.deleted_channel.forbidden.app_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
switch channel.Type {
|
||||
case model.ChannelTypeOpen:
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionOrderBookmarkPublicChannel) {
|
||||
c.SetPermissionError(model.PermissionOrderBookmarkPublicChannel)
|
||||
return
|
||||
}
|
||||
|
||||
case model.ChannelTypePrivate:
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionOrderBookmarkPrivateChannel) {
|
||||
c.SetPermissionError(model.PermissionOrderBookmarkPrivateChannel)
|
||||
return
|
||||
}
|
||||
|
||||
case model.ChannelTypeGroup, model.ChannelTypeDirect:
|
||||
// Any member of DM/GMs but guests can manage channel bookmarks
|
||||
if _, errGet := c.App.GetChannelMember(c.AppContext, channel.Id, c.AppContext.Session().UserId); errGet != nil {
|
||||
c.Err = model.NewAppError("updateChannelBookmarkSortOrder", "api.channel.bookmark.update_channel_bookmark_sort_order.direct_or_group_channels.forbidden.app_error", nil, errGet.Message, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
user, gAppErr := c.App.GetUser(c.AppContext.Session().UserId)
|
||||
if gAppErr != nil {
|
||||
c.Err = gAppErr
|
||||
return
|
||||
}
|
||||
|
||||
if user.IsGuest() {
|
||||
c.Err = model.NewAppError("updateChannelBookmarkSortOrder", "api.channel.bookmark.update_channel_bookmark_sort_order.direct_or_group_channels_by_guests.forbidden.app_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
c.Err = model.NewAppError("updateChannelBookmarkSortOrder", "api.channel.bookmark.update_channel_bookmark_sort_order.forbidden.app_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
bookmarks, appErr := c.App.UpdateChannelBookmarkSortOrder(c.Params.ChannelBookmarkId, c.Params.ChannelId, newSortOrder, connectionID)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
for _, b := range bookmarks {
|
||||
if b.Id == c.Params.ChannelBookmarkId {
|
||||
auditRec.AddEventResultState(b)
|
||||
auditRec.AddEventObjectType("channelBookmarkWithFileInfo")
|
||||
break
|
||||
}
|
||||
}
|
||||
auditRec.Success()
|
||||
c.LogAudit("")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(bookmarks); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func deleteChannelBookmark(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if c.App.Channels().License() == nil {
|
||||
c.Err = model.NewAppError("deleteChannelBookmark", "api.channel.bookmark.channel_bookmark.license.error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
connectionID := r.Header.Get(model.ConnectionId)
|
||||
|
||||
c.RequireChannelId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventDeleteChannelBookmark, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "id", c.Params.ChannelBookmarkId)
|
||||
|
||||
channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if channel.DeleteAt != 0 {
|
||||
c.Err = model.NewAppError("deleteChannelBookmark", "api.channel.bookmark.delete_channel_bookmark.deleted_channel.forbidden.app_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
switch channel.Type {
|
||||
case model.ChannelTypeOpen:
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionDeleteBookmarkPublicChannel) {
|
||||
c.SetPermissionError(model.PermissionDeleteBookmarkPublicChannel)
|
||||
return
|
||||
}
|
||||
|
||||
case model.ChannelTypePrivate:
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionDeleteBookmarkPrivateChannel) {
|
||||
c.SetPermissionError(model.PermissionDeleteBookmarkPrivateChannel)
|
||||
return
|
||||
}
|
||||
|
||||
case model.ChannelTypeGroup, model.ChannelTypeDirect:
|
||||
// Any member of DM/GMs but guests can manage channel bookmarks
|
||||
if _, errGet := c.App.GetChannelMember(c.AppContext, channel.Id, c.AppContext.Session().UserId); errGet != nil {
|
||||
c.Err = model.NewAppError("deleteChannelBookmark", "api.channel.bookmark.delete_channel_bookmark.direct_or_group_channels.forbidden.app_error", nil, errGet.Message, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
user, gAppErr := c.App.GetUser(c.AppContext.Session().UserId)
|
||||
if gAppErr != nil {
|
||||
c.Err = gAppErr
|
||||
return
|
||||
}
|
||||
|
||||
if user.IsGuest() {
|
||||
c.Err = model.NewAppError("deleteChannelBookmark", "api.channel.bookmark.delete_channel_bookmark.direct_or_group_channels_by_guests.forbidden.app_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
c.Err = model.NewAppError("deleteChannelBookmark", "api.channel.bookmark.delete_channel_bookmark.forbidden.app_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
oldBookmark, obErr := c.App.GetBookmark(c.Params.ChannelBookmarkId, false)
|
||||
if obErr != nil {
|
||||
c.Err = obErr
|
||||
return
|
||||
}
|
||||
|
||||
// The channel bookmark should belong to the same channel specified in the URL
|
||||
if oldBookmark.ChannelId != c.Params.ChannelId {
|
||||
c.SetInvalidParam("channel_id")
|
||||
return
|
||||
}
|
||||
auditRec.AddEventPriorState(oldBookmark)
|
||||
|
||||
bookmark, appErr := c.App.DeleteChannelBookmark(c.Params.ChannelBookmarkId, connectionID)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(bookmark)
|
||||
c.LogAudit("bookmark=" + bookmark.DisplayName)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(bookmark); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func listChannelBookmarksForChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if c.App.Channels().License() == nil {
|
||||
c.Err = model.NewAppError("listChannelBookmarksForChannel", "api.channel.bookmark.channel_bookmark.license.error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireChannelId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel) {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
|
||||
bookmarks, appErr := c.App.GetChannelBookmarks(c.Params.ChannelId, c.Params.BookmarksSince)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(bookmarks); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
1704
channels/api4/channel_bookmark_test.go
Normal file
1704
channels/api4/channel_bookmark_test.go
Normal file
File diff suppressed because it is too large
Load Diff
395
channels/api4/channel_category.go
Normal file
395
channels/api4/channel_category.go
Normal file
@ -0,0 +1,395 @@
|
||||
// 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/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/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
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
|
||||
c.SetPermissionError(model.PermissionViewTeam)
|
||||
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
|
||||
}
|
||||
|
||||
if _, err := w.Write(categoriesJSON); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
|
||||
c.SetPermissionError(model.PermissionViewTeam)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventCreateCategoryForTeamForUser, model.AuditStatusFail)
|
||||
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()
|
||||
|
||||
if _, err := w.Write(categoryJSON); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
|
||||
c.SetPermissionError(model.PermissionViewTeam)
|
||||
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
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
|
||||
c.SetPermissionError(model.PermissionViewTeam)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventUpdateCategoryOrderForTeamForUser, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
categoryOrder, err := model.NonSortedArrayFromJSON(r.Body)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("updateCategoryOrderForTeamForUser", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, categoryId := range categoryOrder {
|
||||
if !c.App.SessionHasPermissionToCategory(c.AppContext, *c.AppContext.Session(), c.Params.UserId, c.Params.TeamId, categoryId) {
|
||||
c.SetInvalidParam("category")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
appErr := c.App.UpdateSidebarCategoryOrder(c.AppContext, c.Params.UserId, c.Params.TeamId, categoryOrder)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
if _, err := w.Write([]byte(model.ArrayToJSON(categoryOrder))); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
|
||||
c.SetPermissionError(model.PermissionViewTeam)
|
||||
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
|
||||
}
|
||||
|
||||
if _, err := w.Write(categoriesJSON); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
|
||||
c.SetPermissionError(model.PermissionViewTeam)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventUpdateCategoriesForTeamForUser, model.AuditStatusFail)
|
||||
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()
|
||||
if _, err := w.Write(categoriesJSON); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
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, channelListToMap(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, "", http.StatusBadRequest).Wrap(err)
|
||||
}
|
||||
|
||||
channelMap := channelListToMap(channels)
|
||||
|
||||
for _, category := range categories {
|
||||
category.Channels = validateSidebarCategoryChannels(c, userId, category.Channels, channelMap)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func channelListToMap(channelList model.ChannelList) map[string]*model.Channel {
|
||||
channelMap := make(map[string]*model.Channel, len(channelList))
|
||||
for _, channel := range channelList {
|
||||
channelMap[channel.Id] = channel
|
||||
}
|
||||
return channelMap
|
||||
}
|
||||
|
||||
// validateSidebarCategoryChannels returns a normalized slice of channel IDs by removing duplicates from it and
|
||||
// ensuring that it only contains IDs of channels in the given map of Channels by IDs.
|
||||
func validateSidebarCategoryChannels(c *Context, userId string, channelIds []string, channelMap map[string]*model.Channel) []string {
|
||||
var filtered []string
|
||||
|
||||
for _, channelId := range channelIds {
|
||||
if _, ok := channelMap[channelId]; ok {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
filtered = model.RemoveDuplicateStringsNonSort(filtered)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
|
||||
c.SetPermissionError(model.PermissionViewTeam)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventUpdateCategoryForTeamForUser, model.AuditStatusFail)
|
||||
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()
|
||||
if _, err := w.Write(categoryJSON); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
|
||||
c.SetPermissionError(model.PermissionViewTeam)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventDeleteCategoryForTeamForUser, model.AuditStatusFail)
|
||||
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)
|
||||
}
|
||||
1629
channels/api4/channel_category_test.go
Normal file
1629
channels/api4/channel_category_test.go
Normal file
File diff suppressed because it is too large
Load Diff
445
channels/api4/channel_local.go
Normal file
445
channels/api4/channel_local.go
Normal file
@ -0,0 +1,445 @@
|
||||
// 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/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app"
|
||||
)
|
||||
|
||||
func (api *API) InitChannelLocal() {
|
||||
api.BaseRoutes.Channels.Handle("", api.APILocal(getAllChannels)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Channels.Handle("", api.APILocal(localCreateChannel)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Channel.Handle("", api.APILocal(getChannel)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ChannelByName.Handle("", api.APILocal(getChannelByName)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Channel.Handle("", api.APILocal(localDeleteChannel)).Methods(http.MethodDelete)
|
||||
api.BaseRoutes.Channel.Handle("/patch", api.APILocal(localPatchChannel)).Methods(http.MethodPut)
|
||||
api.BaseRoutes.Channel.Handle("/move", api.APILocal(localMoveChannel)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Channel.Handle("/privacy", api.APILocal(localUpdateChannelPrivacy)).Methods(http.MethodPut)
|
||||
api.BaseRoutes.Channel.Handle("/restore", api.APILocal(localRestoreChannel)).Methods(http.MethodPost)
|
||||
|
||||
api.BaseRoutes.ChannelMember.Handle("", api.APILocal(localRemoveChannelMember)).Methods(http.MethodDelete)
|
||||
api.BaseRoutes.ChannelMember.Handle("", api.APILocal(getChannelMember)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ChannelMembers.Handle("", api.APILocal(localAddChannelMember)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.ChannelMembers.Handle("", api.APILocal(getChannelMembers)).Methods(http.MethodGet)
|
||||
|
||||
api.BaseRoutes.ChannelsForTeam.Handle("", api.APILocal(getPublicChannelsForTeam)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ChannelsForTeam.Handle("/deleted", api.APILocal(getDeletedChannelsForTeam)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ChannelsForTeam.Handle("/private", api.APILocal(getPrivateChannelsForTeam)).Methods(http.MethodGet)
|
||||
|
||||
api.BaseRoutes.ChannelByName.Handle("", api.APILocal(getChannelByName)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ChannelByNameForTeamName.Handle("", api.APILocal(getChannelByNameForTeamName)).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
func localCreateChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var channel *model.Channel
|
||||
err := json.NewDecoder(r.Body).Decode(&channel)
|
||||
if err != nil || channel == nil {
|
||||
c.SetInvalidParamWithErr("channel", err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventLocalCreateChannel, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "channel", channel)
|
||||
|
||||
sc, appErr := c.App.CreateChannel(c.AppContext, channel, false)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(sc)
|
||||
auditRec.AddEventObjectType("channel")
|
||||
c.LogAudit("name=" + channel.Name)
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(sc); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func localUpdateChannelPrivacy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireChannelId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
props := model.StringInterfaceFromJSON(r.Body)
|
||||
privacy, ok := props["privacy"].(string)
|
||||
if !ok || (model.ChannelType(privacy) != model.ChannelTypeOpen && model.ChannelType(privacy) != model.ChannelTypePrivate) {
|
||||
c.SetInvalidParam("privacy")
|
||||
return
|
||||
}
|
||||
|
||||
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventLocalUpdateChannelPrivacy, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "privacy", privacy)
|
||||
|
||||
if channel.Name == model.DefaultChannelName && model.ChannelType(privacy) == model.ChannelTypePrivate {
|
||||
c.Err = model.NewAppError("updateChannelPrivacy", "api.channel.update_channel_privacy.default_channel_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
channel.Type = model.ChannelType(privacy)
|
||||
|
||||
updatedChannel, err := c.App.UpdateChannelPrivacy(c.AppContext, channel, nil)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventResultState(channel)
|
||||
auditRec.AddEventObjectType("channel")
|
||||
auditRec.Success()
|
||||
c.LogAudit("name=" + updatedChannel.Name)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(updatedChannel); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func localRestoreChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireChannelId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventLocalRestoreChannel, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "channel_id", c.Params.ChannelId)
|
||||
|
||||
channel, err = c.App.RestoreChannel(c.AppContext, channel, "")
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventResultState(channel)
|
||||
auditRec.AddEventObjectType("channel")
|
||||
auditRec.Success()
|
||||
c.LogAudit("name=" + channel.Name)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(channel); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func localAddChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireChannelId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventLocalAddChannelMember, model.AuditStatusFail)
|
||||
model.AddEventParameterToAuditRec(auditRec, "channel_id", c.Params.ChannelId)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
props := model.StringInterfaceFromJSON(r.Body)
|
||||
userId, ok := props["user_id"].(string)
|
||||
if !ok || !model.IsValidId(userId) {
|
||||
c.SetInvalidParam("user_id")
|
||||
return
|
||||
}
|
||||
|
||||
model.AddEventParameterToAuditRec(auditRec, "user_id", userId)
|
||||
|
||||
member := &model.ChannelMember{
|
||||
ChannelId: c.Params.ChannelId,
|
||||
UserId: userId,
|
||||
}
|
||||
|
||||
postRootId, ok := props["post_root_id"].(string)
|
||||
if ok && postRootId != "" && !model.IsValidId(postRootId) {
|
||||
c.SetInvalidParam("post_root_id")
|
||||
return
|
||||
}
|
||||
|
||||
model.AddEventParameterToAuditRec(auditRec, "post_root_id", postRootId)
|
||||
|
||||
if ok && len(postRootId) == 26 {
|
||||
rootPost, err := c.App.GetSinglePost(c.AppContext, postRootId, false)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
if rootPost.ChannelId != member.ChannelId {
|
||||
c.SetInvalidParam("post_root_id")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
channel, err := c.App.GetChannel(c.AppContext, member.ChannelId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "channel", channel)
|
||||
|
||||
if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
|
||||
c.Err = model.NewAppError("localAddChannelMember", "api.channel.add_user_to_channel.type.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if channel.IsGroupConstrained() {
|
||||
nonMembers, err := c.App.FilterNonGroupChannelMembers(c.AppContext, []string{member.UserId}, channel)
|
||||
if err != nil {
|
||||
if v, ok := err.(*model.AppError); ok {
|
||||
c.Err = v
|
||||
} else {
|
||||
c.Err = model.NewAppError("localAddChannelMember", "api.channel.add_members.error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if len(nonMembers) > 0 {
|
||||
c.Err = model.NewAppError("localAddChannelMember", "api.channel.add_members.user_denied", map[string]any{"UserIDs": nonMembers}, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cm, err := c.App.AddChannelMember(c.AppContext, member.UserId, channel, app.ChannelMemberOpts{
|
||||
PostRootID: postRootId,
|
||||
})
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddMeta("add_user_id", cm.UserId)
|
||||
auditRec.AddEventResultState(cm)
|
||||
auditRec.AddEventObjectType("channel_member")
|
||||
c.LogAudit("name=" + channel.Name + " user_id=" + cm.UserId)
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(cm); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func localRemoveChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireChannelId().RequireUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
user, err := c.App.GetUser(c.Params.UserId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if !(channel.Type == model.ChannelTypeOpen || channel.Type == model.ChannelTypePrivate) {
|
||||
c.Err = model.NewAppError("removeChannelMember", "api.channel.remove_channel_member.type.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if channel.IsGroupConstrained() && !user.IsBot {
|
||||
c.Err = model.NewAppError("removeChannelMember", "api.channel.remove_member.group_constrained.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventLocalRemoveChannelMember, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "channel_id", c.Params.ChannelId)
|
||||
model.AddEventParameterToAuditRec(auditRec, "remove_user_id", c.Params.UserId)
|
||||
|
||||
if err = c.App.RemoveUserFromChannel(c.AppContext, c.Params.UserId, "", channel); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
c.LogAudit("name=" + channel.Name + " user_id=" + c.Params.UserId)
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func localPatchChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireChannelId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var patch *model.ChannelPatch
|
||||
err := json.NewDecoder(r.Body).Decode(&patch)
|
||||
if err != nil || patch == nil {
|
||||
c.SetInvalidParamWithErr("channel", err)
|
||||
return
|
||||
}
|
||||
|
||||
originalOldChannel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
channel := originalOldChannel.DeepCopy()
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventLocalPatchChannel, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "channel_patch", patch)
|
||||
|
||||
channel.Patch(patch)
|
||||
rchannel, appErr := c.App.UpdateChannel(c.AppContext, channel)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
appErr = c.App.FillInChannelProps(c.AppContext, rchannel)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
c.LogAudit("")
|
||||
auditRec.AddEventResultState(rchannel)
|
||||
auditRec.AddEventObjectType("channel")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(rchannel); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func localMoveChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireChannelId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
props := model.StringInterfaceFromJSON(r.Body)
|
||||
teamId, ok := props["team_id"].(string)
|
||||
if !ok {
|
||||
c.SetInvalidParam("team_id")
|
||||
return
|
||||
}
|
||||
|
||||
force, ok := props["force"].(bool)
|
||||
if !ok {
|
||||
c.SetInvalidParam("force")
|
||||
return
|
||||
}
|
||||
|
||||
team, err := c.App.GetTeam(teamId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventLocalMoveChannel, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "team_id", teamId)
|
||||
model.AddEventParameterToAuditRec(auditRec, "force", force)
|
||||
|
||||
// TODO do we need these?
|
||||
auditRec.AddMeta("channel_id", channel.Id)
|
||||
auditRec.AddMeta("channel_name", channel.Name)
|
||||
auditRec.AddMeta("team_id", team.Id)
|
||||
auditRec.AddMeta("team_name", team.Name)
|
||||
|
||||
if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
|
||||
c.Err = model.NewAppError("moveChannel", "api.channel.move_channel.type.invalid", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
err = c.App.RemoveAllDeactivatedMembersFromChannel(c.AppContext, channel)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if force {
|
||||
err = c.App.RemoveUsersFromChannelNotMemberOfTeam(c.AppContext, nil, channel, team)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = c.App.MoveChannel(c.AppContext, team, channel, nil)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventResultState(channel)
|
||||
auditRec.AddEventObjectType("channel")
|
||||
auditRec.Success()
|
||||
c.LogAudit("channel=" + channel.Name)
|
||||
c.LogAudit("team=" + team.Name)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(channel); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func localDeleteChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireChannelId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventLocalDeleteChannel, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
auditRec.AddEventPriorState(channel)
|
||||
model.AddEventParameterToAuditRec(auditRec, "channel_id", c.Params.ChannelId)
|
||||
|
||||
if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
|
||||
c.Err = model.NewAppError("localDeleteChannel", "api.channel.delete_channel.type.invalid", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if c.Params.Permanent {
|
||||
err = c.App.PermanentDeleteChannel(c.AppContext, channel)
|
||||
} else {
|
||||
err = c.App.DeleteChannel(c.AppContext, channel, "")
|
||||
}
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(channel)
|
||||
auditRec.AddEventObjectType("channel")
|
||||
c.LogAudit("name=" + channel.Name)
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
6367
channels/api4/channel_test.go
Normal file
6367
channels/api4/channel_test.go
Normal file
File diff suppressed because it is too large
Load Diff
632
channels/api4/cloud.go
Normal file
632
channels/api4/cloud.go
Normal file
@ -0,0 +1,632 @@
|
||||
// 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/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/shared/web"
|
||||
)
|
||||
|
||||
func (api *API) InitCloud() {
|
||||
// GET /api/v4/cloud/products
|
||||
api.BaseRoutes.Cloud.Handle("/products", api.APISessionRequired(getCloudProducts)).Methods(http.MethodGet)
|
||||
// GET /api/v4/cloud/limits
|
||||
api.BaseRoutes.Cloud.Handle("/limits", api.APISessionRequired(getCloudLimits)).Methods(http.MethodGet)
|
||||
|
||||
// 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(http.MethodGet)
|
||||
api.BaseRoutes.Cloud.Handle("/customer", api.APISessionRequired(updateCloudCustomer)).Methods(http.MethodPut)
|
||||
api.BaseRoutes.Cloud.Handle("/customer/address", api.APISessionRequired(updateCloudCustomerAddress)).Methods(http.MethodPut)
|
||||
|
||||
// GET /api/v4/cloud/subscription
|
||||
api.BaseRoutes.Cloud.Handle("/subscription", api.APISessionRequired(getSubscription)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Cloud.Handle("/subscription/invoices", api.APISessionRequired(getInvoicesForSubscription)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Cloud.Handle("/subscription/invoices/{invoice_id:[_A-Za-z0-9]+}/pdf", api.APISessionRequired(getSubscriptionInvoicePDF)).Methods(http.MethodGet)
|
||||
|
||||
// GET /api/v4/cloud/validate-business-email
|
||||
api.BaseRoutes.Cloud.Handle("/validate-business-email", api.APISessionRequired(validateBusinessEmail)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Cloud.Handle("/validate-workspace-business-email", api.APISessionRequired(validateWorkspaceBusinessEmail)).Methods(http.MethodPost)
|
||||
|
||||
// POST /api/v4/cloud/webhook
|
||||
api.BaseRoutes.Cloud.Handle("/webhook", api.CloudAPIKeyRequired(handleCWSWebhook)).Methods(http.MethodPost)
|
||||
|
||||
// GET /api/v4/cloud/installation
|
||||
api.BaseRoutes.Cloud.Handle("/installation", api.APISessionRequired(getInstallation)).Methods(http.MethodGet)
|
||||
|
||||
// GET /api/v4/cloud/cws-health-check
|
||||
api.BaseRoutes.Cloud.Handle("/check-cws-connection", api.APIHandler(handleCheckCWSConnection)).Methods(http.MethodGet)
|
||||
|
||||
// GET /api/v4/cloud/preview/modal_data
|
||||
api.BaseRoutes.Cloud.Handle("/preview/modal_data", api.APISessionRequired(getPreviewModalData)).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
func ensureCloudInterface(c *Context, where string) bool {
|
||||
cloud := c.App.Cloud()
|
||||
disabled := c.App.Config().CloudSettings.Disable != nil && *c.App.Config().CloudSettings.Disable
|
||||
if cloud == nil {
|
||||
c.Err = model.NewAppError(where, "api.server.cws.needs_enterprise_edition", nil, "", http.StatusBadRequest)
|
||||
return false
|
||||
}
|
||||
if disabled {
|
||||
c.Err = model.NewAppError(where, "api.server.cws.disabled", nil, "", http.StatusUnprocessableEntity)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getPreviewSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
license := c.App.Channels().License()
|
||||
subscription := &model.Subscription{
|
||||
ID: "cloud-preview",
|
||||
ProductID: license.SkuName,
|
||||
StartAt: license.StartsAt,
|
||||
TrialEndAt: license.ExpiresAt,
|
||||
EndAt: license.ExpiresAt,
|
||||
IsFreeTrial: "true",
|
||||
IsCloudPreview: true,
|
||||
}
|
||||
|
||||
json, err := json.Marshal(subscription)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getSubscription", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := w.Write(json); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// Preview subscription is a special case for cloud preview licenses.
|
||||
if c.App.Channels().License().IsCloudPreview() {
|
||||
getPreviewSubscription(c, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
ensured := ensureCloudInterface(c, "Api4.getSubscription")
|
||||
if !ensured {
|
||||
return
|
||||
}
|
||||
|
||||
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, "", http.StatusInternalServerError).Wrap(err)
|
||||
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,
|
||||
EndAt: subscription.EndAt,
|
||||
CancelAt: subscription.CancelAt,
|
||||
DelinquentSince: subscription.DelinquentSince,
|
||||
CustomerID: "",
|
||||
AddOns: []string{},
|
||||
StartAt: 0,
|
||||
CreateAt: 0,
|
||||
Seats: 0,
|
||||
Status: "",
|
||||
DNS: "",
|
||||
LastInvoice: &model.Invoice{},
|
||||
BillingType: "",
|
||||
}
|
||||
}
|
||||
|
||||
if model.GetServiceEnvironment() != model.ServiceEnvironmentTest {
|
||||
subscription.SimulatedCurrentTimeMs = nil
|
||||
}
|
||||
|
||||
if !c.App.Config().FeatureFlags.CloudAnnualRenewals {
|
||||
subscription.WillRenew = ""
|
||||
subscription.CancelAt = nil
|
||||
}
|
||||
|
||||
json, err := json.Marshal(subscription)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getSubscription", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := w.Write(json); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func validateBusinessEmail(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
ensured := ensureCloudInterface(c, "Api4.validateBusinessEmail")
|
||||
if !ensured {
|
||||
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 || emailToValidate == 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) {
|
||||
ensured := ensureCloudInterface(c, "Api4.validateWorkspaceBusinessEmail")
|
||||
if !ensured {
|
||||
return
|
||||
}
|
||||
|
||||
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, "", http.StatusBadRequest).Wrap(err)
|
||||
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, "", http.StatusForbidden).Wrap(errValidatingAdminEmail)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// if any of the emails is valid, return ok
|
||||
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 getCloudProducts(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
ensured := ensureCloudInterface(c, "Api4.getCloudProducts")
|
||||
if !ensured {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if _, err := w.Write(byteSanitizedProductsData); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := w.Write(byteProductsData); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getCloudLimits(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
ensured := ensureCloudInterface(c, "Api4.getCloudLimits")
|
||||
if !ensured {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if _, err := w.Write(json); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
ensured := ensureCloudInterface(c, "Api4.getCloudCustomer")
|
||||
if !ensured {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if _, err := w.Write(json); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getInstallation(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
ensured := ensureCloudInterface(c, "Api4.getInstallation")
|
||||
if !ensured {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadIPFilters) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadIPFilters)
|
||||
return
|
||||
}
|
||||
|
||||
installation, err := c.App.Cloud().GetInstallation(c.AppContext.Session().UserId)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getInstallation", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(installation); err != nil {
|
||||
c.Err = model.NewAppError("Api4.getInstallation", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func updateCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
ensured := ensureCloudInterface(c, "Api4.updateCloudCustomer")
|
||||
if !ensured {
|
||||
return
|
||||
}
|
||||
|
||||
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 || customerInfo == 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
|
||||
}
|
||||
|
||||
if _, err := w.Write(json); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func updateCloudCustomerAddress(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
ensured := ensureCloudInterface(c, "Api4.updateCloudCustomerAddress")
|
||||
if !ensured {
|
||||
return
|
||||
}
|
||||
|
||||
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 || address == 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
|
||||
}
|
||||
|
||||
if _, err := w.Write(json); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getInvoicesForSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
ensured := ensureCloudInterface(c, "Api4.getInvoicesForSubscription")
|
||||
if !ensured {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if _, err := w.Write(json); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getSubscriptionInvoicePDF(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
ensured := ensureCloudInterface(c, "Api4.getSubscriptionInvoicePDF")
|
||||
if !ensured {
|
||||
return
|
||||
}
|
||||
|
||||
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, "", http.StatusInternalServerError).Wrap(appErr)
|
||||
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) {
|
||||
ensured := ensureCloudInterface(c, "Api4.handleCWSWebhook")
|
||||
if !ensured {
|
||||
return
|
||||
}
|
||||
|
||||
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, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
var event *model.CWSWebhookPayload
|
||||
if err = json.Unmarshal(bodyBytes, &event); err != nil || event == nil {
|
||||
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
switch event.Event {
|
||||
case model.EventTypeSendAdminWelcomeEmail:
|
||||
user, appErr := c.App.GetUserByUsername(event.CloudWorkspaceOwner.UserName)
|
||||
if appErr != nil {
|
||||
c.Err = model.NewAppError("Api4.handleCWSWebhook", appErr.Id, nil, "", appErr.StatusCode).Wrap(appErr)
|
||||
return
|
||||
}
|
||||
|
||||
teams, appErr := c.App.GetAllTeams()
|
||||
if appErr != nil {
|
||||
c.Err = model.NewAppError("Api4.handleCWSWebhook", appErr.Id, nil, "", appErr.StatusCode).Wrap(appErr)
|
||||
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, "", http.StatusInternalServerError).Wrap(err)
|
||||
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, "", http.StatusInternalServerError).Wrap(err)
|
||||
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) {
|
||||
ensured := ensureCloudInterface(c, "Api4.handleCheckCWSConnection")
|
||||
if !ensured {
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.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 getPreviewModalData(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
modalData, err := c.App.GetPreviewModalData()
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
responseData, jsonErr := json.Marshal(modalData)
|
||||
if jsonErr != nil {
|
||||
c.Err = model.NewAppError("Api4.getPreviewModalData", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
if _, writeErr := w.Write(responseData); writeErr != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(writeErr))
|
||||
}
|
||||
}
|
||||
442
channels/api4/cloud_test.go
Normal file
442
channels/api4/cloud_test.go
Normal file
@ -0,0 +1,442 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
|
||||
)
|
||||
|
||||
func TestGetSubscription(t *testing.T) {
|
||||
mainHelper.Parallel(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: 2000000000,
|
||||
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) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
_, _, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
|
||||
require.NoError(t, err)
|
||||
|
||||
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(context.Background())
|
||||
|
||||
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) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
_, _, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
|
||||
require.NoError(t, err)
|
||||
|
||||
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(context.Background())
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, subscriptionReturned, subscription)
|
||||
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateBusinessEmail(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
t.Run("Returns forbidden for invalid business email", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
_, _, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
|
||||
require.NoError(t, err)
|
||||
|
||||
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(context.Background(), &validBusinessEmail)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusForbidden, res.StatusCode, "403")
|
||||
})
|
||||
|
||||
t.Run("Validate business email for admin", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
_, _, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
|
||||
require.NoError(t, err)
|
||||
|
||||
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(context.Background(), &validBusinessEmail)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, "200")
|
||||
})
|
||||
|
||||
t.Run("Empty body returns bad request", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
_, _, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
|
||||
require.NoError(t, err)
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
r, err := th.SystemAdminClient.DoAPIPost(context.Background(), "/cloud/validate-business-email", "")
|
||||
require.Error(t, err)
|
||||
closeBody(r)
|
||||
require.Equal(t, http.StatusBadRequest, r.StatusCode, "Status Bad Request")
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateWorkspaceBusinessEmail(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
t.Run("validate the Cloud Customer has used a valid email to create the workspace", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
_, _, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
|
||||
require.NoError(t, err)
|
||||
|
||||
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(context.Background())
|
||||
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) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
_, _, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
|
||||
require.NoError(t, err)
|
||||
|
||||
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(context.Background())
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Error while grabbing the cloud customer returns bad request", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
_, _, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
|
||||
require.NoError(t, err)
|
||||
|
||||
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.DoAPIPost(context.Background(), "/cloud/validate-workspace-business-email", "")
|
||||
require.Error(t, err)
|
||||
closeBody(r)
|
||||
require.Equal(t, http.StatusBadRequest, r.StatusCode, "Status Bad Request")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetCloudProducts(t *testing.T) {
|
||||
mainHelper.Parallel(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) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
_, _, err := th.Client.Login(context.Background(), th.SystemAdminUser.Email, th.SystemAdminUser.Password)
|
||||
require.NoError(t, err)
|
||||
|
||||
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(context.Background())
|
||||
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) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
_, _, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
|
||||
require.NoError(t, err)
|
||||
|
||||
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(context.Background())
|
||||
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")
|
||||
})
|
||||
}
|
||||
38
channels/api4/cluster.go
Normal file
38
channels/api4/cluster.go
Normal file
@ -0,0 +1,38 @@
|
||||
// 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/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitCluster() {
|
||||
api.BaseRoutes.Cluster.Handle("/status", api.APISessionRequired(getClusterStatus)).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
func getClusterStatus(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionToAndNotRestrictedAdmin(*c.AppContext.Session(), model.PermissionSysconsoleReadEnvironmentHighAvailability) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadEnvironmentHighAvailability)
|
||||
return
|
||||
}
|
||||
|
||||
infos, err := c.App.GetClusterStatus(c.AppContext)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getClusterStatus", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(infos)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getClusterStatus", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
if _, err := w.Write(js); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
40
channels/api4/cluster_test.go
Normal file
40
channels/api4/cluster_test.go
Normal file
@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func TestGetClusterStatus(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("as system user", func(t *testing.T) {
|
||||
_, resp, err := th.Client.GetClusterStatus(context.Background())
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("as system admin", func(t *testing.T) {
|
||||
infos, _, err := th.SystemAdminClient.GetClusterStatus(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, infos, "cluster status should not be nil")
|
||||
})
|
||||
|
||||
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.GetClusterStatus(context.Background())
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
}
|
||||
553
channels/api4/command.go
Normal file
553
channels/api4/command.go
Normal file
@ -0,0 +1,553 @@
|
||||
// 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/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitCommand() {
|
||||
api.BaseRoutes.Commands.Handle("", api.APISessionRequired(createCommand)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Commands.Handle("", api.APISessionRequired(listCommands)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Commands.Handle("/execute", api.APISessionRequired(executeCommand)).Methods(http.MethodPost)
|
||||
|
||||
api.BaseRoutes.Command.Handle("", api.APISessionRequired(getCommand)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Command.Handle("", api.APISessionRequired(updateCommand)).Methods(http.MethodPut)
|
||||
api.BaseRoutes.Command.Handle("/move", api.APISessionRequired(moveCommand)).Methods(http.MethodPut)
|
||||
api.BaseRoutes.Command.Handle("", api.APISessionRequired(deleteCommand)).Methods(http.MethodDelete)
|
||||
|
||||
api.BaseRoutes.Team.Handle("/commands/autocomplete", api.APISessionRequired(listAutocompleteCommands)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Team.Handle("/commands/autocomplete_suggestions", api.APISessionRequired(listCommandAutocompleteSuggestions)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Command.Handle("/regen_token", api.APISessionRequired(regenCommandToken)).Methods(http.MethodPut)
|
||||
}
|
||||
|
||||
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(model.AuditEventCreateCommand, model.AuditStatusFail)
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "command", &cmd)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageOwnSlashCommands) {
|
||||
c.SetPermissionError(model.PermissionManageOwnSlashCommands)
|
||||
return
|
||||
}
|
||||
|
||||
userId := c.AppContext.Session().UserId
|
||||
if cmd.CreatorId != "" && cmd.CreatorId != userId {
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageOthersSlashCommands) {
|
||||
c.LogAudit("fail - inappropriate permissions")
|
||||
c.SetPermissionError(model.PermissionManageOthersSlashCommands)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := c.App.GetUser(cmd.CreatorId); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
userId = cmd.CreatorId
|
||||
}
|
||||
|
||||
cmd.CreatorId = 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(model.AuditEventUpdateCommand, model.AuditStatusFail)
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "command", &cmd)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
oldCmd, err := c.App.GetCommand(c.Params.CommandId)
|
||||
if err != nil {
|
||||
model.AddEventParameterToAuditRec(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.PermissionManageOwnSlashCommands) {
|
||||
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(model.AuditEventMoveCommand, model.AuditStatusFail)
|
||||
model.AddEventParameterToAuditRec(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
|
||||
}
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "team", newTeam)
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), newTeam.Id, model.PermissionManageOwnSlashCommands) {
|
||||
c.LogAudit("fail - inappropriate permissions")
|
||||
c.SetPermissionError(model.PermissionManageOwnSlashCommands)
|
||||
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.PermissionManageOwnSlashCommands) {
|
||||
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
|
||||
}
|
||||
|
||||
// Verify that the command creator has permission to the new team
|
||||
// This prevents moving a command to a team where its creator doesn't have access
|
||||
if !c.App.HasPermissionToTeam(c.AppContext, cmd.CreatorId, newTeam.Id, model.PermissionManageOwnSlashCommands) {
|
||||
c.LogAudit("fail - command creator does not have permission to new team")
|
||||
c.Err = model.NewAppError("moveCommand", "api.command.move_command.creator_no_permission.app_error", nil, "creator_id="+cmd.CreatorId+" team_id="+newTeam.Id, http.StatusBadRequest)
|
||||
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(model.AuditEventDeleteCommand, model.AuditStatusFail)
|
||||
model.AddEventParameterToAuditRec(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.PermissionManageOwnSlashCommands) {
|
||||
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.PermissionManageOwnSlashCommands) {
|
||||
c.SetPermissionError(model.PermissionManageOwnSlashCommands)
|
||||
return
|
||||
}
|
||||
|
||||
// Filter to only commands the user can manage
|
||||
userIdFilter := c.AppContext.Session().UserId
|
||||
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageOthersSlashCommands) {
|
||||
userIdFilter = "" // Empty means return all commands
|
||||
}
|
||||
|
||||
commands, err = c.App.ListTeamCommandsByUser(teamId, userIdFilter)
|
||||
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.PermissionManageOwnSlashCommands) {
|
||||
commands, err = c.App.ListAutocompleteCommands(teamId, c.AppContext.T)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Filter custom commands to only those the user can manage
|
||||
userIdFilter := c.AppContext.Session().UserId
|
||||
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageOthersSlashCommands) {
|
||||
userIdFilter = "" // Empty means return all commands
|
||||
}
|
||||
|
||||
commands, err = c.App.ListAllCommandsByUser(teamId, userIdFilter, 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.PermissionManageOwnSlashCommands) {
|
||||
// again, return not_found to ensure id existence does not leak.
|
||||
c.SetCommandNotFoundError()
|
||||
return
|
||||
}
|
||||
|
||||
if c.AppContext.Session().UserId != cmd.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageOthersSlashCommands) {
|
||||
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(model.AuditEventExecuteCommand, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "command_args", &commandArgs)
|
||||
|
||||
// Checks that user is a member of the specified channel, and that they have permission to create a post in it.
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), commandArgs.ChannelId, model.PermissionCreatePost) {
|
||||
c.SetPermissionError(model.PermissionCreatePost)
|
||||
return
|
||||
}
|
||||
|
||||
channel, err := c.App.GetChannel(c.AppContext, commandArgs.ChannelId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if channel.DeleteAt != 0 {
|
||||
c.Err = model.NewAppError("createPost", "api.command.execute_command.deleted.error", nil, "", http.StatusBadRequest)
|
||||
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 {
|
||||
restrictDM, appErr := c.App.CheckIfChannelIsRestrictedDM(c.AppContext, channel)
|
||||
if appErr != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if restrictDM {
|
||||
c.Err = model.NewAppError("createPost", "api.command.execute_command.restricted_dm.error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 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.PermissionCreatePost) {
|
||||
c.SetPermissionError(model.PermissionCreatePost)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commandArgs.UserId = c.AppContext.Session().UserId
|
||||
commandArgs.T = c.AppContext.T
|
||||
commandArgs.SiteURL = c.GetSiteURLHeader()
|
||||
|
||||
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,
|
||||
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
|
||||
}
|
||||
if _, err := w.Write(js); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func regenCommandToken(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireCommandId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventRegenCommandToken, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
cmd, err := c.App.GetCommand(c.Params.CommandId)
|
||||
if err != nil {
|
||||
model.AddEventParameterToAuditRec(auditRec, "command_id", c.Params.CommandId)
|
||||
c.SetCommandNotFoundError()
|
||||
return
|
||||
}
|
||||
auditRec.AddEventPriorState(cmd)
|
||||
model.AddEventParameterToAuditRec(auditRec, "command_id", c.Params.CommandId)
|
||||
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageOwnSlashCommands) {
|
||||
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
|
||||
|
||||
if _, err := w.Write([]byte(model.MapToJSON(resp))); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
40
channels/api4/command_help_test.go
Normal file
40
channels/api4/command_help_test.go
Normal file
@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func TestHelpCommand(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
client := th.Client
|
||||
channel := th.BasicChannel
|
||||
|
||||
HelpLink := *th.App.Config().SupportSettings.HelpLink
|
||||
defer func() {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.SupportSettings.HelpLink = HelpLink })
|
||||
}()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.SupportSettings.HelpLink = "" })
|
||||
rs1, _, err := client.ExecuteCommand(context.Background(), channel.Id, "/help ")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, rs1.Text, model.SupportSettingsDefaultHelpLink, "failed to default help link")
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.SupportSettings.HelpLink = "https://docs.mattermost.com/guides/user.html"
|
||||
})
|
||||
rs2, _, err := client.ExecuteCommand(context.Background(), channel.Id, "/help ")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, rs2.Text, "https://docs.mattermost.com/guides/user.html", "failed to help link")
|
||||
}
|
||||
51
channels/api4/command_local.go
Normal file
51
channels/api4/command_local.go
Normal file
@ -0,0 +1,51 @@
|
||||
// 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/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitCommandLocal() {
|
||||
api.BaseRoutes.Commands.Handle("", api.APILocal(localCreateCommand)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Commands.Handle("", api.APILocal(listCommands)).Methods(http.MethodGet)
|
||||
|
||||
api.BaseRoutes.Command.Handle("", api.APILocal(getCommand)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Command.Handle("", api.APILocal(updateCommand)).Methods(http.MethodPut)
|
||||
api.BaseRoutes.Command.Handle("/move", api.APILocal(moveCommand)).Methods(http.MethodPut)
|
||||
api.BaseRoutes.Command.Handle("", api.APILocal(deleteCommand)).Methods(http.MethodDelete)
|
||||
}
|
||||
|
||||
func localCreateCommand(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(model.AuditEventLocalCreateCommand, model.AuditStatusFail)
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "command", &cmd)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
1862
channels/api4/command_test.go
Normal file
1862
channels/api4/command_test.go
Normal file
File diff suppressed because it is too large
Load Diff
495
channels/api4/commands_test.go
Normal file
495
channels/api4/commands_test.go
Normal file
@ -0,0 +1,495 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
_ "github.com/mattermost/mattermost/server/v8/channels/app/slashcommands"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func TestEchoCommand(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
client := th.Client
|
||||
channel1 := th.BasicChannel
|
||||
|
||||
echoTestString := "/echo test"
|
||||
|
||||
r1, _, err := client.ExecuteCommand(context.Background(), channel1.Id, echoTestString)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, r1, "Echo command failed to execute")
|
||||
|
||||
r1, _, err = client.ExecuteCommand(context.Background(), channel1.Id, "/echo ")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, r1, "Echo command failed to execute")
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
p1, _, err := client.GetPostsForChannel(context.Background(), channel1.Id, 0, 2, "", false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, p1.Order, 2, "Echo command failed to send")
|
||||
}
|
||||
|
||||
func TestGroupmsgCommands(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
client := th.Client
|
||||
team := th.BasicTeam
|
||||
user1 := th.BasicUser
|
||||
user2 := th.BasicUser2
|
||||
user3 := th.CreateUser()
|
||||
user4 := th.CreateUser()
|
||||
user5 := th.CreateUser()
|
||||
user6 := th.CreateUser()
|
||||
user7 := th.CreateUser()
|
||||
user8 := th.CreateUser()
|
||||
user9 := th.CreateUser()
|
||||
th.LinkUserToTeam(user3, team)
|
||||
th.LinkUserToTeam(user4, team)
|
||||
|
||||
rs1, _, err := client.ExecuteCommand(context.Background(), th.BasicChannel.Id, "/groupmsg "+user2.Username+","+user3.Username)
|
||||
require.NoError(t, err)
|
||||
|
||||
group1 := model.GetGroupNameFromUserIds([]string{user1.Id, user2.Id, user3.Id})
|
||||
require.True(t, strings.HasSuffix(rs1.GotoLocation, "/"+team.Name+"/channels/"+group1), "failed to create group channel")
|
||||
|
||||
rs2, _, err := client.ExecuteCommand(context.Background(), th.BasicChannel.Id, "/groupmsg "+user3.Username+","+user4.Username+" foobar")
|
||||
require.NoError(t, err)
|
||||
group2 := model.GetGroupNameFromUserIds([]string{user1.Id, user3.Id, user4.Id})
|
||||
|
||||
require.True(t, strings.HasSuffix(rs2.GotoLocation, "/"+team.Name+"/channels/"+group2), "failed to create second direct channel")
|
||||
|
||||
result, _, err := client.SearchPosts(context.Background(), team.Id, "foobar", false)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, 0, len(result.Order), "post did not get sent to direct message")
|
||||
|
||||
rs3, _, err := client.ExecuteCommand(context.Background(), th.BasicChannel.Id, "/groupmsg "+user2.Username+","+user3.Username)
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.HasSuffix(rs3.GotoLocation, "/"+team.Name+"/channels/"+group1), "failed to go back to existing group channel")
|
||||
|
||||
_, _, err = client.ExecuteCommand(context.Background(), th.BasicChannel.Id, "/groupmsg "+user2.Username+" foobar")
|
||||
require.NoError(t, err)
|
||||
_, _, err = client.ExecuteCommand(context.Background(), th.BasicChannel.Id, "/groupmsg "+user2.Username+","+user3.Username+","+user4.Username+","+user5.Username+","+user6.Username+","+user7.Username+","+user8.Username+","+user9.Username+" foobar")
|
||||
require.NoError(t, err)
|
||||
_, _, err = client.ExecuteCommand(context.Background(), th.BasicChannel.Id, "/groupmsg junk foobar")
|
||||
require.NoError(t, err)
|
||||
_, _, err = client.ExecuteCommand(context.Background(), th.BasicChannel.Id, "/groupmsg junk,junk2 foobar")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestInvitePeopleCommand(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
client := th.Client
|
||||
channel := th.BasicChannel
|
||||
|
||||
r1, _, err := client.ExecuteCommand(context.Background(), channel.Id, "/invite_people test@example.com")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, r1, "Command failed to execute")
|
||||
|
||||
r2, _, err := client.ExecuteCommand(context.Background(), channel.Id, "/invite_people test1@example.com test2@example.com")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, r2, "Command failed to execute")
|
||||
|
||||
r3, _, err := client.ExecuteCommand(context.Background(), channel.Id, "/invite_people")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, r3, "Command failed to execute")
|
||||
}
|
||||
|
||||
// also used to test /open (see command_open_test.go)
|
||||
func testJoinCommands(t *testing.T, alias string) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
client := th.Client
|
||||
team := th.BasicTeam
|
||||
user2 := th.BasicUser2
|
||||
|
||||
channel0 := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.ChannelTypeOpen, TeamId: team.Id}
|
||||
channel0, _, err := client.CreateChannel(context.Background(), channel0)
|
||||
require.NoError(t, err)
|
||||
|
||||
channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.ChannelTypeOpen, TeamId: team.Id}
|
||||
channel1, _, err = client.CreateChannel(context.Background(), channel1)
|
||||
require.NoError(t, err)
|
||||
_, err = client.RemoveUserFromChannel(context.Background(), channel1.Id, th.BasicUser.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
channel2 := &model.Channel{DisplayName: "BB", Name: "bb" + model.NewId() + "a", Type: model.ChannelTypeOpen, TeamId: team.Id}
|
||||
channel2, _, err = client.CreateChannel(context.Background(), channel2)
|
||||
require.NoError(t, err)
|
||||
_, err = client.RemoveUserFromChannel(context.Background(), channel2.Id, th.BasicUser.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
channel3, _, err := client.CreateDirectChannel(context.Background(), th.BasicUser.Id, user2.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
rs5, _, err := client.ExecuteCommand(context.Background(), channel0.Id, "/"+alias+" "+channel2.Name)
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.HasSuffix(rs5.GotoLocation, "/"+team.Name+"/channels/"+channel2.Name), "failed to join channel")
|
||||
|
||||
rs6, _, err := client.ExecuteCommand(context.Background(), channel0.Id, "/"+alias+" "+channel3.Name)
|
||||
require.NoError(t, err)
|
||||
require.False(t, strings.HasSuffix(rs6.GotoLocation, "/"+team.Name+"/channels/"+channel3.Name), "should not have joined direct message channel")
|
||||
|
||||
c1, _, err := client.GetChannelsForTeamForUser(context.Background(), th.BasicTeam.Id, th.BasicUser.Id, false, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
found := false
|
||||
for _, c := range c1 {
|
||||
if c.Id == channel2.Id {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
require.True(t, found, "did not join channel")
|
||||
|
||||
// test case insensitively
|
||||
channel4 := &model.Channel{DisplayName: "BB", Name: "bb" + model.NewId() + "a", Type: model.ChannelTypeOpen, TeamId: team.Id}
|
||||
channel4, _, err = client.CreateChannel(context.Background(), channel4)
|
||||
require.NoError(t, err)
|
||||
_, err = client.RemoveUserFromChannel(context.Background(), channel4.Id, th.BasicUser.Id)
|
||||
require.NoError(t, err)
|
||||
rs7, _, err := client.ExecuteCommand(context.Background(), channel0.Id, "/"+alias+" "+strings.ToUpper(channel4.Name))
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.HasSuffix(rs7.GotoLocation, "/"+team.Name+"/channels/"+channel4.Name), "failed to join channel")
|
||||
}
|
||||
|
||||
func TestJoinCommands(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
testJoinCommands(t, "join")
|
||||
}
|
||||
|
||||
func TestLoadTestHelpCommands(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
client := th.Client
|
||||
channel := th.BasicChannel
|
||||
|
||||
enableTesting := *th.App.Config().ServiceSettings.EnableTesting
|
||||
defer func() {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableTesting = enableTesting })
|
||||
}()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableTesting = true })
|
||||
|
||||
rs, _, err := client.ExecuteCommand(context.Background(), channel.Id, "/test help")
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.Contains(rs.Text, "Mattermost testing commands to help"), rs.Text)
|
||||
}
|
||||
|
||||
func TestLoadTestSetupCommands(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
client := th.Client
|
||||
channel := th.BasicChannel
|
||||
|
||||
enableTesting := *th.App.Config().ServiceSettings.EnableTesting
|
||||
defer func() {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableTesting = enableTesting })
|
||||
}()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableTesting = true })
|
||||
|
||||
rs, _, err := client.ExecuteCommand(context.Background(), channel.Id, "/test setup fuzz 1 1 1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Created environment", rs.Text, rs.Text)
|
||||
}
|
||||
|
||||
func TestLoadTestUsersCommands(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
client := th.Client
|
||||
channel := th.BasicChannel
|
||||
|
||||
enableTesting := *th.App.Config().ServiceSettings.EnableTesting
|
||||
defer func() {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableTesting = enableTesting })
|
||||
}()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableTesting = true })
|
||||
|
||||
rs, _, err := client.ExecuteCommand(context.Background(), channel.Id, "/test users fuzz 1 2")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Added users", rs.Text, rs.Text)
|
||||
}
|
||||
|
||||
func TestLoadTestChannelsCommands(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
client := th.Client
|
||||
channel := th.BasicChannel
|
||||
|
||||
enableTesting := *th.App.Config().ServiceSettings.EnableTesting
|
||||
defer func() {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableTesting = enableTesting })
|
||||
}()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableTesting = true })
|
||||
|
||||
rs, _, err := client.ExecuteCommand(context.Background(), channel.Id, "/test channels fuzz 1 2")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Added channels", rs.Text, rs.Text)
|
||||
}
|
||||
|
||||
func TestLoadTestPostsCommands(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
client := th.Client
|
||||
channel := th.BasicChannel
|
||||
|
||||
enableTesting := *th.App.Config().ServiceSettings.EnableTesting
|
||||
defer func() {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableTesting = enableTesting })
|
||||
}()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableTesting = true })
|
||||
|
||||
rs, _, err := client.ExecuteCommand(context.Background(), channel.Id, "/test posts fuzz 2 3 2")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Added posts", rs.Text, rs.Text)
|
||||
}
|
||||
|
||||
func TestLeaveCommands(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
client := th.Client
|
||||
team := th.BasicTeam
|
||||
user2 := th.BasicUser2
|
||||
|
||||
channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.ChannelTypeOpen, TeamId: team.Id}
|
||||
channel1, _, err := client.CreateChannel(context.Background(), channel1)
|
||||
require.NoError(t, err)
|
||||
_, _, err = client.AddChannelMember(context.Background(), channel1.Id, th.BasicUser.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
channel2 := &model.Channel{DisplayName: "BB", Name: "bb" + model.NewId() + "a", Type: model.ChannelTypePrivate, TeamId: team.Id}
|
||||
channel2, _, err = client.CreateChannel(context.Background(), channel2)
|
||||
require.NoError(t, err)
|
||||
_, _, err = client.AddChannelMember(context.Background(), channel2.Id, th.BasicUser.Id)
|
||||
require.NoError(t, err)
|
||||
_, _, err = client.AddChannelMember(context.Background(), channel2.Id, user2.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
channel3, _, err := client.CreateDirectChannel(context.Background(), th.BasicUser.Id, user2.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
rs1, _, err := client.ExecuteCommand(context.Background(), channel1.Id, "/leave")
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.HasSuffix(rs1.GotoLocation, "/"+team.Name+"/channels/"+model.DefaultChannelName), "failed to leave open channel 1")
|
||||
|
||||
rs2, _, err := client.ExecuteCommand(context.Background(), channel2.Id, "/leave")
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.HasSuffix(rs2.GotoLocation, "/"+team.Name+"/channels/"+model.DefaultChannelName), "failed to leave private channel 1")
|
||||
|
||||
_, _, err = client.ExecuteCommand(context.Background(), channel3.Id, "/leave")
|
||||
require.Error(t, err)
|
||||
|
||||
cdata, _, err := client.GetChannelsForTeamForUser(context.Background(), th.BasicTeam.Id, th.BasicUser.Id, false, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
found := false
|
||||
for _, c := range cdata {
|
||||
if c.Id == channel1.Id || c.Id == channel2.Id {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
require.False(t, found, "did not leave right channels")
|
||||
|
||||
for _, c := range cdata {
|
||||
if c.Name == model.DefaultChannelName {
|
||||
_, err := client.RemoveUserFromChannel(context.Background(), c.Id, th.BasicUser.Id)
|
||||
require.Error(t, err, "should have errored on leaving default channel")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogoutTestCommand(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
_, _, err := th.Client.ExecuteCommand(context.Background(), th.BasicChannel.Id, "/logout")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMeCommand(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
client := th.Client
|
||||
channel := th.BasicChannel
|
||||
|
||||
testString := "/me hello"
|
||||
|
||||
r1, _, err := client.ExecuteCommand(context.Background(), channel.Id, testString)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, r1, "Command failed to execute")
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
p1, _, err := client.GetPostsForChannel(context.Background(), channel.Id, 0, 2, "", false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, p1.Order, 2, "Command failed to send")
|
||||
|
||||
pt := p1.Posts[p1.Order[0]].Type
|
||||
require.Equal(t, model.PostTypeMe, pt, "invalid post type")
|
||||
|
||||
msg := p1.Posts[p1.Order[0]].Message
|
||||
want := "*hello*"
|
||||
require.Equal(t, want, msg, "invalid me response")
|
||||
}
|
||||
|
||||
func TestMsgCommands(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
client := th.Client
|
||||
team := th.BasicTeam
|
||||
user1 := th.BasicUser
|
||||
user2 := th.BasicUser2
|
||||
user3 := th.CreateUser()
|
||||
th.LinkUserToTeam(user3, team)
|
||||
|
||||
_, _, err := client.CreateDirectChannel(context.Background(), th.BasicUser.Id, user2.Id)
|
||||
require.NoError(t, err)
|
||||
_, _, err = client.CreateDirectChannel(context.Background(), th.BasicUser.Id, user3.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
rs1, _, err := client.ExecuteCommand(context.Background(), th.BasicChannel.Id, "/msg "+user2.Username)
|
||||
require.NoError(t, err)
|
||||
require.Condition(t, func() bool {
|
||||
return strings.HasSuffix(rs1.GotoLocation, "/"+team.Name+"/channels/"+user1.Id+"__"+user2.Id) ||
|
||||
strings.HasSuffix(rs1.GotoLocation, "/"+team.Name+"/channels/"+user2.Id+"__"+user1.Id)
|
||||
}, "failed to create direct channel")
|
||||
|
||||
rs2, _, err := client.ExecuteCommand(context.Background(), th.BasicChannel.Id, "/msg "+user3.Username+" foobar")
|
||||
require.NoError(t, err)
|
||||
require.Condition(t, func() bool {
|
||||
return strings.HasSuffix(rs2.GotoLocation, "/"+team.Name+"/channels/"+user1.Id+"__"+user3.Id) ||
|
||||
strings.HasSuffix(rs2.GotoLocation, "/"+team.Name+"/channels/"+user3.Id+"__"+user1.Id)
|
||||
}, "failed to create second direct channel")
|
||||
|
||||
result, _, err := client.SearchPosts(context.Background(), th.BasicTeam.Id, "foobar", false)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, 0, len(result.Order), "post did not get sent to direct message")
|
||||
|
||||
rs3, _, err := client.ExecuteCommand(context.Background(), th.BasicChannel.Id, "/msg "+user2.Username)
|
||||
require.NoError(t, err)
|
||||
require.Condition(t, func() bool {
|
||||
return strings.HasSuffix(rs3.GotoLocation, "/"+team.Name+"/channels/"+user1.Id+"__"+user2.Id) ||
|
||||
strings.HasSuffix(rs3.GotoLocation, "/"+team.Name+"/channels/"+user2.Id+"__"+user1.Id)
|
||||
}, "failed to go back to existing direct channel")
|
||||
|
||||
_, _, err = client.ExecuteCommand(context.Background(), th.BasicChannel.Id, "/msg "+th.BasicUser.Username+" foobar")
|
||||
require.NoError(t, err)
|
||||
_, _, err = client.ExecuteCommand(context.Background(), th.BasicChannel.Id, "/msg junk foobar")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestOpenCommands(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
testJoinCommands(t, "open")
|
||||
}
|
||||
|
||||
func TestSearchCommand(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
_, _, err := th.Client.ExecuteCommand(context.Background(), th.BasicChannel.Id, "/search")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSettingsCommand(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
_, _, err := th.Client.ExecuteCommand(context.Background(), th.BasicChannel.Id, "/settings")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestShortcutsCommand(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
_, _, err := th.Client.ExecuteCommand(context.Background(), th.BasicChannel.Id, "/shortcuts")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestShrugCommand(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
client := th.Client
|
||||
channel := th.BasicChannel
|
||||
|
||||
testString := "/shrug"
|
||||
|
||||
r1, _, err := client.ExecuteCommand(context.Background(), channel.Id, testString)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, r1, "Command failed to execute")
|
||||
|
||||
time.Sleep(time.Second)
|
||||
p1, _, err := client.GetPostsForChannel(context.Background(), channel.Id, 0, 2, "", false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, p1.Order, 2, "Command failed to send")
|
||||
require.Equal(t, `¯\\\_(ツ)\_/¯`, p1.Posts[p1.Order[0]].Message, "invalid shrug response")
|
||||
}
|
||||
|
||||
func TestStatusCommands(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
commandAndTest(t, th, "away")
|
||||
commandAndTest(t, th, "offline")
|
||||
commandAndTest(t, th, "online")
|
||||
}
|
||||
|
||||
func commandAndTest(t *testing.T, th *TestHelper, status string) {
|
||||
client := th.Client
|
||||
channel := th.BasicChannel
|
||||
user := th.BasicUser
|
||||
|
||||
r1, _, err := client.ExecuteCommand(context.Background(), channel.Id, "/"+status)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, "Command failed to execute", r1)
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
rstatus, _, err := client.GetUserStatus(context.Background(), user.Id, "")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, status, rstatus.Status, "Error setting status")
|
||||
}
|
||||
163
channels/api4/compliance.go
Normal file
163
channels/api4/compliance.go
Normal file
@ -0,0 +1,163 @@
|
||||
// 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/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitCompliance() {
|
||||
api.BaseRoutes.Compliance.Handle("/reports", api.APISessionRequired(createComplianceReport)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Compliance.Handle("/reports", api.APISessionRequired(getComplianceReports)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Compliance.Handle("/reports/{report_id:[A-Za-z0-9]+}", api.APISessionRequired(getComplianceReport)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Compliance.Handle("/reports/{report_id:[A-Za-z0-9]+}/download", api.APISessionRequiredTrustRequester(downloadComplianceReport)).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
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(model.AuditEventCreateComplianceReport, model.AuditStatusFail)
|
||||
model.AddEventParameterAuditableToAuditRec(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(c.AppContext, &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(model.AuditEventGetComplianceReports, model.AuditStatusFail)
|
||||
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(model.AuditEventGetComplianceReport, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadComplianceExportJob) {
|
||||
c.SetPermissionError(model.PermissionReadComplianceExportJob)
|
||||
return
|
||||
}
|
||||
|
||||
model.AddEventParameterToAuditRec(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(model.AuditEventDownloadComplianceReport, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(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()
|
||||
|
||||
if _, err := w.Write(reportBytes); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
446
channels/api4/config.go
Normal file
446
channels/api4/config.go
Normal file
@ -0,0 +1,446 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/i18n"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils"
|
||||
"github.com/mattermost/mattermost/server/v8/config"
|
||||
)
|
||||
|
||||
var (
|
||||
writeFilter func(c *Context, structField reflect.StructField) bool
|
||||
readFilter func(c *Context, structField reflect.StructField) bool
|
||||
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(http.MethodGet)
|
||||
api.BaseRoutes.APIRoot.Handle("/config", api.APISessionRequired(updateConfig)).Methods(http.MethodPut)
|
||||
api.BaseRoutes.APIRoot.Handle("/config/patch", api.APISessionRequired(patchConfig)).Methods(http.MethodPut)
|
||||
api.BaseRoutes.APIRoot.Handle("/config/reload", api.APISessionRequired(configReload)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.APIRoot.Handle("/config/client", api.APIHandler(getClientConfig)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.APIRoot.Handle("/config/environment", api.APISessionRequired(getEnvironmentConfig)).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
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(model.AuditEventGetConfig, model.AuditStatusFail)
|
||||
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, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
filterMasked, _ := strconv.ParseBool(r.URL.Query().Get("remove_masked"))
|
||||
filterDefaults, _ := strconv.ParseBool(r.URL.Query().Get("remove_defaults"))
|
||||
auditRec.Success()
|
||||
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
|
||||
filterOpts := model.ConfigFilterOptions{
|
||||
GetConfigOptions: model.GetConfigOptions{
|
||||
RemoveDefaults: filterDefaults,
|
||||
RemoveMasked: filterMasked,
|
||||
},
|
||||
}
|
||||
if c.App.Channels().License().IsCloud() {
|
||||
filterOpts.TagFilters = append(filterOpts.TagFilters, model.FilterTag{
|
||||
TagType: model.ConfigAccessTagType,
|
||||
TagName: model.ConfigAccessTagCloudRestrictable,
|
||||
})
|
||||
}
|
||||
m, err := model.FilterConfig(cfg, filterOpts)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getConfig", "api.filter_config_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(m); 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(model.AuditEventConfigReload, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if !c.App.SessionHasPermissionToAndNotRestrictedAdmin(*c.AppContext.Session(), model.PermissionReloadConfig) {
|
||||
c.SetPermissionError(model.PermissionReloadConfig)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.ReloadConfig(); err != nil {
|
||||
c.Err = model.NewAppError("configReload", "api.config.reload_config.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
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(model.AuditEventUpdateConfig, model.AuditStatusFail)
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// if ES autocomplete was enabled, we need to make sure that index has been checked.
|
||||
// we need to stop enabling ES autocomplete otherwise.
|
||||
if !*appCfg.ElasticsearchSettings.EnableAutocomplete && *cfg.ElasticsearchSettings.EnableAutocomplete {
|
||||
if !c.App.SearchEngine().ElasticsearchEngine.IsAutocompletionEnabled() {
|
||||
c.Err = model.NewAppError("updateConfig", "api.config.update.elasticsearch.autocomplete_cannot_be_enabled_error", nil, "", http.StatusBadRequest)
|
||||
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)
|
||||
|
||||
c.App.SanitizedConfig(newCfg)
|
||||
|
||||
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
|
||||
}
|
||||
if _, err := w.Write(js); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
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) {
|
||||
var config map[string]string
|
||||
if c.AppContext.Session().UserId == "" {
|
||||
config = c.App.Srv().Platform().LimitedClientConfigWithComputed()
|
||||
} else {
|
||||
config = c.App.Srv().Platform().ClientConfigWithComputed()
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(config); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
if _, err := w.Write([]byte(model.StringInterfaceToJSON(envConfig))); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
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(model.AuditEventPatchConfig, model.AuditStatusFail)
|
||||
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)
|
||||
|
||||
c.App.SanitizedConfig(newCfg)
|
||||
|
||||
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
|
||||
}
|
||||
if _, err := w.Write(js); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
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 c.AppContext.Session().IsUnrestricted() {
|
||||
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 {
|
||||
c.Logger.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)
|
||||
}
|
||||
}
|
||||
196
channels/api4/config_local.go
Normal file
196
channels/api4/config_local.go
Normal file
@ -0,0 +1,196 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils"
|
||||
"github.com/mattermost/mattermost/server/v8/config"
|
||||
)
|
||||
|
||||
func (api *API) InitConfigLocal() {
|
||||
api.BaseRoutes.APIRoot.Handle("/config", api.APILocal(localGetConfig)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.APIRoot.Handle("/config", api.APILocal(localUpdateConfig)).Methods(http.MethodPut)
|
||||
api.BaseRoutes.APIRoot.Handle("/config/patch", api.APILocal(localPatchConfig)).Methods(http.MethodPut)
|
||||
api.BaseRoutes.APIRoot.Handle("/config/reload", api.APILocal(configReload)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.APIRoot.Handle("/config/migrate", api.APILocal(localMigrateConfig)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.APIRoot.Handle("/config/client", api.APILocal(localGetClientConfig)).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
func localGetConfig(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventLocalGetConfig, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
filterMasked, _ := strconv.ParseBool(r.URL.Query().Get("remove_masked"))
|
||||
filterDefaults, _ := strconv.ParseBool(r.URL.Query().Get("remove_defaults"))
|
||||
|
||||
filterOpts := model.ConfigFilterOptions{
|
||||
GetConfigOptions: model.GetConfigOptions{
|
||||
RemoveDefaults: filterDefaults,
|
||||
RemoveMasked: filterMasked,
|
||||
},
|
||||
}
|
||||
|
||||
m, err := model.FilterConfig(c.App.Config(), filterOpts)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getConfig", "api.filter_config_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
auditRec.Success()
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
if err := json.NewEncoder(w).Encode(m); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func localUpdateConfig(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(model.AuditEventLocalUpdateConfig, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
cfg.SetDefaults()
|
||||
|
||||
appCfg := c.App.Config()
|
||||
|
||||
c.App.HandleMessageExportConfig(cfg, appCfg)
|
||||
|
||||
appErr := cfg.IsValid()
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
oldCfg, newCfg, appErr := c.App.SaveConfig(cfg, true)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
diffs, diffErr := config.Diff(oldCfg, newCfg)
|
||||
if diffErr != nil {
|
||||
c.Err = model.NewAppError("updateConfig", "api.config.update_config.diff.app_error", nil, "", http.StatusInternalServerError).Wrap(diffErr)
|
||||
return
|
||||
}
|
||||
auditRec.AddEventPriorState(&diffs)
|
||||
|
||||
c.App.SanitizedConfig(newCfg)
|
||||
|
||||
auditRec.Success()
|
||||
c.LogAudit("updateConfig")
|
||||
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
if err := json.NewEncoder(w).Encode(newCfg); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func localPatchConfig(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(model.AuditEventLocalPatchConfig, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
appCfg := c.App.Config()
|
||||
filterFn := func(structField reflect.StructField, base, patch reflect.Value) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
if cfg.MessageExportSettings.EnableExport != nil {
|
||||
c.App.HandleMessageExportConfig(cfg, appCfg)
|
||||
}
|
||||
|
||||
updatedCfg, mergeErr := config.Merge(appCfg, cfg, &utils.MergeConfig{
|
||||
StructFieldFilter: filterFn,
|
||||
})
|
||||
|
||||
if mergeErr != nil {
|
||||
c.Err = model.NewAppError("patchConfig", "api.config.update_config.restricted_merge.app_error", nil, "", http.StatusInternalServerError).Wrap(mergeErr)
|
||||
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)
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
if err := json.NewEncoder(w).Encode(c.App.GetSanitizedConfig()); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func localMigrateConfig(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
props := model.StringInterfaceFromJSON(r.Body)
|
||||
from, ok := props["from"].(string)
|
||||
if !ok {
|
||||
c.SetInvalidParam("from")
|
||||
return
|
||||
}
|
||||
to, ok := props["to"].(string)
|
||||
if !ok {
|
||||
c.SetInvalidParam("to")
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventMigrateConfig, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
err := config.Migrate(from, to)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("migrateConfig", "api.config.migrate_config.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func localGetClientConfig(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventLocalGetClientConfig, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
_, err := w.Write([]byte(model.MapToJSON(c.App.Srv().Platform().ClientConfigWithComputed())))
|
||||
if err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
937
channels/api4/config_test.go
Normal file
937
channels/api4/config_test.go
Normal file
@ -0,0 +1,937 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app"
|
||||
"github.com/mattermost/mattermost/server/v8/config"
|
||||
)
|
||||
|
||||
func TestGetConfig(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
client := th.Client
|
||||
|
||||
_, resp, err := client.GetConfig(context.Background())
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
t.Run("Get config for system admin client", func(t *testing.T) {
|
||||
cfg, _, err := th.SystemAdminClient.GetConfig(context.Background())
|
||||
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")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Get config for local client", func(t *testing.T) {
|
||||
cfg, _, err := th.LocalClient.GetConfig(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotEqual(t, model.FakeSetting, *cfg.SqlSettings.DataSource)
|
||||
require.NotEqual(t, model.FakeSetting, *cfg.FileSettings.PublicLinkSalt)
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
_, _, err := th.Client.Login(context.Background(), th.BasicUser.Username, th.BasicUser.Password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 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(context.Background())
|
||||
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()
|
||||
|
||||
_, _, err := th.Client.Login(context.Background(), th.BasicUser.Username, th.BasicUser.Password)
|
||||
require.NoError(t, err)
|
||||
_, resp, _ := th.Client.GetConfig(context.Background())
|
||||
|
||||
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(context.Background())
|
||||
require.NoError(t, err)
|
||||
t.Run("Can read value with permission", func(t *testing.T) {
|
||||
assert.NotNil(t, cfg.FeatureFlags)
|
||||
assert.NotNil(t, cfg.ExperimentalSettings.RestrictSystemAdmin)
|
||||
})
|
||||
}
|
||||
|
||||
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(context.Background())
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||
_, err := client.ReloadConfig(context.Background())
|
||||
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(context.Background())
|
||||
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(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, resp, err := client.UpdateConfig(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), "/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.NewPointer(4)
|
||||
badcfg.PasswordSettings.MinimumLength = model.NewPointer(4)
|
||||
_, resp, err = client.UpdateConfig(context.Background(), badcfg)
|
||||
require.Error(t, err)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
CheckErrorID(t, err, "model.config.is_valid.password_length.app_error")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Ensure PluginSettings.EnableUploads settings are protected", func(t *testing.T) {
|
||||
t.Run("sysadmin", func(t *testing.T) {
|
||||
oldEnableUploads := *th.App.Config().PluginSettings.EnableUploads
|
||||
*cfg.PluginSettings.EnableUploads = !oldEnableUploads
|
||||
|
||||
cfg, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), 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 = th.SystemAdminClient.UpdateConfig(context.Background(), cfg)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, oldEnableUploads, *cfg.PluginSettings.EnableUploads)
|
||||
assert.Equal(t, oldEnableUploads, *th.App.Config().PluginSettings.EnableUploads)
|
||||
})
|
||||
|
||||
t.Run("local mode", func(t *testing.T) {
|
||||
oldEnableUploads := *th.App.Config().PluginSettings.EnableUploads
|
||||
*cfg.PluginSettings.EnableUploads = !oldEnableUploads
|
||||
|
||||
cfg, _, err = th.LocalClient.UpdateConfig(context.Background(), cfg)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, oldEnableUploads, *cfg.PluginSettings.EnableUploads)
|
||||
assert.NotEqual(t, oldEnableUploads, *th.App.Config().PluginSettings.EnableUploads)
|
||||
|
||||
cfg.PluginSettings.EnableUploads = nil
|
||||
cfg, _, err = th.LocalClient.UpdateConfig(context.Background(), 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) {
|
||||
t.Run("sysadmin", func(t *testing.T) {
|
||||
oldPublicKeys := th.App.Config().PluginSettings.SignaturePublicKeyFiles
|
||||
cfg.PluginSettings.SignaturePublicKeyFiles = append(cfg.PluginSettings.SignaturePublicKeyFiles, "new_signature")
|
||||
|
||||
cfg, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), 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 = th.SystemAdminClient.UpdateConfig(context.Background(), cfg)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, oldPublicKeys, cfg.PluginSettings.SignaturePublicKeyFiles)
|
||||
assert.Equal(t, oldPublicKeys, th.App.Config().PluginSettings.SignaturePublicKeyFiles)
|
||||
})
|
||||
|
||||
t.Run("local mode", func(t *testing.T) {
|
||||
oldPublicKeys := th.App.Config().PluginSettings.SignaturePublicKeyFiles
|
||||
cfg.PluginSettings.SignaturePublicKeyFiles = append(cfg.PluginSettings.SignaturePublicKeyFiles, "new_signature")
|
||||
|
||||
cfg, _, err = th.LocalClient.UpdateConfig(context.Background(), cfg)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, oldPublicKeys, cfg.PluginSettings.SignaturePublicKeyFiles)
|
||||
assert.NotEqual(t, oldPublicKeys, th.App.Config().PluginSettings.SignaturePublicKeyFiles)
|
||||
|
||||
cfg.PluginSettings.SignaturePublicKeyFiles = nil
|
||||
cfg, _, err = th.LocalClient.UpdateConfig(context.Background(), 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(context.Background(), 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(context.Background(), 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 func() {
|
||||
appErr := th.App.Srv().RemoveLicense()
|
||||
require.Nil(t, appErr)
|
||||
}()
|
||||
|
||||
cfg2 := th.App.Config().Clone()
|
||||
*cfg2.ComplianceSettings.Directory = "hellodir"
|
||||
|
||||
_, resp, err = th.SystemAdminClient.UpdateConfig(context.Background(), 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(context.Background(), cfg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, nonEmptyURL, *cfg.ServiceSettings.SiteURL)
|
||||
|
||||
// Check that the Site URL can't be cleared
|
||||
cfg.ServiceSettings.SiteURL = model.NewPointer("")
|
||||
cfg, resp, err = th.SystemAdminClient.UpdateConfig(context.Background(), 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(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, nonEmptyURL, *cfg.ServiceSettings.SiteURL)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetConfigWithoutManageSystemPermission(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
_, _, err := th.Client.Login(context.Background(), th.BasicUser.Username, th.BasicUser.Password)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("any sysconsole read permission provides config read access", func(t *testing.T) {
|
||||
// forbidden by default
|
||||
_, resp, err := th.Client.GetConfig(context.Background())
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
// add any sysconsole read permission
|
||||
th.AddPermissionToRole(model.SysconsoleReadPermissions[0].Id, model.SystemUserRoleId)
|
||||
_, _, err = th.Client.GetConfig(context.Background())
|
||||
// should be readable now
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateConfigWithoutManageSystemPermission(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
_, _, err := th.Client.Login(context.Background(), th.BasicUser.Username, th.BasicUser.Password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 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(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, resp, err := th.Client.UpdateConfig(context.Background(), 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(context.Background())
|
||||
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(context.Background(), cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// ensure the config setting was not updated
|
||||
cfg, _, err = th.SystemAdminClient.GetConfig(context.Background())
|
||||
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(context.Background())
|
||||
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(context.Background(), cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// ensure the config setting was updated
|
||||
cfg, _, err = th.Client.GetConfig(context.Background())
|
||||
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(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
*cfg.MessageExportSettings.EnableExport = true
|
||||
_, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), 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(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
*cfg.MessageExportSettings.EnableExport = false
|
||||
_, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), 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(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
*cfg.MessageExportSettings.EnableExport = true
|
||||
_, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), 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(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
*cfg.MessageExportSettings.EnableExport = false
|
||||
_, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), 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(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := originalCfg.Clone()
|
||||
*cfg.TeamSettings.SiteName = "MyFancyName" // Allowed
|
||||
*cfg.ServiceSettings.SiteURL = "http://example.com" // Ignored
|
||||
|
||||
returnedCfg, _, err := th.SystemAdminClient.UpdateConfig(context.Background(), 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(context.Background())
|
||||
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(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := originalCfg.Clone()
|
||||
*cfg.TeamSettings.SiteName = "MyFancyName" // Allowed
|
||||
*cfg.ServiceSettings.SiteURL = "http://example.com" // Ignored
|
||||
|
||||
returnedCfg, _, err := th.LocalClient.UpdateConfig(context.Background(), 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(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
timeoutVal := *cfg.ServiceSettings.ReadTimeout
|
||||
cfg.ServiceSettings.ReadTimeout = model.NewPointer(timeoutVal + 1)
|
||||
cfg, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), cfg)
|
||||
require.NoError(t, err)
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.ServiceSettings.ReadTimeout = model.NewPointer(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(context.Background())
|
||||
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(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, envConfig)
|
||||
})
|
||||
|
||||
t.Run("as regular user", func(t *testing.T) {
|
||||
client := th.Client
|
||||
|
||||
envConfig, _, err := client.GetEnvironmentConfig(context.Background())
|
||||
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(context.Background())
|
||||
require.Error(t, err)
|
||||
CheckUnauthorizedStatus(t, resp)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetClientConfig(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.GetClientConfig(context.Background(), "")
|
||||
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.GetClientConfig(context.Background(), "")
|
||||
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("format=old (backward compatibility)", func(t *testing.T) {
|
||||
client := th.Client
|
||||
|
||||
resp, err := client.DoAPIGet(context.Background(), "/config/client?format=old", "")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var config map[string]string
|
||||
err = json.NewDecoder(resp.Body).Decode(&config)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, config["Version"], "config not returned correctly")
|
||||
})
|
||||
|
||||
t.Run("format=junk (ignored)", func(t *testing.T) {
|
||||
client := th.Client
|
||||
|
||||
resp, err := client.DoAPIGet(context.Background(), "/config/client?format=junk", "")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var config map[string]string
|
||||
err = json.NewDecoder(resp.Body).Decode(&config)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, config["Version"], "config not returned correctly")
|
||||
})
|
||||
}
|
||||
|
||||
func TestPatchConfig(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
// Ensure ConsoleLevel is set to DEBUG
|
||||
config := model.Config{LogSettings: model.LogSettings{
|
||||
ConsoleLevel: model.NewPointer("DEBUG"),
|
||||
}}
|
||||
_, _, err := th.SystemAdminClient.PatchConfig(context.Background(), &config)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("config is missing", func(t *testing.T) {
|
||||
_, response, err := th.Client.PatchConfig(context.Background(), nil)
|
||||
require.Error(t, err)
|
||||
CheckBadRequestStatus(t, response)
|
||||
})
|
||||
|
||||
t.Run("user is not system admin", func(t *testing.T) {
|
||||
_, response, err := th.Client.PatchConfig(context.Background(), &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.NewPointer("INFO"),
|
||||
}}
|
||||
|
||||
updatedConfig, _, _ := th.SystemAdminClient.PatchConfig(context.Background(), &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.NewPointer("INFO"),
|
||||
}}
|
||||
|
||||
oldConfig, _, _ := th.LocalClient.GetConfig(context.Background())
|
||||
updatedConfig, _, _ := th.LocalClient.PatchConfig(context.Background(), &config)
|
||||
|
||||
assert.Equal(t, "INFO", *updatedConfig.LogSettings.ConsoleLevel)
|
||||
// reset the config
|
||||
_, _, err := th.LocalClient.UpdateConfig(context.Background(), 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.NewPointer(4),
|
||||
}}
|
||||
|
||||
_, response, err := client.PatchConfig(context.Background(), &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(context.Background())
|
||||
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.NewPointer(false)}
|
||||
config := model.Config{PasswordSettings: model.PasswordSettings{
|
||||
Lowercase: model.NewPointer(true),
|
||||
MinimumLength: model.NewPointer(15),
|
||||
}, LogSettings: model.LogSettings{
|
||||
ConsoleLevel: model.NewPointer("INFO"),
|
||||
},
|
||||
TeamSettings: model.TeamSettings{
|
||||
ExperimentalDefaultChannels: []string{"another-channel"},
|
||||
},
|
||||
PluginSettings: model.PluginSettings{
|
||||
PluginStates: states,
|
||||
},
|
||||
}
|
||||
|
||||
_, response, err := client.PatchConfig(context.Background(), &config)
|
||||
require.NoError(t, err)
|
||||
|
||||
updatedConfig, _, err := client.GetConfig(context.Background())
|
||||
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(context.Background(), oldConfig)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("should sanitize config", func(t *testing.T) {
|
||||
config := model.Config{PasswordSettings: model.PasswordSettings{
|
||||
Symbol: model.NewPointer(true),
|
||||
}}
|
||||
|
||||
updatedConfig, _, err := client.PatchConfig(context.Background(), &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.NewPointer(true),
|
||||
}}
|
||||
|
||||
updatedConfig, resp, err := client.PatchConfig(context.Background(), &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(context.Background(), 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(context.Background(), 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(context.Background())
|
||||
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.NewPointer(nonEmptyURL),
|
||||
},
|
||||
}
|
||||
updatedConfig, _, err := th.SystemAdminClient.PatchConfig(context.Background(), &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.NewPointer(""),
|
||||
},
|
||||
}
|
||||
_, resp, err := th.SystemAdminClient.PatchConfig(context.Background(), &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(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, nonEmptyURL, *cfg.ServiceSettings.SiteURL)
|
||||
|
||||
// Check that sending an empty config returns no error.
|
||||
_, _, err = th.SystemAdminClient.PatchConfig(context.Background(), &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 func() {
|
||||
err = f.RemoveFile("from.json")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
_, err = config.NewStoreFromDSN("to.json", false, nil, true)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
err = f.RemoveFile("to.json")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
_, err = th.LocalClient.MigrateConfig(context.Background(), "from.json", "to.json")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
600
channels/api4/content_flagging.go
Normal file
600
channels/api4/content_flagging.go
Normal file
@ -0,0 +1,600 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitContentFlagging() {
|
||||
if !api.srv.Config().FeatureFlags.ContentFlagging {
|
||||
return
|
||||
}
|
||||
|
||||
api.BaseRoutes.ContentFlagging.Handle("/flag/config", api.APISessionRequired(getFlaggingConfiguration)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/team/{team_id:[A-Za-z0-9]+}/status", api.APISessionRequired(getTeamPostFlaggingFeatureStatus)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/post/{post_id:[A-Za-z0-9]+}/flag", api.APISessionRequired(flagPost)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/fields", api.APISessionRequired(getContentFlaggingFields)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/post/{post_id:[A-Za-z0-9]+}/field_values", api.APISessionRequired(getPostPropertyValues)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/post/{post_id:[A-Za-z0-9]+}", api.APISessionRequired(getFlaggedPost)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/post/{post_id:[A-Za-z0-9]+}/remove", api.APISessionRequired(removeFlaggedPost)).Methods(http.MethodPut)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/post/{post_id:[A-Za-z0-9]+}/keep", api.APISessionRequired(keepFlaggedPost)).Methods(http.MethodPut)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/config", api.APISessionRequired(saveContentFlaggingSettings)).Methods(http.MethodPut)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/config", api.APISessionRequired(getContentFlaggingSettings)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/team/{team_id:[A-Za-z0-9]+}/reviewers/search", api.APISessionRequired(searchReviewers)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/post/{post_id:[A-Za-z0-9]+}/assign/{content_reviewer_id:[A-Za-z0-9]+}", api.APISessionRequired(assignFlaggedPostReviewer)).Methods(http.MethodPost)
|
||||
}
|
||||
|
||||
func requireContentFlaggingAvailable(c *Context) {
|
||||
if !model.MinimumEnterpriseAdvancedLicense(c.App.License()) {
|
||||
c.Err = model.NewAppError("requireContentFlaggingEnabled", "api.content_flagging.error.license", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func requireContentFlaggingEnabled(c *Context) {
|
||||
requireContentFlaggingAvailable(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
contentFlaggingEnabled := c.App.Config().ContentFlaggingSettings.EnableContentFlagging
|
||||
if contentFlaggingEnabled == nil || !*contentFlaggingEnabled {
|
||||
c.Err = model.NewAppError("requireContentFlaggingEnabled", "api.content_flagging.error.disabled", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func requireTeamContentReviewer(c *Context, userId, teamId string) {
|
||||
isReviewer, appErr := c.App.IsUserTeamContentReviewer(userId, teamId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if !isReviewer {
|
||||
c.Err = model.NewAppError("requireTeamContentReviewer", "api.content_flagging.error.user_not_reviewer", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func requireFlaggedPost(c *Context, postId string) {
|
||||
if postId == "" {
|
||||
c.SetInvalidParam("flagged_post_id")
|
||||
return
|
||||
}
|
||||
|
||||
_, appErr := c.App.GetPostContentFlaggingPropertyValue(postId, app.ContentFlaggingPropertyNameStatus)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func getFlaggingConfiguration(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireContentFlaggingEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// A team ID is expected to be specified by a content reviewer.
|
||||
// When specified, we verify that the user is a content reviewer of the team.
|
||||
// If the user is indeed a content reviewer, we return the configuration along with some extra fields
|
||||
// that only a reviewer should be aware of.
|
||||
// If no team ID is specified, we return the configuration as is, without the extra fields.
|
||||
// This is the expected usage for non-reviewers.
|
||||
teamId := r.URL.Query().Get("team_id")
|
||||
asReviewer := false
|
||||
if teamId != "" {
|
||||
requireTeamContentReviewer(c, c.AppContext.Session().UserId, teamId)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
asReviewer = true
|
||||
}
|
||||
|
||||
config := getFlaggingConfig(c.App.Config().ContentFlaggingSettings, asReviewer)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(config); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
c.Err = model.NewAppError("getFlaggingConfiguration", "api.encoding_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
func getTeamPostFlaggingFeatureStatus(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireContentFlaggingEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireTeamId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
teamID := c.Params.TeamId
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionViewTeam) {
|
||||
c.SetPermissionError(model.PermissionViewTeam)
|
||||
return
|
||||
}
|
||||
|
||||
enabled, appErr := c.App.ContentFlaggingEnabledForTeam(teamID)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
payload := map[string]bool{
|
||||
"enabled": enabled,
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
c.Err = model.NewAppError("getTeamPostFlaggingFeatureStatus", "api.encoding_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
func flagPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireContentFlaggingEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.RequirePostId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var flagRequest model.FlagContentRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&flagRequest); err != nil {
|
||||
c.SetInvalidParamWithErr("flagPost", err)
|
||||
return
|
||||
}
|
||||
|
||||
postId := c.Params.PostId
|
||||
userId := c.AppContext.Session().UserId
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventFlagPost, model.AuditStatusFail)
|
||||
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
||||
model.AddEventParameterToAuditRec(auditRec, "postId", postId)
|
||||
model.AddEventParameterToAuditRec(auditRec, "userId", userId)
|
||||
|
||||
post, appErr := c.App.GetPostIfAuthorized(c.AppContext, postId, c.AppContext.Session(), false)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
channel, appErr := c.App.GetChannel(c.AppContext, post.ChannelId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
enabled, appErr := c.App.ContentFlaggingEnabledForTeam(channel.TeamId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if !enabled {
|
||||
c.Err = model.NewAppError("flagPost", "api.content_flagging.error.not_available_on_team", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
appErr = c.App.FlagPost(c.AppContext, post, channel.TeamId, userId, flagRequest)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventObjectType("post")
|
||||
|
||||
writeOKResponse(w)
|
||||
}
|
||||
|
||||
func getFlaggingConfig(contentFlaggingSettings model.ContentFlaggingSettings, asReviewer bool) *model.ContentFlaggingReportingConfig {
|
||||
config := &model.ContentFlaggingReportingConfig{
|
||||
Reasons: contentFlaggingSettings.AdditionalSettings.Reasons,
|
||||
ReporterCommentRequired: contentFlaggingSettings.AdditionalSettings.ReporterCommentRequired,
|
||||
ReviewerCommentRequired: contentFlaggingSettings.AdditionalSettings.ReviewerCommentRequired,
|
||||
}
|
||||
|
||||
if asReviewer {
|
||||
config.NotifyReporterOnRemoval = model.NewPointer(slices.Contains(contentFlaggingSettings.NotificationSettings.EventTargetMapping[model.EventContentRemoved], model.TargetReporter))
|
||||
|
||||
config.NotifyReporterOnDismissal = model.NewPointer(slices.Contains(contentFlaggingSettings.NotificationSettings.EventTargetMapping[model.EventContentDismissed], model.TargetReporter))
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func getContentFlaggingFields(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireContentFlaggingEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
groupId, appErr := c.App.ContentFlaggingGroupId()
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
mappedFields, appErr := c.App.GetContentFlaggingMappedFields(groupId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(mappedFields); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
c.Err = model.NewAppError("getContentFlaggingFields", "api.encoding_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
func getPostPropertyValues(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireContentFlaggingEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.RequirePostId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// The requesting user must be a reviewer of the post's team
|
||||
// to be able to fetch the post's Content Flagging property values
|
||||
postId := c.Params.PostId
|
||||
post, appErr := c.App.GetSinglePost(c.AppContext, postId, true)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
channel, appErr := c.App.GetChannel(c.AppContext, post.ChannelId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
userId := c.AppContext.Session().UserId
|
||||
requireTeamContentReviewer(c, userId, channel.TeamId)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
propertyValues, appErr := c.App.GetPostContentFlaggingPropertyValues(postId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(propertyValues); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
c.Err = model.NewAppError("getPostPropertyValues", "api.encoding_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
func getFlaggedPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireContentFlaggingEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.RequirePostId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// A user can obtain a flagged post if-
|
||||
// 1. The post is currently flagged and in any status
|
||||
// 2. The user is a reviewer of the post's team
|
||||
|
||||
// check if user is a reviewer of the post's team
|
||||
postId := c.Params.PostId
|
||||
userId := c.AppContext.Session().UserId
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventGetFlaggedPost, model.AuditStatusFail)
|
||||
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
||||
model.AddEventParameterToAuditRec(auditRec, "postId", postId)
|
||||
model.AddEventParameterToAuditRec(auditRec, "userId", userId)
|
||||
|
||||
post, appErr := c.App.GetSinglePost(c.AppContext, postId, true)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
channel, appErr := c.App.GetChannel(c.AppContext, post.ChannelId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
requireTeamContentReviewer(c, userId, channel.TeamId)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// This validates that the post is flagged
|
||||
requireFlaggedPost(c, postId)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
post = c.App.PreparePostForClientWithEmbedsAndImages(c.AppContext, post, &model.PreparePostForClientOpts{IncludePriority: true, RetainContent: true, IncludeDeleted: true})
|
||||
post, err := c.App.SanitizePostMetadataForUser(c.AppContext, post, c.AppContext.Session().UserId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if err := post.EncodeJSON(w); err != nil {
|
||||
c.Err = model.NewAppError("getFlaggedPost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
func removeFlaggedPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
actionRequest, userId, post := keepRemoveFlaggedPostChecks(c, r)
|
||||
if c.Err != nil {
|
||||
c.Err.Where = "removeFlaggedPost"
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventPermanentlyRemoveFlaggedPost, model.AuditStatusFail)
|
||||
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
||||
model.AddEventParameterToAuditRec(auditRec, "postId", post.Id)
|
||||
model.AddEventParameterToAuditRec(auditRec, "userId", userId)
|
||||
|
||||
if appErr := c.App.PermanentDeleteFlaggedPost(c.AppContext, actionRequest, userId, post); appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
writeOKResponse(w)
|
||||
}
|
||||
|
||||
func keepFlaggedPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
actionRequest, userId, post := keepRemoveFlaggedPostChecks(c, r)
|
||||
if c.Err != nil {
|
||||
c.Err.Where = "keepFlaggedPost"
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventKeepFlaggedPost, model.AuditStatusFail)
|
||||
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
||||
model.AddEventParameterToAuditRec(auditRec, "postId", post.Id)
|
||||
model.AddEventParameterToAuditRec(auditRec, "userId", userId)
|
||||
|
||||
if appErr := c.App.KeepFlaggedPost(c.AppContext, actionRequest, userId, post); appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
writeOKResponse(w)
|
||||
}
|
||||
|
||||
func keepRemoveFlaggedPostChecks(c *Context, r *http.Request) (*model.FlagContentActionRequest, string, *model.Post) {
|
||||
requireContentFlaggingEnabled(c)
|
||||
if c.Err != nil {
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
c.RequirePostId()
|
||||
if c.Err != nil {
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
var actionRequest model.FlagContentActionRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&actionRequest); err != nil {
|
||||
c.SetInvalidParamWithErr("flagContentActionRequestBody", err)
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
postId := c.Params.PostId
|
||||
userId := c.AppContext.Session().UserId
|
||||
|
||||
post, appErr := c.App.GetSinglePost(c.AppContext, postId, true)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
channel, appErr := c.App.GetChannel(c.AppContext, post.ChannelId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
requireTeamContentReviewer(c, userId, channel.TeamId)
|
||||
if c.Err != nil {
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
commentRequired := c.App.Config().ContentFlaggingSettings.AdditionalSettings.ReviewerCommentRequired
|
||||
if err := actionRequest.IsValid(*commentRequired); err != nil {
|
||||
c.Err = err
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
return &actionRequest, userId, post
|
||||
}
|
||||
|
||||
func saveContentFlaggingSettings(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireContentFlaggingAvailable(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var config model.ContentFlaggingSettingsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
|
||||
c.SetInvalidParamWithErr("config", err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventUpdateContentFlaggingConfig, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
config.SetDefaults()
|
||||
if appErr := config.IsValid(); appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
appErr := c.App.SaveContentFlaggingConfig(config)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
writeOKResponse(w)
|
||||
}
|
||||
|
||||
func getContentFlaggingSettings(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireContentFlaggingAvailable(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
reviewerIDs, appErr := c.App.GetContentFlaggingConfigReviewerIDs()
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
config := c.App.Config().ContentFlaggingSettings
|
||||
|
||||
fullConfig := model.ContentFlaggingSettingsRequest{
|
||||
ReviewerSettings: &model.ReviewSettingsRequest{
|
||||
ReviewerSettings: *config.ReviewerSettings,
|
||||
ReviewerIDsSettings: *reviewerIDs,
|
||||
},
|
||||
ContentFlaggingSettingsBase: model.ContentFlaggingSettingsBase{
|
||||
EnableContentFlagging: config.EnableContentFlagging,
|
||||
NotificationSettings: config.NotificationSettings,
|
||||
AdditionalSettings: config.AdditionalSettings,
|
||||
},
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(fullConfig); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
c.Err = model.NewAppError("getContentFlaggingSettings", "api.encoding_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
func searchReviewers(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireContentFlaggingEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireTeamId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
teamId := c.Params.TeamId
|
||||
userId := c.AppContext.Session().UserId
|
||||
searchTerm := strings.TrimSpace(r.URL.Query().Get("term"))
|
||||
|
||||
requireTeamContentReviewer(c, userId, teamId)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
reviewers, appErr := c.App.SearchReviewers(c.AppContext, searchTerm, teamId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(reviewers); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
c.Err = model.NewAppError("searchReviewers", "api.encoding_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
func assignFlaggedPostReviewer(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireContentFlaggingEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.RequirePostId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireContentReviewerId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
postId := c.Params.PostId
|
||||
post, appErr := c.App.GetSinglePost(c.AppContext, postId, true)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
channel, appErr := c.App.GetChannel(c.AppContext, post.ChannelId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
assignedBy := c.AppContext.Session().UserId
|
||||
requireTeamContentReviewer(c, assignedBy, channel.TeamId)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
reviewerId := c.Params.ContentReviewerId
|
||||
requireTeamContentReviewer(c, reviewerId, channel.TeamId)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventSetReviewer, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "assigningUserId", assignedBy)
|
||||
model.AddEventParameterToAuditRec(auditRec, "reviewerUserId", reviewerId)
|
||||
|
||||
appErr = c.App.AssignFlaggedPostReviewer(c.AppContext, postId, channel.TeamId, reviewerId, assignedBy)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
writeOKResponse(w)
|
||||
}
|
||||
973
channels/api4/content_flagging_test.go
Normal file
973
channels/api4/content_flagging_test.go
Normal file
@ -0,0 +1,973 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils/testutils"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func setBasicCommonReviewerConfig(th *TestHelper) *model.AppError {
|
||||
config := model.ContentFlaggingSettingsRequest{
|
||||
ContentFlaggingSettingsBase: model.ContentFlaggingSettingsBase{
|
||||
EnableContentFlagging: model.NewPointer(true),
|
||||
},
|
||||
ReviewerSettings: &model.ReviewSettingsRequest{
|
||||
ReviewerSettings: model.ReviewerSettings{
|
||||
CommonReviewers: model.NewPointer(true),
|
||||
},
|
||||
ReviewerIDsSettings: model.ReviewerIDsSettings{
|
||||
CommonReviewerIds: []string{th.BasicUser.Id},
|
||||
},
|
||||
},
|
||||
}
|
||||
config.SetDefaults()
|
||||
return th.App.SaveContentFlaggingConfig(config)
|
||||
}
|
||||
|
||||
func TestGetFlaggingConfiguration(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
client := th.Client
|
||||
|
||||
t.Run("Should return 501 when Enterprise Advanced license is not present even if feature is enabled", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(true)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
status, resp, err := client.GetFlaggingConfiguration(context.Background())
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
require.Nil(t, status)
|
||||
})
|
||||
|
||||
t.Run("Should return 501 when feature is disabled", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(false)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
status, resp, err := client.GetFlaggingConfiguration(context.Background())
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
require.Nil(t, status)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSaveContentFlaggingSettings(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
client := th.Client
|
||||
|
||||
t.Run("Should return 403 when user does not have manage system permission", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
config := model.ContentFlaggingSettingsRequest{
|
||||
ContentFlaggingSettingsBase: model.ContentFlaggingSettingsBase{
|
||||
EnableContentFlagging: model.NewPointer(true),
|
||||
},
|
||||
ReviewerSettings: &model.ReviewSettingsRequest{
|
||||
ReviewerSettings: model.ReviewerSettings{
|
||||
CommonReviewers: model.NewPointer(true),
|
||||
},
|
||||
ReviewerIDsSettings: model.ReviewerIDsSettings{
|
||||
CommonReviewerIds: []string{th.BasicUser.Id},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Use basic user who doesn't have manage system permission
|
||||
th.LoginBasic()
|
||||
resp, err := client.SaveContentFlaggingSettings(context.Background(), &config)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Should return 400 when config is invalid", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
// Invalid config - missing required fields
|
||||
config := model.ContentFlaggingSettingsRequest{
|
||||
ReviewerSettings: &model.ReviewSettingsRequest{
|
||||
ReviewerSettings: model.ReviewerSettings{
|
||||
CommonReviewers: model.NewPointer(true),
|
||||
TeamAdminsAsReviewers: model.NewPointer(false),
|
||||
},
|
||||
ReviewerIDsSettings: model.ReviewerIDsSettings{
|
||||
CommonReviewerIds: []string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
config.SetDefaults()
|
||||
|
||||
th.LoginSystemAdmin()
|
||||
resp, err := th.SystemAdminClient.SaveContentFlaggingSettings(context.Background(), &config)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Should successfully save content flagging settings when user has manage system permission", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
config := model.ContentFlaggingSettingsRequest{
|
||||
ContentFlaggingSettingsBase: model.ContentFlaggingSettingsBase{
|
||||
EnableContentFlagging: model.NewPointer(true),
|
||||
},
|
||||
ReviewerSettings: &model.ReviewSettingsRequest{
|
||||
ReviewerSettings: model.ReviewerSettings{
|
||||
CommonReviewers: model.NewPointer(true),
|
||||
},
|
||||
ReviewerIDsSettings: model.ReviewerIDsSettings{
|
||||
CommonReviewerIds: []string{th.BasicUser.Id},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Use system admin who has manage system permission
|
||||
resp, err := th.SystemAdminClient.SaveContentFlaggingSettings(context.Background(), &config)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetContentFlaggingSettings(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("Should return 403 when user does not have manage system permission", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
// Use basic user who doesn't have manage system permission
|
||||
th.LoginBasic()
|
||||
settings, resp, err := th.Client.GetContentFlaggingSettings(context.Background())
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
require.Nil(t, settings)
|
||||
})
|
||||
|
||||
t.Run("Should successfully get content flagging settings when user has manage system permission", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
// First save some settings
|
||||
appErr := setBasicCommonReviewerConfig(th)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Use system admin who has manage system permission
|
||||
settings, resp, err := th.SystemAdminClient.GetContentFlaggingSettings(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.NotNil(t, settings)
|
||||
require.NotNil(t, settings.EnableContentFlagging)
|
||||
require.True(t, *settings.EnableContentFlagging)
|
||||
require.NotNil(t, settings.ReviewerSettings)
|
||||
require.NotNil(t, settings.ReviewerSettings.CommonReviewers)
|
||||
require.True(t, *settings.ReviewerSettings.CommonReviewers)
|
||||
require.NotNil(t, settings.ReviewerSettings.CommonReviewerIds)
|
||||
require.Contains(t, settings.ReviewerSettings.CommonReviewerIds, th.BasicUser.Id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetPostPropertyValues(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
client := th.Client
|
||||
|
||||
t.Run("Should return 501 when Enterprise Advanced license is not present even if feature is enabled", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(true)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
post := th.CreatePost()
|
||||
propertyValues, resp, err := client.GetPostPropertyValues(context.Background(), post.Id)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
require.Nil(t, propertyValues)
|
||||
})
|
||||
|
||||
t.Run("Should return 501 when feature is disabled", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(false)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
post := th.CreatePost()
|
||||
propertyValues, resp, err := client.GetPostPropertyValues(context.Background(), post.Id)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
require.Nil(t, propertyValues)
|
||||
})
|
||||
|
||||
t.Run("Should return 404 when post does not exist", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(true)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
propertyValues, resp, err := client.GetPostPropertyValues(context.Background(), model.NewId())
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
require.Nil(t, propertyValues)
|
||||
})
|
||||
|
||||
t.Run("Should return 403 when user is not a reviewer", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(true)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
post := th.CreatePost()
|
||||
propertyValues, resp, err := client.GetPostPropertyValues(context.Background(), post.Id)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
require.Nil(t, propertyValues)
|
||||
})
|
||||
|
||||
t.Run("Should successfully get property values when user is a reviewer", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
appErr := setBasicCommonReviewerConfig(th)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
post := th.CreatePost()
|
||||
response, err := client.FlagPostForContentReview(context.Background(), post.Id, &model.FlagContentRequest{
|
||||
Reason: "Sensitive data",
|
||||
Comment: "This is sensitive content",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||
|
||||
// Now get the property values
|
||||
propertyValues, resp, err := client.GetPostPropertyValues(context.Background(), post.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.NotNil(t, propertyValues)
|
||||
require.Len(t, propertyValues, 6)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetFlaggedPost(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
client := th.Client
|
||||
|
||||
t.Run("Should return 501 when Enterprise Advanced license is not present even if feature is enabled", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(true)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
post := th.CreatePost()
|
||||
flaggedPost, resp, err := client.GetContentFlaggedPost(context.Background(), post.Id)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
require.Nil(t, flaggedPost)
|
||||
})
|
||||
|
||||
t.Run("Should return 501 when feature is disabled", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(false)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
post := th.CreatePost()
|
||||
flaggedPost, resp, err := client.GetContentFlaggedPost(context.Background(), post.Id)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
require.Nil(t, flaggedPost)
|
||||
})
|
||||
|
||||
t.Run("Should return 404 when post does not exist", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(true)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
flaggedPost, resp, err := client.GetContentFlaggedPost(context.Background(), model.NewId())
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
require.Nil(t, flaggedPost)
|
||||
})
|
||||
|
||||
t.Run("Should return 403 when user is not a reviewer", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
|
||||
config := model.ContentFlaggingSettingsRequest{
|
||||
ContentFlaggingSettingsBase: model.ContentFlaggingSettingsBase{
|
||||
EnableContentFlagging: model.NewPointer(true),
|
||||
},
|
||||
ReviewerSettings: &model.ReviewSettingsRequest{
|
||||
ReviewerSettings: model.ReviewerSettings{
|
||||
CommonReviewers: model.NewPointer(false),
|
||||
},
|
||||
ReviewerIDsSettings: model.ReviewerIDsSettings{
|
||||
TeamReviewersSetting: map[string]*model.TeamReviewerSetting{
|
||||
th.BasicTeam.Id: {
|
||||
Enabled: model.NewPointer(true),
|
||||
ReviewerIds: []string{}, // Empty list - user is not a reviewer
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
config.SetDefaults()
|
||||
appErr := th.App.SaveContentFlaggingConfig(config)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
post := th.CreatePost()
|
||||
flaggedPost, resp, err := client.GetContentFlaggedPost(context.Background(), post.Id)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
require.Nil(t, flaggedPost)
|
||||
})
|
||||
|
||||
t.Run("Should return 404 when post is not flagged", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
|
||||
appErr := setBasicCommonReviewerConfig(th)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
post := th.CreatePost()
|
||||
flaggedPost, resp, err := client.GetContentFlaggedPost(context.Background(), post.Id)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
require.Nil(t, flaggedPost)
|
||||
})
|
||||
|
||||
t.Run("Should successfully get flagged post when user is a reviewer and post is flagged", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
|
||||
appErr := setBasicCommonReviewerConfig(th)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
post := th.CreatePost()
|
||||
|
||||
// First flag the post
|
||||
flagRequest := &model.FlagContentRequest{
|
||||
Reason: "Sensitive data",
|
||||
Comment: "This is sensitive content",
|
||||
}
|
||||
resp, err := client.FlagPostForContentReview(context.Background(), post.Id, flagRequest)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// Now get the flagged post
|
||||
flaggedPost, resp, err := client.GetContentFlaggedPost(context.Background(), post.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.NotNil(t, flaggedPost)
|
||||
require.Equal(t, post.Id, flaggedPost.Id)
|
||||
})
|
||||
|
||||
t.Run("Should return flagged post's file info", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
|
||||
appErr := setBasicCommonReviewerConfig(th)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
data, err2 := testutils.ReadTestFile("test.png")
|
||||
require.NoError(t, err2)
|
||||
|
||||
fileResponse, _, err := client.UploadFile(context.Background(), data, th.BasicChannel.Id, "test.png")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(fileResponse.FileInfos))
|
||||
fileInfo := fileResponse.FileInfos[0]
|
||||
|
||||
post := th.CreatePostInChannelWithFiles(th.BasicChannel, fileInfo)
|
||||
|
||||
// First flag the post
|
||||
flagRequest := &model.FlagContentRequest{
|
||||
Reason: "Sensitive data",
|
||||
Comment: "This is sensitive content",
|
||||
}
|
||||
resp, err := client.FlagPostForContentReview(context.Background(), post.Id, flagRequest)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
flaggedPost, resp, err := client.GetContentFlaggedPost(context.Background(), post.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.Equal(t, 1, len(flaggedPost.Metadata.Files))
|
||||
require.Equal(t, fileInfo.Id, flaggedPost.Metadata.Files[0].Id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFlagPost(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
client := th.Client
|
||||
|
||||
t.Run("Should return 501 when Enterprise Advanced license is not present even if feature is enabled", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(true)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
post := th.CreatePost()
|
||||
flagRequest := &model.FlagContentRequest{
|
||||
Reason: "spam",
|
||||
Comment: "This is spam content",
|
||||
}
|
||||
|
||||
resp, err := client.FlagPostForContentReview(context.Background(), post.Id, flagRequest)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Should return 501 when feature is disabled", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(false)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
post := th.CreatePost()
|
||||
flagRequest := &model.FlagContentRequest{
|
||||
Reason: "spam",
|
||||
Comment: "This is spam content",
|
||||
}
|
||||
|
||||
resp, err := client.FlagPostForContentReview(context.Background(), post.Id, flagRequest)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Should return 404 when post does not exist", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(true)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
flagRequest := &model.FlagContentRequest{
|
||||
Reason: "spam",
|
||||
Comment: "This is spam content",
|
||||
}
|
||||
|
||||
resp, err := client.FlagPostForContentReview(context.Background(), model.NewId(), flagRequest)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Should return 403 when user does not have permission to view post", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(true)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
// Create a private channel and post
|
||||
privateChannel := th.CreatePrivateChannel()
|
||||
post := th.CreatePostWithClient(th.Client, privateChannel)
|
||||
th.RemoveUserFromChannel(th.BasicUser, privateChannel)
|
||||
|
||||
flagRequest := &model.FlagContentRequest{
|
||||
Reason: "spam",
|
||||
Comment: "This is spam content",
|
||||
}
|
||||
|
||||
resp, err := client.FlagPostForContentReview(context.Background(), post.Id, flagRequest)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Should return 400 when content flagging is not enabled for the team", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
config := model.ContentFlaggingSettingsRequest{
|
||||
ContentFlaggingSettingsBase: model.ContentFlaggingSettingsBase{
|
||||
EnableContentFlagging: model.NewPointer(true),
|
||||
},
|
||||
ReviewerSettings: &model.ReviewSettingsRequest{
|
||||
ReviewerSettings: model.ReviewerSettings{
|
||||
CommonReviewers: model.NewPointer(false),
|
||||
},
|
||||
ReviewerIDsSettings: model.ReviewerIDsSettings{
|
||||
TeamReviewersSetting: map[string]*model.TeamReviewerSetting{
|
||||
th.BasicTeam.Id: {Enabled: model.NewPointer(false)},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
config.SetDefaults()
|
||||
appErr := th.App.SaveContentFlaggingConfig(config)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
post := th.CreatePost()
|
||||
flagRequest := &model.FlagContentRequest{
|
||||
Reason: "spam",
|
||||
Comment: "This is spam content",
|
||||
}
|
||||
|
||||
resp, err := client.FlagPostForContentReview(context.Background(), post.Id, flagRequest)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Should successfully flag a post when all conditions are met", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
appErr := setBasicCommonReviewerConfig(th)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
post := th.CreatePost()
|
||||
flagRequest := &model.FlagContentRequest{
|
||||
Reason: "Sensitive data",
|
||||
Comment: "This is sensitive data",
|
||||
}
|
||||
|
||||
resp, err := client.FlagPostForContentReview(context.Background(), post.Id, flagRequest)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetTeamPostReportingFeatureStatus(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
client := th.Client
|
||||
|
||||
t.Run("Should return 501 when Enterprise Advanced license is not present even if feature is enabled", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(true)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
status, resp, err := client.GetTeamPostFlaggingFeatureStatus(context.Background(), model.NewId())
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
require.Nil(t, status)
|
||||
})
|
||||
|
||||
t.Run("Should return 501 when feature is disabled", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(false)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
status, resp, err := client.GetTeamPostFlaggingFeatureStatus(context.Background(), model.NewId())
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
require.Nil(t, status)
|
||||
})
|
||||
|
||||
t.Run("Should return Forbidden error when calling for a team without the team membership", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
config := model.ContentFlaggingSettingsRequest{
|
||||
ContentFlaggingSettingsBase: model.ContentFlaggingSettingsBase{
|
||||
EnableContentFlagging: model.NewPointer(true),
|
||||
},
|
||||
ReviewerSettings: &model.ReviewSettingsRequest{
|
||||
ReviewerSettings: model.ReviewerSettings{
|
||||
CommonReviewers: model.NewPointer(true),
|
||||
},
|
||||
ReviewerIDsSettings: model.ReviewerIDsSettings{
|
||||
CommonReviewerIds: []string{"reviewer_user_id_1", "reviewer_user_id_2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
config.SetDefaults()
|
||||
appErr := th.App.SaveContentFlaggingConfig(config)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// using basic user because the default user is a system admin, and they have
|
||||
// access to all teams even without being an explicit team member
|
||||
th.LoginBasic()
|
||||
team := th.CreateTeam()
|
||||
// unlinking from the created team as by default the team's creator is
|
||||
// a team member, so we need to leave the team explicitly
|
||||
th.UnlinkUserFromTeam(th.BasicUser, team)
|
||||
|
||||
status, resp, err := client.GetTeamPostFlaggingFeatureStatus(context.Background(), team.Id)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
require.Nil(t, status)
|
||||
|
||||
// now we will join the team and that will allow us to call the endpoint without error
|
||||
th.LinkUserToTeam(th.BasicUser, team)
|
||||
status, resp, err = client.GetTeamPostFlaggingFeatureStatus(context.Background(), team.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.True(t, status["enabled"])
|
||||
})
|
||||
}
|
||||
|
||||
func TestSearchReviewers(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
client := th.Client
|
||||
|
||||
t.Run("Should return 501 when Enterprise Advanced license is not present even if feature is enabled", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(true)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
reviewers, resp, err := client.SearchContentFlaggingReviewers(context.Background(), th.BasicTeam.Id, "test")
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
require.Nil(t, reviewers)
|
||||
})
|
||||
|
||||
t.Run("Should return 501 when feature is disabled", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(false)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
reviewers, resp, err := client.SearchContentFlaggingReviewers(context.Background(), th.BasicTeam.Id, "test")
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
require.Nil(t, reviewers)
|
||||
})
|
||||
|
||||
t.Run("Should return 403 when user is not a reviewer", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
config := model.ContentFlaggingSettingsRequest{
|
||||
ContentFlaggingSettingsBase: model.ContentFlaggingSettingsBase{
|
||||
EnableContentFlagging: model.NewPointer(true),
|
||||
},
|
||||
ReviewerSettings: &model.ReviewSettingsRequest{
|
||||
ReviewerSettings: model.ReviewerSettings{
|
||||
CommonReviewers: model.NewPointer(false),
|
||||
},
|
||||
ReviewerIDsSettings: model.ReviewerIDsSettings{
|
||||
TeamReviewersSetting: map[string]*model.TeamReviewerSetting{
|
||||
th.BasicTeam.Id: {
|
||||
Enabled: model.NewPointer(true),
|
||||
ReviewerIds: []string{}, // Empty list - user is not a reviewer
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
config.SetDefaults()
|
||||
appErr := th.App.SaveContentFlaggingConfig(config)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
reviewers, resp, err := client.SearchContentFlaggingReviewers(context.Background(), th.BasicTeam.Id, "test")
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
require.Nil(t, reviewers)
|
||||
})
|
||||
|
||||
t.Run("Should successfully search reviewers when user is a reviewer", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
appErr := setBasicCommonReviewerConfig(th)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
reviewers, resp, err := client.SearchContentFlaggingReviewers(context.Background(), th.BasicTeam.Id, "basic")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.NotNil(t, reviewers)
|
||||
})
|
||||
|
||||
t.Run("Should successfully search reviewers when user is a team reviewer", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
config := model.ContentFlaggingSettingsRequest{
|
||||
ContentFlaggingSettingsBase: model.ContentFlaggingSettingsBase{
|
||||
EnableContentFlagging: model.NewPointer(true),
|
||||
},
|
||||
ReviewerSettings: &model.ReviewSettingsRequest{
|
||||
ReviewerSettings: model.ReviewerSettings{
|
||||
CommonReviewers: model.NewPointer(false),
|
||||
},
|
||||
ReviewerIDsSettings: model.ReviewerIDsSettings{
|
||||
TeamReviewersSetting: map[string]*model.TeamReviewerSetting{
|
||||
th.BasicTeam.Id: {
|
||||
Enabled: model.NewPointer(true),
|
||||
ReviewerIds: []string{th.BasicUser.Id},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
config.SetDefaults()
|
||||
appErr := th.App.SaveContentFlaggingConfig(config)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
reviewers, resp, err := client.SearchContentFlaggingReviewers(context.Background(), th.BasicTeam.Id, "basic")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.NotNil(t, reviewers)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAssignContentFlaggingReviewer(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
client := th.Client
|
||||
|
||||
t.Run("Should return 501 when Enterprise Advanced license is not present even if feature is enabled", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(true)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
post := th.CreatePost()
|
||||
resp, err := client.AssignContentFlaggingReviewer(context.Background(), post.Id, th.BasicUser.Id)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Should return 501 when feature is disabled", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(false)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
post := th.CreatePost()
|
||||
resp, err := client.AssignContentFlaggingReviewer(context.Background(), post.Id, th.BasicUser.Id)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Should return 404 when post does not exist", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(true)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
resp, err := client.AssignContentFlaggingReviewer(context.Background(), model.NewId(), th.BasicUser.Id)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Should return 400 when user ID is invalid", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
appErr := setBasicCommonReviewerConfig(th)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
post := th.CreatePost()
|
||||
resp, err := client.AssignContentFlaggingReviewer(context.Background(), post.Id, "invalidUserId")
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Should return 403 when assigning user is not a reviewer", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
config := model.ContentFlaggingSettingsRequest{
|
||||
ContentFlaggingSettingsBase: model.ContentFlaggingSettingsBase{
|
||||
EnableContentFlagging: model.NewPointer(true),
|
||||
},
|
||||
ReviewerSettings: &model.ReviewSettingsRequest{
|
||||
ReviewerSettings: model.ReviewerSettings{
|
||||
CommonReviewers: model.NewPointer(false),
|
||||
},
|
||||
ReviewerIDsSettings: model.ReviewerIDsSettings{
|
||||
TeamReviewersSetting: map[string]*model.TeamReviewerSetting{
|
||||
th.BasicTeam.Id: {
|
||||
Enabled: model.NewPointer(true),
|
||||
ReviewerIds: []string{}, // Empty list - user is not a reviewer
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
config.SetDefaults()
|
||||
appErr := th.App.SaveContentFlaggingConfig(config)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
post := th.CreatePost()
|
||||
resp, err := client.AssignContentFlaggingReviewer(context.Background(), post.Id, th.BasicUser.Id)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Should return 400 when assignee is not a reviewer", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
// Create another user who will not be a reviewer
|
||||
nonReviewerUser := th.CreateUser()
|
||||
th.LinkUserToTeam(nonReviewerUser, th.BasicTeam)
|
||||
|
||||
config := model.ContentFlaggingSettingsRequest{
|
||||
ContentFlaggingSettingsBase: model.ContentFlaggingSettingsBase{
|
||||
EnableContentFlagging: model.NewPointer(true),
|
||||
},
|
||||
ReviewerSettings: &model.ReviewSettingsRequest{
|
||||
ReviewerSettings: model.ReviewerSettings{
|
||||
CommonReviewers: model.NewPointer(true),
|
||||
},
|
||||
ReviewerIDsSettings: model.ReviewerIDsSettings{
|
||||
CommonReviewerIds: []string{th.BasicUser.Id}, // Only BasicUser is a reviewer
|
||||
},
|
||||
},
|
||||
}
|
||||
config.SetDefaults()
|
||||
appErr := th.App.SaveContentFlaggingConfig(config)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
post := th.CreatePost()
|
||||
// Try to assign non-reviewer user
|
||||
resp, err := client.AssignContentFlaggingReviewer(context.Background(), post.Id, nonReviewerUser.Id)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Should successfully assign reviewer when all conditions are met", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
// Create another reviewer user
|
||||
reviewerUser := th.CreateUser()
|
||||
th.LinkUserToTeam(reviewerUser, th.BasicTeam)
|
||||
|
||||
config := model.ContentFlaggingSettingsRequest{
|
||||
ContentFlaggingSettingsBase: model.ContentFlaggingSettingsBase{
|
||||
EnableContentFlagging: model.NewPointer(true),
|
||||
},
|
||||
ReviewerSettings: &model.ReviewSettingsRequest{
|
||||
ReviewerSettings: model.ReviewerSettings{
|
||||
CommonReviewers: model.NewPointer(true),
|
||||
},
|
||||
ReviewerIDsSettings: model.ReviewerIDsSettings{
|
||||
CommonReviewerIds: []string{th.BasicUser.Id, reviewerUser.Id},
|
||||
},
|
||||
},
|
||||
}
|
||||
config.SetDefaults()
|
||||
appErr := th.App.SaveContentFlaggingConfig(config)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
post := th.CreatePost()
|
||||
|
||||
// First flag the post so it can be assigned
|
||||
flagRequest := &model.FlagContentRequest{
|
||||
Reason: "Sensitive data",
|
||||
Comment: "This is sensitive content",
|
||||
}
|
||||
flagResp, err := client.FlagPostForContentReview(context.Background(), post.Id, flagRequest)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, flagResp.StatusCode)
|
||||
|
||||
// Now assign the reviewer
|
||||
resp, err := client.AssignContentFlaggingReviewer(context.Background(), post.Id, reviewerUser.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Should successfully assign reviewer when user is team reviewer", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense()
|
||||
|
||||
// Create another reviewer user
|
||||
reviewerUser := th.CreateUser()
|
||||
th.LinkUserToTeam(reviewerUser, th.BasicTeam)
|
||||
|
||||
config := model.ContentFlaggingSettingsRequest{
|
||||
ContentFlaggingSettingsBase: model.ContentFlaggingSettingsBase{
|
||||
EnableContentFlagging: model.NewPointer(true),
|
||||
},
|
||||
ReviewerSettings: &model.ReviewSettingsRequest{
|
||||
ReviewerSettings: model.ReviewerSettings{
|
||||
CommonReviewers: model.NewPointer(false),
|
||||
},
|
||||
ReviewerIDsSettings: model.ReviewerIDsSettings{
|
||||
TeamReviewersSetting: map[string]*model.TeamReviewerSetting{
|
||||
th.BasicTeam.Id: {
|
||||
Enabled: model.NewPointer(true),
|
||||
ReviewerIds: []string{th.BasicUser.Id, reviewerUser.Id},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
config.SetDefaults()
|
||||
appErr := th.App.SaveContentFlaggingConfig(config)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
post := th.CreatePost()
|
||||
|
||||
// First flag the post so it can be assigned
|
||||
flagRequest := &model.FlagContentRequest{
|
||||
Reason: "Sensitive data",
|
||||
Comment: "This is sensitive content",
|
||||
}
|
||||
flagResp, err := client.FlagPostForContentReview(context.Background(), post.Id, flagRequest)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, flagResp.StatusCode)
|
||||
|
||||
// Now assign the reviewer
|
||||
resp, err := client.AssignContentFlaggingReviewer(context.Background(), post.Id, reviewerUser.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
155
channels/api4/cors_test.go
Normal file
155
channels/api4/cors_test.go
Normal file
@ -0,0 +1,155 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
|
||||
)
|
||||
|
||||
const (
|
||||
acAllowOrigin = "Access-Control-Allow-Origin"
|
||||
acExposeHeaders = "Access-Control-Expose-Headers"
|
||||
acMaxAge = "Access-Control-Max-Age"
|
||||
acAllowCredentials = "Access-Control-Allow-Credentials"
|
||||
acAllowMethods = "Access-Control-Allow-Methods"
|
||||
acAllowHeaders = "Access-Control-Allow-Headers"
|
||||
)
|
||||
|
||||
func TestCORSRequestHandling(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
for name, testcase := range map[string]struct {
|
||||
AllowCorsFrom string
|
||||
CorsExposedHeaders string
|
||||
CorsAllowCredentials bool
|
||||
ModifyRequest func(req *http.Request)
|
||||
ExpectedAllowOrigin string
|
||||
ExpectedExposeHeaders string
|
||||
ExpectedAllowCredentials string
|
||||
}{
|
||||
"NoCORS": {
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
func(req *http.Request) {
|
||||
},
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
},
|
||||
"CORSEnabled": {
|
||||
"http://somewhere.com",
|
||||
"",
|
||||
false,
|
||||
func(req *http.Request) {
|
||||
},
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
},
|
||||
"CORSEnabledStarOrigin": {
|
||||
"*",
|
||||
"",
|
||||
false,
|
||||
func(req *http.Request) {
|
||||
req.Header.Set("Origin", "http://pre-release.mattermost.com")
|
||||
},
|
||||
"*",
|
||||
"",
|
||||
"",
|
||||
},
|
||||
"CORSEnabledStarNoOrigin": { // CORS spec requires this, not a bug.
|
||||
"*",
|
||||
"",
|
||||
false,
|
||||
func(req *http.Request) {
|
||||
},
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
},
|
||||
"CORSEnabledMatching": {
|
||||
"http://mattermost.com",
|
||||
"",
|
||||
false,
|
||||
func(req *http.Request) {
|
||||
req.Header.Set("Origin", "http://mattermost.com")
|
||||
},
|
||||
"http://mattermost.com",
|
||||
"",
|
||||
"",
|
||||
},
|
||||
"CORSEnabledMultiple": {
|
||||
"http://spinmint.com http://mattermost.com",
|
||||
"",
|
||||
false,
|
||||
func(req *http.Request) {
|
||||
req.Header.Set("Origin", "http://mattermost.com")
|
||||
},
|
||||
"http://mattermost.com",
|
||||
"",
|
||||
"",
|
||||
},
|
||||
"CORSEnabledWithCredentials": {
|
||||
"http://mattermost.com",
|
||||
"",
|
||||
true,
|
||||
func(req *http.Request) {
|
||||
req.Header.Set("Origin", "http://mattermost.com")
|
||||
},
|
||||
"http://mattermost.com",
|
||||
"",
|
||||
"true",
|
||||
},
|
||||
"CORSEnabledWithHeaders": {
|
||||
"http://mattermost.com",
|
||||
"x-my-special-header x-blueberry",
|
||||
true,
|
||||
func(req *http.Request) {
|
||||
req.Header.Set("Origin", "http://mattermost.com")
|
||||
},
|
||||
"http://mattermost.com",
|
||||
"X-My-Special-Header, X-Blueberry",
|
||||
"true",
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
th := SetupConfigWithStoreMock(t, func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.AllowCorsFrom = testcase.AllowCorsFrom
|
||||
*cfg.ServiceSettings.CorsExposedHeaders = testcase.CorsExposedHeaders
|
||||
*cfg.ServiceSettings.CorsAllowCredentials = testcase.CorsAllowCredentials
|
||||
})
|
||||
defer th.TearDown()
|
||||
licenseStore := mocks.LicenseStore{}
|
||||
licenseStore.On("Get", "").Return(&model.LicenseRecord{}, nil)
|
||||
th.App.Srv().Store().(*mocks.Store).On("License").Return(&licenseStore)
|
||||
|
||||
port := th.App.Srv().ListenAddr.Port
|
||||
host := fmt.Sprintf("http://localhost:%v", port)
|
||||
url := fmt.Sprintf("%v/api/v4/system/ping", host)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
require.NoError(t, err)
|
||||
testcase.ModifyRequest(req)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.Equal(t, testcase.ExpectedAllowOrigin, resp.Header.Get(acAllowOrigin))
|
||||
assert.Equal(t, testcase.ExpectedExposeHeaders, resp.Header.Get(acExposeHeaders))
|
||||
assert.Equal(t, "", resp.Header.Get(acMaxAge))
|
||||
assert.Equal(t, testcase.ExpectedAllowCredentials, resp.Header.Get(acAllowCredentials))
|
||||
assert.Equal(t, "", resp.Header.Get(acAllowMethods))
|
||||
assert.Equal(t, "", resp.Header.Get(acAllowHeaders))
|
||||
})
|
||||
}
|
||||
}
|
||||
375
channels/api4/custom_profile_attributes.go
Normal file
375
channels/api4/custom_profile_attributes.go
Normal file
@ -0,0 +1,375 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitCustomProfileAttributes() {
|
||||
if api.srv.Config().FeatureFlags.CustomProfileAttributes {
|
||||
api.BaseRoutes.CustomProfileAttributesFields.Handle("", api.APISessionRequired(listCPAFields)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.CustomProfileAttributesFields.Handle("", api.APISessionRequired(createCPAField)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APISessionRequired(patchCPAField)).Methods(http.MethodPatch)
|
||||
api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APISessionRequired(deleteCPAField)).Methods(http.MethodDelete)
|
||||
api.BaseRoutes.User.Handle("/custom_profile_attributes", api.APISessionRequired(listCPAValues)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.CustomProfileAttributesValues.Handle("", api.APISessionRequired(patchCPAValues)).Methods(http.MethodPatch)
|
||||
api.BaseRoutes.CustomProfileAttributes.Handle("/group", api.APISessionRequired(getCPAGroup)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.User.Handle("/custom_profile_attributes", api.APISessionRequired(patchCPAValuesForUser)).Methods(http.MethodPatch)
|
||||
}
|
||||
}
|
||||
|
||||
func listCPAFields(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !model.MinimumEnterpriseLicense(c.App.Channels().License()) {
|
||||
c.Err = model.NewAppError("Api4.listCPAFields", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
fields, appErr := c.App.ListCPAFields()
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(fields); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func createCPAField(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
if !model.MinimumEnterpriseLicense(c.App.Channels().License()) {
|
||||
c.Err = model.NewAppError("Api4.createCPAField", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
var pf *model.CPAField
|
||||
err := json.NewDecoder(r.Body).Decode(&pf)
|
||||
if err != nil || pf == nil {
|
||||
c.SetInvalidParamWithErr("property_field", err)
|
||||
return
|
||||
}
|
||||
|
||||
pf.Name = strings.TrimSpace(pf.Name)
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventCreateCPAField, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "property_field", pf)
|
||||
|
||||
createdField, appErr := c.App.CreateCPAField(pf)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(createdField)
|
||||
auditRec.AddEventObjectType("property_field")
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(createdField); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func patchCPAField(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
if !model.MinimumEnterpriseLicense(c.App.Channels().License()) {
|
||||
c.Err = model.NewAppError("Api4.patchCPAField", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireFieldId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var patch *model.PropertyFieldPatch
|
||||
err := json.NewDecoder(r.Body).Decode(&patch)
|
||||
if err != nil || patch == nil {
|
||||
c.SetInvalidParamWithErr("property_field_patch", err)
|
||||
return
|
||||
}
|
||||
|
||||
if patch.Name != nil {
|
||||
*patch.Name = strings.TrimSpace(*patch.Name)
|
||||
}
|
||||
if err := patch.IsValid(); err != nil {
|
||||
if appErr, ok := err.(*model.AppError); ok {
|
||||
c.Err = appErr
|
||||
} else {
|
||||
c.Err = model.NewAppError("createCPAField", "api.custom_profile_attributes.invalid_field_patch", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventPatchCPAField, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "property_field_patch", patch)
|
||||
|
||||
originalField, appErr := c.App.GetCPAField(c.Params.FieldId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventPriorState(originalField)
|
||||
|
||||
patchedField, appErr := c.App.PatchCPAField(c.Params.FieldId, patch)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(patchedField)
|
||||
auditRec.AddEventObjectType("property_field")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(patchedField); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func deleteCPAField(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
if !model.MinimumEnterpriseLicense(c.App.Channels().License()) {
|
||||
c.Err = model.NewAppError("Api4.deleteCPAField", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireFieldId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventDeleteCPAField, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "field_id", c.Params.FieldId)
|
||||
|
||||
field, appErr := c.App.GetCPAField(c.Params.FieldId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
auditRec.AddEventPriorState(field)
|
||||
|
||||
if appErr := c.App.DeleteCPAField(c.Params.FieldId); appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(field)
|
||||
auditRec.AddEventObjectType("property_field")
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func getCPAGroup(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !model.MinimumEnterpriseLicense(c.App.Channels().License()) {
|
||||
c.Err = model.NewAppError("Api4.getCPAGroup", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
groupID, err := c.App.CpaGroupID()
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getCPAGroup", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(map[string]string{"id": groupID}); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func patchCPAValues(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !model.MinimumEnterpriseLicense(c.App.Channels().License()) {
|
||||
c.Err = model.NewAppError("Api4.patchCPAValues", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
userID := c.AppContext.Session().UserId
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), userID) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]json.RawMessage
|
||||
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
|
||||
c.SetInvalidParamWithErr("value", err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventPatchCPAValues, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "user_id", userID)
|
||||
|
||||
// if the user is not an admin, we need to check that there are no
|
||||
// admin-managed fields
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
fields, appErr := c.App.ListCPAFields()
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
// Check if any of the fields being updated are admin-managed
|
||||
for _, field := range fields {
|
||||
if _, isBeingUpdated := updates[field.ID]; isBeingUpdated {
|
||||
// Convert to CPAField to check if managed
|
||||
cpaField, fErr := model.NewCPAFieldFromPropertyField(field)
|
||||
if fErr != nil {
|
||||
c.Err = model.NewAppError("Api4.patchCPAValues", "app.custom_profile_attributes.property_field_conversion.app_error", nil, "", http.StatusInternalServerError).Wrap(fErr)
|
||||
return
|
||||
}
|
||||
if cpaField.IsAdminManaged() {
|
||||
c.Err = model.NewAppError("Api4.patchCPAValues", "app.custom_profile_attributes.property_field_is_managed.app_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results := make(map[string]json.RawMessage, len(updates))
|
||||
for fieldID, rawValue := range updates {
|
||||
patchedValue, appErr := c.App.PatchCPAValue(userID, fieldID, rawValue, false)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
results[fieldID] = patchedValue.Value
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventObjectType("patchCPAValues")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(results); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func listCPAValues(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !model.MinimumEnterpriseLicense(c.App.Channels().License()) {
|
||||
c.Err = model.NewAppError("Api4.listCPAValues", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
userID := c.Params.UserId
|
||||
// we check unrestricted sessions to allow local mode requests to go through
|
||||
if !c.AppContext.Session().IsUnrestricted() {
|
||||
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext, c.AppContext.Session().UserId, userID)
|
||||
if err != nil || !canSee {
|
||||
c.SetPermissionError(model.PermissionViewMembers)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
values, appErr := c.App.ListCPAValues(userID)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
returnValue := make(map[string]json.RawMessage)
|
||||
for _, value := range values {
|
||||
returnValue[value.FieldID] = value.Value
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(returnValue); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func patchCPAValuesForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !model.MinimumEnterpriseLicense(c.App.Channels().License()) {
|
||||
c.Err = model.NewAppError("Api4.patchCPAValues", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Get userID from URL
|
||||
c.RequireUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
userID := c.Params.UserId
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), userID) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]json.RawMessage
|
||||
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
|
||||
c.SetInvalidParamWithErr("value", err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventPatchCPAValues, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "user_id", userID)
|
||||
|
||||
// if the user is not an admin, we need to check that there are no
|
||||
// admin-managed fields
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
fields, appErr := c.App.ListCPAFields()
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
// Check if any of the fields being updated are admin-managed
|
||||
for _, field := range fields {
|
||||
if _, isBeingUpdated := updates[field.ID]; isBeingUpdated {
|
||||
// Convert to CPAField to check if managed
|
||||
cpaField, fErr := model.NewCPAFieldFromPropertyField(field)
|
||||
if fErr != nil {
|
||||
c.Err = model.NewAppError("Api4.patchCPAValues", "app.custom_profile_attributes.property_field_conversion.app_error", nil, "", http.StatusInternalServerError).Wrap(fErr)
|
||||
return
|
||||
}
|
||||
if cpaField.IsAdminManaged() {
|
||||
c.Err = model.NewAppError("Api4.patchCPAValues", "app.custom_profile_attributes.property_field_is_managed.app_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results := make(map[string]json.RawMessage, len(updates))
|
||||
for fieldID, rawValue := range updates {
|
||||
patchedValue, appErr := c.App.PatchCPAValue(userID, fieldID, rawValue, false)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
results[fieldID] = patchedValue.Value
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventObjectType("patchCPAValues")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(results); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
18
channels/api4/custom_profile_attributes_local.go
Normal file
18
channels/api4/custom_profile_attributes_local.go
Normal file
@ -0,0 +1,18 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import "net/http"
|
||||
|
||||
func (api *API) InitCustomProfileAttributesLocal() {
|
||||
if api.srv.Config().FeatureFlags.CustomProfileAttributes {
|
||||
api.BaseRoutes.CustomProfileAttributesFields.Handle("", api.APILocal(listCPAFields)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.CustomProfileAttributesFields.Handle("", api.APILocal(createCPAField)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APILocal(patchCPAField)).Methods(http.MethodPatch)
|
||||
api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APILocal(deleteCPAField)).Methods(http.MethodDelete)
|
||||
api.BaseRoutes.User.Handle("/custom_profile_attributes", api.APILocal(listCPAValues)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.CustomProfileAttributesValues.Handle("", api.APILocal(patchCPAValues)).Methods(http.MethodPatch)
|
||||
api.BaseRoutes.User.Handle("/custom_profile_attributes", api.APILocal(patchCPAValuesForUser)).Methods(http.MethodPatch)
|
||||
}
|
||||
}
|
||||
1281
channels/api4/custom_profile_attributes_test.go
Normal file
1281
channels/api4/custom_profile_attributes_test.go
Normal file
File diff suppressed because it is too large
Load Diff
514
channels/api4/data_retention.go
Normal file
514
channels/api4/data_retention.go
Normal file
@ -0,0 +1,514 @@
|
||||
// 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/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitDataRetention() {
|
||||
api.BaseRoutes.DataRetention.Handle("/policy", api.APISessionRequired(getGlobalPolicy)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.DataRetention.Handle("/policies", api.APISessionRequired(getPolicies)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.DataRetention.Handle("/policies_count", api.APISessionRequired(getPoliciesCount)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.DataRetention.Handle("/policies", api.APISessionRequired(createPolicy)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}", api.APISessionRequired(getPolicy)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}", api.APISessionRequired(patchPolicy)).Methods(http.MethodPatch)
|
||||
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}", api.APISessionRequired(deletePolicy)).Methods(http.MethodDelete)
|
||||
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/teams", api.APISessionRequired(getTeamsForPolicy)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/teams", api.APISessionRequired(addTeamsToPolicy)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/teams", api.APISessionRequired(removeTeamsFromPolicy)).Methods(http.MethodDelete)
|
||||
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/teams/search", api.APISessionRequired(searchTeamsInPolicy)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/channels", api.APISessionRequired(getChannelsForPolicy)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/channels", api.APISessionRequired(addChannelsToPolicy)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/channels", api.APISessionRequired(removeChannelsFromPolicy)).Methods(http.MethodDelete)
|
||||
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/channels/search", api.APISessionRequired(searchChannelsInPolicy)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.User.Handle("/data_retention/team_policies", api.APISessionRequired(getTeamPoliciesForUser)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.User.Handle("/data_retention/channel_policies", api.APISessionRequired(getChannelPoliciesForUser)).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if _, err := w.Write(js); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if _, err := w.Write(js); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if _, err := w.Write(js); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
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(model.AuditEventCreatePolicy, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterAuditableToAuditRec(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)
|
||||
if _, err := w.Write(js); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
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(model.AuditEventPatchPolicy, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterAuditableToAuditRec(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()
|
||||
if _, err := w.Write(js); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func deletePolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequirePolicyId()
|
||||
policyId := c.Params.PolicyId
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventDeletePolicy, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(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
|
||||
}
|
||||
if _, err := w.Write(b); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
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.NewPointer(c.Params.PolicyId)
|
||||
props.IncludePolicyID = model.NewPointer(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
|
||||
}
|
||||
if _, err := w.Write(js); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func addTeamsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequirePolicyId()
|
||||
policyId := c.Params.PolicyId
|
||||
teamIDs, err := model.SortedArrayFromJSON(r.Body)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("addTeamsToPolicy", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventAddTeamsToPolicy, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "policy_id", policyId)
|
||||
model.AddEventParameterToAuditRec(auditRec, "team_ids", teamIDs)
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
|
||||
return
|
||||
}
|
||||
|
||||
appErr := c.App.AddTeamsToRetentionPolicy(policyId, teamIDs)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func removeTeamsFromPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequirePolicyId()
|
||||
policyId := c.Params.PolicyId
|
||||
teamIDs, err := model.SortedArrayFromJSON(r.Body)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("removeTeamsFromPolicy", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventRemoveTeamsFromPolicy, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "policy_id", policyId)
|
||||
model.AddEventParameterToAuditRec(auditRec, "team_ids", teamIDs)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
|
||||
return
|
||||
}
|
||||
|
||||
appErr := c.App.RemoveTeamsFromRetentionPolicy(policyId, teamIDs)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
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
|
||||
}
|
||||
if _, err := w.Write(b); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
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 || props == 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, "", http.StatusInternalServerError).Wrap(jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := w.Write(channelsJSON); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func addChannelsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequirePolicyId()
|
||||
policyId := c.Params.PolicyId
|
||||
channelIDs, err := model.SortedArrayFromJSON(r.Body)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("addChannelsToPolicy", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventAddChannelsToPolicy, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "policy_id", policyId)
|
||||
model.AddEventParameterToAuditRec(auditRec, "channel_ids", channelIDs)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
|
||||
return
|
||||
}
|
||||
|
||||
appErr := c.App.AddChannelsToRetentionPolicy(policyId, channelIDs)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func removeChannelsFromPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequirePolicyId()
|
||||
policyId := c.Params.PolicyId
|
||||
channelIDs, err := model.SortedArrayFromJSON(r.Body)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("removeChannelsFromPolicy", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventRemoveChannelsFromPolicy, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "policy_id", policyId)
|
||||
model.AddEventParameterToAuditRec(auditRec, "channel_ids", channelIDs)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
|
||||
return
|
||||
}
|
||||
|
||||
appErr := c.App.RemoveChannelsFromRetentionPolicy(policyId, channelIDs)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
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, "", http.StatusInternalServerError).Wrap(jsonErr)
|
||||
return
|
||||
}
|
||||
if _, err := w.Write(js); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
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, "", http.StatusInternalServerError).Wrap(jsonErr)
|
||||
return
|
||||
}
|
||||
if _, err := w.Write(js); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
1257
channels/api4/data_retention_test.go
Normal file
1257
channels/api4/data_retention_test.go
Normal file
File diff suppressed because it is too large
Load Diff
56
channels/api4/doc.go
Normal file
56
channels/api4/doc.go
Normal file
@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/*
|
||||
Package api4 implements the HTTP REST API layer for Mattermost server.
|
||||
|
||||
This package provides the primary interface between client applications
|
||||
(web, mobile, desktop) and the Mattermost server backend. It exposes
|
||||
HTTP endpoints that follow REST conventions for managing users, teams,
|
||||
channels, posts, and other Mattermost resources.
|
||||
|
||||
# Architecture
|
||||
|
||||
The API is structured around resource-based endpoints under the /api/v4/ path.
|
||||
Each endpoint is handled by specific handler functions that provide different
|
||||
levels of authentication and authorization:
|
||||
|
||||
- APIHandler: Public endpoints requiring no authentication
|
||||
- APISessionRequired: Endpoints requiring authenticated user sessions
|
||||
- APISessionRequiredTrustRequester: Authenticated endpoints for trusted requests
|
||||
- CloudAPIKeyRequired: Cloud installation webhook endpoints
|
||||
- RemoteClusterTokenRequired: Remote cluster communication endpoints
|
||||
- APILocal: Local mode access via UNIX socket
|
||||
|
||||
# Key Responsibilities
|
||||
|
||||
- Input validation: Validate request parameters and body content
|
||||
- Permission checks: Verify user has required permissions for the operation
|
||||
- HTTP handling: Parse requests, format responses, set appropriate status codes
|
||||
- Error formatting: Convert app layer errors to appropriate HTTP responses
|
||||
- Audit logging: Log security-relevant operations
|
||||
|
||||
# Error Handling
|
||||
|
||||
The API uses consistent error responses with appropriate HTTP status codes.
|
||||
All handlers use the Context object for standardized error reporting and
|
||||
audit logging. Errors are returned in a structured JSON format with
|
||||
error codes, messages, and additional context when available.
|
||||
|
||||
# Security
|
||||
|
||||
Security is implemented through multiple layers:
|
||||
|
||||
- Authentication via sessions, tokens, or API keys
|
||||
- Role-based access control and permission checking
|
||||
- CSRF protection through request validation
|
||||
- Rate limiting to prevent abuse
|
||||
- Multi-factor authentication support
|
||||
- Secure session management
|
||||
|
||||
The api4 package serves as the HTTP interface layer in Mattermost's
|
||||
layered architecture, providing a stable, versioned API for client
|
||||
applications while maintaining clear separation from business logic
|
||||
and data persistence concerns.
|
||||
*/
|
||||
package api4
|
||||
144
channels/api4/drafts.go
Normal file
144
channels/api4/drafts.go
Normal file
@ -0,0 +1,144 @@
|
||||
// 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/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitDrafts() {
|
||||
api.BaseRoutes.Drafts.Handle("", api.APISessionRequired(upsertDraft)).Methods(http.MethodPost)
|
||||
|
||||
api.BaseRoutes.TeamForUser.Handle("/drafts", api.APISessionRequired(getDrafts)).Methods(http.MethodGet)
|
||||
|
||||
api.BaseRoutes.ChannelForUser.Handle("/drafts/{thread_id:[A-Za-z0-9]+}", api.APISessionRequired(deleteDraft)).Methods(http.MethodDelete)
|
||||
api.BaseRoutes.ChannelForUser.Handle("/drafts", api.APISessionRequired(deleteDraft)).Methods(http.MethodDelete)
|
||||
}
|
||||
|
||||
func upsertDraft(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !*c.App.Config().ServiceSettings.AllowSyncedDrafts {
|
||||
c.Err = model.NewAppError("upsertDraft", "api.drafts.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
var draft model.Draft
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&draft); jsonErr != nil {
|
||||
c.SetInvalidParam("draft")
|
||||
return
|
||||
}
|
||||
|
||||
draft.DeleteAt = 0
|
||||
draft.UserId = c.AppContext.Session().UserId
|
||||
connectionID := r.Header.Get(model.ConnectionId)
|
||||
|
||||
hasPermission := false
|
||||
|
||||
if c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), draft.ChannelId, model.PermissionCreatePost) {
|
||||
hasPermission = true
|
||||
} else if channel, err := c.App.GetChannel(c.AppContext, draft.ChannelId); err == nil {
|
||||
// Temporary permission check method until advanced permissions, please do not copy
|
||||
if channel.Type == model.ChannelTypeOpen && c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionCreatePostPublic) {
|
||||
hasPermission = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasPermission {
|
||||
c.SetPermissionError(model.PermissionCreatePost)
|
||||
return
|
||||
}
|
||||
|
||||
dt, err := c.App.UpsertDraft(c.AppContext, &draft, connectionID)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(dt); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getDrafts(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !*c.App.Config().ServiceSettings.AllowSyncedDrafts {
|
||||
c.Err = model.NewAppError("getDrafts", "api.drafts.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
hasPermission := false
|
||||
|
||||
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
|
||||
hasPermission = true
|
||||
}
|
||||
|
||||
if !hasPermission {
|
||||
c.SetPermissionError(model.PermissionCreatePost)
|
||||
return
|
||||
}
|
||||
|
||||
drafts, err := c.App.GetDraftsForUser(c.AppContext, c.AppContext.Session().UserId, c.Params.TeamId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(drafts); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func deleteDraft(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !*c.App.Config().ServiceSettings.AllowSyncedDrafts {
|
||||
c.Err = model.NewAppError("deleteDraft", "api.drafts.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
rootID := ""
|
||||
|
||||
connectionID := r.Header.Get(model.ConnectionId)
|
||||
|
||||
if c.Params.ThreadId != "" {
|
||||
rootID = c.Params.ThreadId
|
||||
}
|
||||
|
||||
userID := c.AppContext.Session().UserId
|
||||
channelID := c.Params.ChannelId
|
||||
|
||||
draft, err := c.App.GetDraft(userID, channelID, rootID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case err.StatusCode == http.StatusNotFound:
|
||||
// If the draft doesn't exist in the server, we don't need to delete.
|
||||
ReturnStatusOK(w)
|
||||
default:
|
||||
c.Err = err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if c.AppContext.Session().UserId != draft.UserId {
|
||||
c.SetPermissionError(model.PermissionDeletePost)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.DeleteDraft(c.AppContext, draft, connectionID); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
214
channels/api4/drafts_test.go
Normal file
214
channels/api4/drafts_test.go
Normal file
@ -0,0 +1,214 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils/testutils"
|
||||
)
|
||||
|
||||
func TestUpsertDraft(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
// set config
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
|
||||
|
||||
client := th.Client
|
||||
channel := th.BasicChannel
|
||||
user := th.BasicUser
|
||||
|
||||
draft := &model.Draft{
|
||||
CreateAt: 12345,
|
||||
UpdateAt: 12345,
|
||||
UserId: user.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "original",
|
||||
}
|
||||
|
||||
// try to upsert draft
|
||||
draftResp, _, err := client.UpsertDraft(context.Background(), draft)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, draft.UserId, draftResp.UserId)
|
||||
assert.Equal(t, draft.Message, draftResp.Message)
|
||||
assert.Equal(t, draft.ChannelId, draftResp.ChannelId)
|
||||
|
||||
// upload file
|
||||
sent, err := testutils.ReadTestFile("test.png")
|
||||
require.NoError(t, err)
|
||||
|
||||
fileResp, _, err := client.UploadFile(context.Background(), sent, channel.Id, "test.png")
|
||||
require.NoError(t, err)
|
||||
|
||||
draftWithFiles := draft
|
||||
draftWithFiles.FileIds = []string{fileResp.FileInfos[0].Id}
|
||||
|
||||
// try to upsert draft with file
|
||||
draftResp, _, err = client.UpsertDraft(context.Background(), draftWithFiles)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, draftWithFiles.UserId, draftResp.UserId)
|
||||
assert.Equal(t, draftWithFiles.Message, draftResp.Message)
|
||||
assert.Equal(t, draftWithFiles.ChannelId, draftResp.ChannelId)
|
||||
assert.ElementsMatch(t, draftWithFiles.FileIds, draftResp.FileIds)
|
||||
|
||||
// try to upsert draft for invalid channel
|
||||
draftInvalidChannel := draft
|
||||
draftInvalidChannel.ChannelId = "12345"
|
||||
|
||||
_, resp, err := client.UpsertDraft(context.Background(), draft)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
// try to upsert draft without config setting set to true
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = false })
|
||||
|
||||
_, resp, err = client.UpsertDraft(context.Background(), draft)
|
||||
require.Error(t, err)
|
||||
CheckNotImplementedStatus(t, resp)
|
||||
}
|
||||
|
||||
func TestGetDrafts(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
|
||||
|
||||
client := th.Client
|
||||
channel1 := th.BasicChannel
|
||||
channel2 := th.BasicChannel2
|
||||
user := th.BasicUser
|
||||
team := th.BasicTeam
|
||||
|
||||
draft1 := &model.Draft{
|
||||
CreateAt: 00001,
|
||||
UserId: user.Id,
|
||||
ChannelId: channel1.Id,
|
||||
Message: "draft1",
|
||||
}
|
||||
|
||||
draft2 := &model.Draft{
|
||||
CreateAt: 11111,
|
||||
UserId: user.Id,
|
||||
ChannelId: channel2.Id,
|
||||
Message: "draft2",
|
||||
}
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
|
||||
|
||||
// upsert draft1
|
||||
_, _, err := client.UpsertDraft(context.Background(), draft1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait a bit so the second draft gets a newer UpdateAt
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// upsert draft2
|
||||
_, _, err = client.UpsertDraft(context.Background(), draft2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// try to get drafts
|
||||
draftResp, _, err := client.GetDrafts(context.Background(), user.Id, team.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, draft2.UserId, draftResp[0].UserId)
|
||||
assert.Equal(t, draft2.Message, draftResp[0].Message)
|
||||
assert.Equal(t, draft2.ChannelId, draftResp[0].ChannelId)
|
||||
|
||||
assert.Equal(t, draft1.UserId, draftResp[1].UserId)
|
||||
assert.Equal(t, draft1.Message, draftResp[1].Message)
|
||||
assert.Equal(t, draft1.ChannelId, draftResp[1].ChannelId)
|
||||
|
||||
assert.Len(t, draftResp, 2)
|
||||
|
||||
// try to get drafts on invalid team
|
||||
_, resp, err := client.GetDrafts(context.Background(), user.Id, "12345")
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
// try to get drafts when config is turned off
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = false })
|
||||
_, resp, err = client.GetDrafts(context.Background(), user.Id, team.Id)
|
||||
require.Error(t, err)
|
||||
CheckNotImplementedStatus(t, resp)
|
||||
}
|
||||
|
||||
func TestDeleteDraft(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
|
||||
|
||||
client := th.Client
|
||||
channel1 := th.BasicChannel
|
||||
channel2 := th.BasicChannel2
|
||||
user := th.BasicUser
|
||||
team := th.BasicTeam
|
||||
|
||||
draft1 := &model.Draft{
|
||||
CreateAt: 00001,
|
||||
UserId: user.Id,
|
||||
ChannelId: channel1.Id,
|
||||
Message: "draft1",
|
||||
RootId: "",
|
||||
}
|
||||
|
||||
draft2 := &model.Draft{
|
||||
CreateAt: 11111,
|
||||
UserId: user.Id,
|
||||
ChannelId: channel2.Id,
|
||||
Message: "draft2",
|
||||
RootId: model.NewId(),
|
||||
}
|
||||
|
||||
// upsert draft1
|
||||
_, _, err := client.UpsertDraft(context.Background(), draft1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait a bit so the second draft gets a newer UpdateAt
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// upsert draft2
|
||||
_, _, err = client.UpsertDraft(context.Background(), draft2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// get drafts
|
||||
draftResp, _, err := client.GetDrafts(context.Background(), user.Id, team.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, draft2.UserId, draftResp[0].UserId)
|
||||
assert.Equal(t, draft2.Message, draftResp[0].Message)
|
||||
assert.Equal(t, draft2.ChannelId, draftResp[0].ChannelId)
|
||||
|
||||
assert.Equal(t, draft1.UserId, draftResp[1].UserId)
|
||||
assert.Equal(t, draft1.Message, draftResp[1].Message)
|
||||
assert.Equal(t, draft1.ChannelId, draftResp[1].ChannelId)
|
||||
|
||||
// try to delete draft1
|
||||
_, _, err = client.DeleteDraft(context.Background(), user.Id, channel1.Id, draft1.RootId)
|
||||
require.NoError(t, err)
|
||||
|
||||
// get drafts
|
||||
draftResp, _, err = client.GetDrafts(context.Background(), user.Id, team.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, draft2.UserId, draftResp[0].UserId)
|
||||
assert.Equal(t, draft2.Message, draftResp[0].Message)
|
||||
assert.Equal(t, draft2.ChannelId, draftResp[0].ChannelId)
|
||||
assert.Len(t, draftResp, 1)
|
||||
}
|
||||
72
channels/api4/elasticsearch.go
Normal file
72
channels/api4/elasticsearch.go
Normal file
@ -0,0 +1,72 @@
|
||||
// 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/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitElasticsearch() {
|
||||
api.BaseRoutes.Elasticsearch.Handle("/test", api.APISessionRequired(testElasticsearch)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Elasticsearch.Handle("/purge_indexes", api.APISessionRequired(purgeElasticsearchIndexes)).Methods(http.MethodPost)
|
||||
}
|
||||
|
||||
func testElasticsearch(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var cfg *model.Config
|
||||
err := json.NewDecoder(r.Body).Decode(&cfg)
|
||||
if err != nil {
|
||||
c.Logger.Warn("Error decoding config.", mlog.Err(err))
|
||||
}
|
||||
if cfg == nil {
|
||||
cfg = c.App.Config()
|
||||
}
|
||||
|
||||
// we set BulkIndexingTimeWindowSeconds to a random value to avoid failing on the nil check
|
||||
// TODO: remove this hack once we remove BulkIndexingTimeWindowSeconds from the config.
|
||||
if cfg.ElasticsearchSettings.BulkIndexingTimeWindowSeconds == nil {
|
||||
cfg.ElasticsearchSettings.BulkIndexingTimeWindowSeconds = model.NewPointer(0)
|
||||
}
|
||||
if checkHasNilFields(&cfg.ElasticsearchSettings) {
|
||||
c.Err = model.NewAppError("testElasticsearch", "api.elasticsearch.test_elasticsearch_settings_nil.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// PERMISSION_TEST_ELASTICSEARCH is an ancillary permission of PERMISSION_SYSCONSOLE_WRITE_ENVIRONMENT_ELASTICSEARCH,
|
||||
// which should prevent read-only managers from password sniffing
|
||||
if !c.App.SessionHasPermissionToAndNotRestrictedAdmin(*c.AppContext.Session(), model.PermissionTestElasticsearch) {
|
||||
c.SetPermissionError(model.PermissionTestElasticsearch)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.TestElasticsearch(c.AppContext, cfg); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func purgeElasticsearchIndexes(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventPurgeElasticsearchIndexes, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if !c.App.SessionHasPermissionToAndNotRestrictedAdmin(*c.AppContext.Session(), model.PermissionPurgeElasticsearchIndexes) {
|
||||
c.SetPermissionError(model.PermissionPurgeElasticsearchIndexes)
|
||||
return
|
||||
}
|
||||
|
||||
specifiedIndexesQuery := r.URL.Query()["index"]
|
||||
if err := c.App.PurgeElasticsearchIndexes(c.AppContext, specifiedIndexesQuery); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
83
channels/api4/elasticsearch_test.go
Normal file
83
channels/api4/elasticsearch_test.go
Normal file
@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func TestElasticsearchTest(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("as system user", func(t *testing.T) {
|
||||
resp, err := th.Client.TestElasticsearch(context.Background())
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("as system admin", func(t *testing.T) {
|
||||
resp, err := th.SystemAdminClient.TestElasticsearch(context.Background())
|
||||
require.Error(t, err)
|
||||
CheckNotImplementedStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("invalid config", func(t *testing.T) {
|
||||
cfg := &model.Config{}
|
||||
cfg.SetDefaults()
|
||||
cfg.ElasticsearchSettings.Password = nil
|
||||
|
||||
data, err := json.Marshal(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := th.SystemAdminClient.DoAPIPost(context.Background(), "/elasticsearch/test", string(data))
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("as restricted system admin", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.ElasticsearchSettings.SetDefaults()
|
||||
*cfg.ExperimentalSettings.RestrictSystemAdmin = true
|
||||
})
|
||||
|
||||
resp, err := th.SystemAdminClient.TestElasticsearch(context.Background())
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
}
|
||||
|
||||
func TestElasticsearchPurgeIndexes(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("as system user", func(t *testing.T) {
|
||||
resp, err := th.Client.PurgeElasticsearchIndexes(context.Background())
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("as system admin", func(t *testing.T) {
|
||||
resp, err := th.SystemAdminClient.PurgeElasticsearchIndexes(context.Background())
|
||||
require.Error(t, err)
|
||||
CheckNotImplementedStatus(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.PurgeElasticsearchIndexes(context.Background())
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
}
|
||||
342
channels/api4/emoji.go
Normal file
342
channels/api4/emoji.go
Normal file
@ -0,0 +1,342 @@
|
||||
// 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/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/web"
|
||||
)
|
||||
|
||||
const (
|
||||
EmojiMaxAutocompleteItems = 100
|
||||
GetEmojisByNamesMax = 200
|
||||
)
|
||||
|
||||
func (api *API) InitEmoji() {
|
||||
api.BaseRoutes.Emojis.Handle("", api.APISessionRequired(createEmoji, handlerParamFileAPI)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Emojis.Handle("", api.APISessionRequired(getEmojiList)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Emojis.Handle("/names", api.APISessionRequired(getEmojisByNames)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Emojis.Handle("/search", api.APISessionRequired(searchEmojis)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Emojis.Handle("/autocomplete", api.APISessionRequired(autocompleteEmojis)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Emoji.Handle("", api.APISessionRequired(deleteEmoji)).Methods(http.MethodDelete)
|
||||
api.BaseRoutes.Emoji.Handle("", api.APISessionRequired(getEmoji)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.EmojiByName.Handle("", api.APISessionRequired(getEmojiByName)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Emoji.Handle("/image", api.APISessionRequiredTrustRequester(getEmojiImage)).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
func createEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if _, err := io.Copy(io.Discard, r.Body); err != nil {
|
||||
c.Logger.Warn("Error while discarding request body", mlog.Err(err))
|
||||
}
|
||||
}()
|
||||
|
||||
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, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventCreateEmoji, model.AuditStatusFail)
|
||||
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, 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(model.AuditEventDeleteEmoji, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
emoji, err := c.App.GetEmoji(c.AppContext, c.Params.EmojiId)
|
||||
if err != nil {
|
||||
model.AddEventParameterToAuditRec(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, 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
|
||||
}
|
||||
|
||||
if !*c.App.Config().ServiceSettings.EnableCustomEmoji {
|
||||
c.Err = model.NewAppError("getEmojiByName", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
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 getEmojisByNames(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
names, err := model.SortedArrayFromJSON(r.Body)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getEmojisByNames", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
} else if len(names) == 0 {
|
||||
c.SetInvalidParam("names")
|
||||
return
|
||||
}
|
||||
|
||||
if !*c.App.Config().ServiceSettings.EnableCustomEmoji {
|
||||
c.Err = model.NewAppError("getEmojisByNames", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
if len(names) > GetEmojisByNamesMax {
|
||||
c.Err = model.NewAppError("getEmojisByNames", "api.emoji.get_multiple_by_name_too_many.request_error", map[string]any{
|
||||
"MaxNames": GetEmojisByNamesMax,
|
||||
}, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
emojis, appErr := c.App.GetMultipleEmojiByName(c.AppContext, names)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(emojis); 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")
|
||||
if _, err := w.Write(image); err != nil {
|
||||
c.Logger.Warn("Error while writing image response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
891
channels/api4/emoji_test.go
Normal file
891
channels/api4/emoji_test.go
Normal file
@ -0,0 +1,891 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
|
||||
)
|
||||
|
||||
func TestCreateEmoji(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
client := th.Client
|
||||
|
||||
EnableCustomEmoji := *th.App.Config().ServiceSettings.EnableCustomEmoji
|
||||
defer func() {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = EnableCustomEmoji })
|
||||
}()
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = false })
|
||||
|
||||
defaultRolePermissions := th.SaveDefaultRolePermissions()
|
||||
defer func() {
|
||||
th.RestoreDefaultRolePermissions(defaultRolePermissions)
|
||||
}()
|
||||
|
||||
// constants to be used along with checkEmojiFile
|
||||
emojiWidth := app.MaxEmojiWidth
|
||||
emojiHeight := app.MaxEmojiHeight * 2
|
||||
// check that emoji gets resized correctly, respecting proportions, and is of expected type
|
||||
checkEmojiFile := func(id, expectedImageType string) {
|
||||
path := *th.App.Config().FileSettings.Directory
|
||||
file, fileErr := os.Open(filepath.Join(path, "/emoji/"+id+"/image"))
|
||||
require.NoError(t, fileErr)
|
||||
defer file.Close()
|
||||
config, imageType, err := image.DecodeConfig(file)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedImageType, imageType)
|
||||
require.Equal(t, emojiWidth/2, config.Width)
|
||||
require.Equal(t, emojiHeight/2, config.Height)
|
||||
}
|
||||
|
||||
emoji := &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
// try to create an emoji when they're disabled
|
||||
_, resp, err := client.CreateEmoji(context.Background(), emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
|
||||
require.Error(t, err)
|
||||
CheckNotImplementedStatus(t, resp)
|
||||
|
||||
// enable emoji creation for next cases
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
|
||||
|
||||
// try to create a valid gif emoji when they're enabled
|
||||
newEmoji, _, err := client.CreateEmoji(context.Background(), emoji, utils.CreateTestGif(t, emojiWidth, emojiHeight), "image.gif")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newEmoji.Name, emoji.Name, "create with wrong name")
|
||||
checkEmojiFile(newEmoji.Id, "gif")
|
||||
|
||||
// try to create an emoji with a duplicate name
|
||||
emoji2 := &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: newEmoji.Name,
|
||||
}
|
||||
_, resp, err = client.CreateEmoji(context.Background(), emoji2, utils.CreateTestGif(t, 10, 10), "image.gif")
|
||||
require.Error(t, err)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
CheckErrorID(t, err, "api.emoji.create.duplicate.app_error")
|
||||
|
||||
// try to create a valid animated gif emoji
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
newEmoji, _, err = client.CreateEmoji(context.Background(), emoji, utils.CreateTestAnimatedGif(t, emojiWidth, emojiHeight, 10), "image.gif")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newEmoji.Name, emoji.Name, "create with wrong name")
|
||||
checkEmojiFile(newEmoji.Id, "gif")
|
||||
|
||||
// try to create a valid webp emoji
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
path, _ := fileutils.FindDir("tests")
|
||||
bytes, err := os.ReadFile(filepath.Join(path, "testwebp.webp"))
|
||||
require.NoError(t, err)
|
||||
newEmoji, _, err = client.CreateEmoji(context.Background(), emoji, bytes, "image.webp")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newEmoji.Name, emoji.Name, "create with wrong name")
|
||||
checkEmojiFile(newEmoji.Id, "png") // emoji must be converted from webp to png
|
||||
|
||||
// try to create a valid jpeg emoji
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
newEmoji, _, err = client.CreateEmoji(context.Background(), emoji, utils.CreateTestJpeg(t, emojiWidth, emojiHeight), "image.jpeg")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newEmoji.Name, emoji.Name, "create with wrong name")
|
||||
checkEmojiFile(newEmoji.Id, "png") // emoji must be converted from jpeg to png
|
||||
|
||||
// try to create a valid png emoji
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
newEmoji, _, err = client.CreateEmoji(context.Background(), emoji, utils.CreateTestPng(t, emojiWidth, emojiHeight), "image.png")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newEmoji.Name, emoji.Name, "create with wrong name")
|
||||
checkEmojiFile(newEmoji.Id, "png")
|
||||
|
||||
// try to create an emoji that's too wide
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
newEmoji, _, err = client.CreateEmoji(context.Background(), emoji, utils.CreateTestGif(t, 1000, 10), "image.gif")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newEmoji.Name, emoji.Name, "create with wrong name")
|
||||
|
||||
// try to create an emoji that's too wide
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
_, _, err = client.CreateEmoji(context.Background(), emoji, utils.CreateTestGif(t, 10, app.MaxEmojiOriginalWidth+1), "image.gif")
|
||||
require.Error(t, err, "should fail - emoji is too wide")
|
||||
|
||||
// try to create an emoji that's too tall
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
_, _, err = client.CreateEmoji(context.Background(), emoji, utils.CreateTestGif(t, app.MaxEmojiOriginalHeight+1, 10), "image.gif")
|
||||
require.Error(t, err, "should fail - emoji is too tall")
|
||||
|
||||
// try to create an emoji that's too large
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
_, _, err = client.CreateEmoji(context.Background(), emoji, utils.CreateTestAnimatedGif(t, 100, 100, 10000), "image.gif")
|
||||
require.Error(t, err, "should fail - emoji is too big")
|
||||
|
||||
// try to create an emoji with data that isn't an image
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
_, resp, err = client.CreateEmoji(context.Background(), emoji, make([]byte, 100), "image.gif")
|
||||
require.Error(t, err)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
CheckErrorID(t, err, "api.emoji.upload.image.app_error")
|
||||
|
||||
// try to create an emoji as another user
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser2.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
_, resp, err = client.CreateEmoji(context.Background(), emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
// try to create an emoji without permissions
|
||||
th.RemovePermissionFromRole(model.PermissionCreateEmojis.Id, model.SystemUserRoleId)
|
||||
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
_, resp, err = client.CreateEmoji(context.Background(), emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
// create an emoji with permissions in one team
|
||||
th.AddPermissionToRole(model.PermissionCreateEmojis.Id, model.TeamUserRoleId)
|
||||
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
_, _, err = client.CreateEmoji(context.Background(), emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGetEmojiList(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
client := th.Client
|
||||
|
||||
EnableCustomEmoji := *th.App.Config().ServiceSettings.EnableCustomEmoji
|
||||
defer func() {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = EnableCustomEmoji })
|
||||
}()
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
|
||||
|
||||
t.Run("should return an empty array when there are no custom emoijs", func(t *testing.T) {
|
||||
listEmoji, _, err := client.GetEmojiList(context.Background(), 0, 100)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotEqual(t, nil, listEmoji)
|
||||
require.Equal(t, []*model.Emoji{}, listEmoji)
|
||||
})
|
||||
|
||||
emojis := []*model.Emoji{
|
||||
{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
},
|
||||
{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
},
|
||||
{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("should return an array of emojis", func(t *testing.T) {
|
||||
for idx, emoji := range emojis {
|
||||
newEmoji, _, err := client.CreateEmoji(context.Background(), emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
|
||||
require.NoError(t, err)
|
||||
emojis[idx] = newEmoji
|
||||
}
|
||||
|
||||
listEmoji, _, err := client.GetEmojiList(context.Background(), 0, 100)
|
||||
require.NoError(t, err)
|
||||
for _, emoji := range emojis {
|
||||
found := false
|
||||
for _, savedEmoji := range listEmoji {
|
||||
if emoji.Id == savedEmoji.Id {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.Truef(t, found, "failed to get emoji with id %v, %v", emoji.Id, len(listEmoji))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should return an empty array when past the maximum page count", func(t *testing.T) {
|
||||
listEmoji, _, err := client.GetEmojiList(context.Background(), 1, 100)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotEqual(t, nil, listEmoji)
|
||||
require.Equal(t, []*model.Emoji{}, listEmoji)
|
||||
})
|
||||
|
||||
t.Run("should not return a deleted emoji", func(t *testing.T) {
|
||||
_, err := client.DeleteEmoji(context.Background(), emojis[0].Id)
|
||||
require.NoError(t, err)
|
||||
listEmoji, _, err := client.GetEmojiList(context.Background(), 0, 100)
|
||||
require.NoError(t, err)
|
||||
found := false
|
||||
for _, savedEmoji := range listEmoji {
|
||||
if savedEmoji.Id == emojis[0].Id {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.Falsef(t, found, "should not get a deleted emoji %v", emojis[0].Id)
|
||||
})
|
||||
|
||||
t.Run("should return fewer results based on the provided page size", func(t *testing.T) {
|
||||
listEmoji, _, err := client.GetEmojiList(context.Background(), 0, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, listEmoji, 1, "should only return 1")
|
||||
})
|
||||
|
||||
t.Run("should return a sorted emoji list", func(t *testing.T) {
|
||||
listEmoji, _, err := client.GetSortedEmojiList(context.Background(), 0, 100, model.EmojiSortByName)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Greater(t, len(listEmoji), 0, "should return more than 0")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetEmojisByNames(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
// Set up some custom emojis
|
||||
adminClient := th.SystemAdminClient
|
||||
|
||||
imageBytes := utils.CreateTestJpeg(t, 10, 10)
|
||||
|
||||
emoji1 := &model.Emoji{
|
||||
CreatorId: th.SystemAdminUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
emoji1, _, err := adminClient.CreateEmoji(context.Background(), emoji1, imageBytes, "emoji.jpg")
|
||||
require.NoError(t, err)
|
||||
|
||||
emoji2 := &model.Emoji{
|
||||
CreatorId: th.SystemAdminUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
emoji2, _, err = adminClient.CreateEmoji(context.Background(), emoji2, imageBytes, "emoji.jpg")
|
||||
require.NoError(t, err)
|
||||
|
||||
client := th.Client
|
||||
|
||||
t.Run("should return a single emoji", func(t *testing.T) {
|
||||
emojis, _, err := client.GetEmojisByNames(context.Background(), []string{emoji1.Name})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, emojis, 1)
|
||||
assert.Equal(t, emoji1.Id, emojis[0].Id)
|
||||
})
|
||||
|
||||
t.Run("should return multiple emojis", func(t *testing.T) {
|
||||
emojis, _, err := client.GetEmojisByNames(context.Background(), []string{emoji1.Name, emoji2.Name})
|
||||
|
||||
var emojiIds []string
|
||||
for _, emoji := range emojis {
|
||||
emojiIds = append(emojiIds, emoji.Id)
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, emojis, 2)
|
||||
assert.Contains(t, emojiIds, emoji1.Id)
|
||||
assert.Contains(t, emojiIds, emoji2.Id)
|
||||
})
|
||||
|
||||
t.Run("should ignore non-existent emojis", func(t *testing.T) {
|
||||
emojis, _, err := client.GetEmojisByNames(context.Background(), []string{emoji1.Name, emoji2.Name, model.NewId()})
|
||||
var emojiIds []string
|
||||
for _, emoji := range emojis {
|
||||
emojiIds = append(emojiIds, emoji.Id)
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, emojis, 2)
|
||||
assert.Contains(t, emojiIds, emoji1.Id)
|
||||
assert.Contains(t, emojiIds, emoji2.Id)
|
||||
})
|
||||
|
||||
t.Run("should return an empty array when no emojis are found", func(t *testing.T) {
|
||||
emojis, _, err := client.GetEmojisByNames(context.Background(), []string{model.NewId(), model.NewId()})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, emojis)
|
||||
assert.Equal(t, []*model.Emoji{}, emojis)
|
||||
})
|
||||
|
||||
t.Run("should return an error when too many emojis are requested", func(t *testing.T) {
|
||||
names := make([]string, GetEmojisByNamesMax+1)
|
||||
for i := range names {
|
||||
names[i] = model.NewId()
|
||||
}
|
||||
|
||||
_, _, err := client.GetEmojisByNames(context.Background(), names)
|
||||
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteEmoji(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
client := th.Client
|
||||
|
||||
EnableCustomEmoji := *th.App.Config().ServiceSettings.EnableCustomEmoji
|
||||
defer func() {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = EnableCustomEmoji })
|
||||
}()
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
|
||||
|
||||
defaultRolePermissions := th.SaveDefaultRolePermissions()
|
||||
defer func() {
|
||||
th.RestoreDefaultRolePermissions(defaultRolePermissions)
|
||||
}()
|
||||
|
||||
emoji := &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
newEmoji, _, err := client.CreateEmoji(context.Background(), emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.DeleteEmoji(context.Background(), newEmoji.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = client.GetEmoji(context.Background(), newEmoji.Id)
|
||||
require.Error(t, err, "expected error fetching deleted emoji")
|
||||
|
||||
// Admin can delete other users emoji
|
||||
newEmoji, _, err = client.CreateEmoji(context.Background(), emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = th.SystemAdminClient.DeleteEmoji(context.Background(), newEmoji.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = th.SystemAdminClient.GetEmoji(context.Background(), newEmoji.Id)
|
||||
require.Error(t, err, "expected error fetching deleted emoji")
|
||||
|
||||
// Try to delete just deleted emoji
|
||||
resp, err := client.DeleteEmoji(context.Background(), newEmoji.Id)
|
||||
require.Error(t, err)
|
||||
CheckNotFoundStatus(t, resp)
|
||||
|
||||
// Try to delete non-existing emoji
|
||||
resp, err = client.DeleteEmoji(context.Background(), model.NewId())
|
||||
require.Error(t, err)
|
||||
CheckNotFoundStatus(t, resp)
|
||||
|
||||
// Try to delete without Id
|
||||
resp, err = client.DeleteEmoji(context.Background(), "")
|
||||
require.Error(t, err)
|
||||
CheckNotFoundStatus(t, resp)
|
||||
|
||||
// Try to delete my custom emoji without permissions
|
||||
newEmoji, _, err = client.CreateEmoji(context.Background(), emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
|
||||
require.NoError(t, err)
|
||||
|
||||
th.RemovePermissionFromRole(model.PermissionDeleteEmojis.Id, model.SystemUserRoleId)
|
||||
resp, err = client.DeleteEmoji(context.Background(), newEmoji.Id)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
th.AddPermissionToRole(model.PermissionDeleteEmojis.Id, model.SystemUserRoleId)
|
||||
|
||||
// Try to delete other user's custom emoji without DELETE_EMOJIS permissions
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
newEmoji, _, err = client.CreateEmoji(context.Background(), emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
|
||||
require.NoError(t, err)
|
||||
|
||||
th.RemovePermissionFromRole(model.PermissionDeleteEmojis.Id, model.SystemUserRoleId)
|
||||
th.AddPermissionToRole(model.PermissionDeleteOthersEmojis.Id, model.SystemUserRoleId)
|
||||
|
||||
_, err = client.Logout(context.Background())
|
||||
require.NoError(t, err)
|
||||
th.LoginBasic2()
|
||||
|
||||
resp, err = client.DeleteEmoji(context.Background(), newEmoji.Id)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
th.RemovePermissionFromRole(model.PermissionDeleteOthersEmojis.Id, model.SystemUserRoleId)
|
||||
th.AddPermissionToRole(model.PermissionDeleteEmojis.Id, model.SystemUserRoleId)
|
||||
|
||||
_, err = client.Logout(context.Background())
|
||||
require.NoError(t, err)
|
||||
th.LoginBasic()
|
||||
|
||||
// Try to delete other user's custom emoji without DELETE_OTHERS_EMOJIS permissions
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
newEmoji, _, err = client.CreateEmoji(context.Background(), emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.Logout(context.Background())
|
||||
require.NoError(t, err)
|
||||
th.LoginBasic2()
|
||||
|
||||
resp, err = client.DeleteEmoji(context.Background(), newEmoji.Id)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
_, err = client.Logout(context.Background())
|
||||
require.NoError(t, err)
|
||||
th.LoginBasic()
|
||||
|
||||
// Try to delete other user's custom emoji with permissions
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
newEmoji, _, err = client.CreateEmoji(context.Background(), emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
|
||||
require.NoError(t, err)
|
||||
|
||||
th.AddPermissionToRole(model.PermissionDeleteEmojis.Id, model.SystemUserRoleId)
|
||||
th.AddPermissionToRole(model.PermissionDeleteOthersEmojis.Id, model.SystemUserRoleId)
|
||||
|
||||
_, err = client.Logout(context.Background())
|
||||
require.NoError(t, err)
|
||||
th.LoginBasic2()
|
||||
|
||||
_, err = client.DeleteEmoji(context.Background(), newEmoji.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.Logout(context.Background())
|
||||
require.NoError(t, err)
|
||||
th.LoginBasic()
|
||||
|
||||
// Try to delete my custom emoji with permissions at team level
|
||||
newEmoji, _, err = client.CreateEmoji(context.Background(), emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
|
||||
require.NoError(t, err)
|
||||
|
||||
th.RemovePermissionFromRole(model.PermissionDeleteEmojis.Id, model.SystemUserRoleId)
|
||||
th.AddPermissionToRole(model.PermissionDeleteEmojis.Id, model.TeamUserRoleId)
|
||||
_, err = client.DeleteEmoji(context.Background(), newEmoji.Id)
|
||||
require.NoError(t, err)
|
||||
th.AddPermissionToRole(model.PermissionDeleteEmojis.Id, model.SystemUserRoleId)
|
||||
th.RemovePermissionFromRole(model.PermissionDeleteEmojis.Id, model.TeamUserRoleId)
|
||||
|
||||
// Try to delete other user's custom emoji with permissions at team level
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
newEmoji, _, err = client.CreateEmoji(context.Background(), emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
|
||||
require.NoError(t, err)
|
||||
|
||||
th.RemovePermissionFromRole(model.PermissionDeleteEmojis.Id, model.SystemUserRoleId)
|
||||
th.RemovePermissionFromRole(model.PermissionDeleteOthersEmojis.Id, model.SystemUserRoleId)
|
||||
|
||||
th.AddPermissionToRole(model.PermissionDeleteEmojis.Id, model.TeamUserRoleId)
|
||||
th.AddPermissionToRole(model.PermissionDeleteOthersEmojis.Id, model.TeamUserRoleId)
|
||||
|
||||
_, err = client.Logout(context.Background())
|
||||
require.NoError(t, err)
|
||||
th.LoginBasic2()
|
||||
|
||||
_, err = client.DeleteEmoji(context.Background(), newEmoji.Id)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGetEmoji(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
client := th.Client
|
||||
|
||||
EnableCustomEmoji := *th.App.Config().ServiceSettings.EnableCustomEmoji
|
||||
defer func() {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = EnableCustomEmoji })
|
||||
}()
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
|
||||
|
||||
emoji := &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
newEmoji, _, err := client.CreateEmoji(context.Background(), emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
|
||||
require.NoError(t, err)
|
||||
|
||||
emoji, _, err = client.GetEmoji(context.Background(), newEmoji.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newEmoji.Id, emoji.Id, "wrong emoji was returned")
|
||||
|
||||
_, resp, err := client.GetEmoji(context.Background(), model.NewId())
|
||||
require.Error(t, err)
|
||||
CheckNotFoundStatus(t, resp)
|
||||
}
|
||||
|
||||
func TestGetEmojiByName(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
client := th.Client
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
|
||||
|
||||
emoji := &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
newEmoji, _, err := client.CreateEmoji(context.Background(), emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
|
||||
require.NoError(t, err)
|
||||
|
||||
emoji, _, err = client.GetEmojiByName(context.Background(), newEmoji.Name)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, newEmoji.Name, emoji.Name)
|
||||
|
||||
_, resp, err := client.GetEmojiByName(context.Background(), model.NewId())
|
||||
require.Error(t, err)
|
||||
CheckNotFoundStatus(t, resp)
|
||||
|
||||
_, err = client.Logout(context.Background())
|
||||
require.NoError(t, err)
|
||||
_, resp, err = client.GetEmojiByName(context.Background(), newEmoji.Name)
|
||||
require.Error(t, err)
|
||||
CheckUnauthorizedStatus(t, resp)
|
||||
}
|
||||
|
||||
func TestGetEmojiImage(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
client := th.Client
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
|
||||
|
||||
emoji1 := &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
emoji1, _, err := client.CreateEmoji(context.Background(), emoji1, utils.CreateTestGif(t, 10, 10), "image.gif")
|
||||
require.NoError(t, err)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = false })
|
||||
|
||||
_, resp, err := client.GetEmojiImage(context.Background(), emoji1.Id)
|
||||
require.Error(t, err)
|
||||
CheckNotImplementedStatus(t, resp)
|
||||
CheckErrorID(t, err, "api.emoji.disabled.app_error")
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.DriverName = "local" })
|
||||
|
||||
emojiImage, _, err := client.GetEmojiImage(context.Background(), emoji1.Id)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, len(emojiImage), 0, "should return the image")
|
||||
|
||||
_, imageType, err := image.DecodeConfig(bytes.NewReader(emojiImage))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, imageType, "gif", "expected gif")
|
||||
|
||||
emoji2 := &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
emoji2, _, err = client.CreateEmoji(context.Background(), emoji2, utils.CreateTestAnimatedGif(t, 10, 10, 10), "image.gif")
|
||||
require.NoError(t, err)
|
||||
|
||||
emojiImage, _, err = client.GetEmojiImage(context.Background(), emoji2.Id)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, len(emojiImage), 0, "no image returned")
|
||||
|
||||
_, imageType, err = image.DecodeConfig(bytes.NewReader(emojiImage))
|
||||
require.NoError(t, err, "unable to identify received image")
|
||||
require.Equal(t, imageType, "gif", "expected gif")
|
||||
|
||||
emoji3 := &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
emoji3, _, err = client.CreateEmoji(context.Background(), emoji3, utils.CreateTestJpeg(t, 10, 10), "image.jpg")
|
||||
require.NoError(t, err)
|
||||
|
||||
emojiImage, _, err = client.GetEmojiImage(context.Background(), emoji3.Id)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, len(emojiImage), 0, "no image returned")
|
||||
|
||||
_, imageType, err = image.DecodeConfig(bytes.NewReader(emojiImage))
|
||||
require.NoError(t, err, "unable to identify received image")
|
||||
require.Equal(t, imageType, "jpeg", "expected jpeg")
|
||||
|
||||
emoji4 := &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
emoji4, _, err = client.CreateEmoji(context.Background(), emoji4, utils.CreateTestPng(t, 10, 10), "image.png")
|
||||
require.NoError(t, err)
|
||||
|
||||
emojiImage, _, err = client.GetEmojiImage(context.Background(), emoji4.Id)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, len(emojiImage), 0, "no image returned")
|
||||
|
||||
_, imageType, err = image.DecodeConfig(bytes.NewReader(emojiImage))
|
||||
require.NoError(t, err, "unable to identify received image")
|
||||
require.Equal(t, imageType, "png", "expected png")
|
||||
|
||||
_, err = client.DeleteEmoji(context.Background(), emoji4.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, resp, err = client.GetEmojiImage(context.Background(), emoji4.Id)
|
||||
require.Error(t, err)
|
||||
CheckNotFoundStatus(t, resp)
|
||||
|
||||
_, resp, err = client.GetEmojiImage(context.Background(), model.NewId())
|
||||
require.Error(t, err)
|
||||
CheckNotFoundStatus(t, resp)
|
||||
|
||||
_, resp, err = client.GetEmojiImage(context.Background(), "")
|
||||
require.Error(t, err)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
}
|
||||
|
||||
func TestSearchEmoji(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
client := th.Client
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
|
||||
|
||||
searchTerm1 := model.NewId()
|
||||
searchTerm2 := model.NewId()
|
||||
|
||||
emojis := []*model.Emoji{
|
||||
{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: searchTerm1,
|
||||
},
|
||||
{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: "blargh_" + searchTerm2,
|
||||
},
|
||||
}
|
||||
|
||||
for idx, emoji := range emojis {
|
||||
newEmoji, _, err := client.CreateEmoji(context.Background(), emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
|
||||
require.NoError(t, err)
|
||||
emojis[idx] = newEmoji
|
||||
}
|
||||
|
||||
t.Run("should return emojis based on the query", func(t *testing.T) {
|
||||
search := &model.EmojiSearch{Term: searchTerm1}
|
||||
remojis, resp, err := client.SearchEmoji(context.Background(), search)
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
|
||||
found := false
|
||||
for _, e := range remojis {
|
||||
if e.Name == emojis[0].Name {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, found)
|
||||
|
||||
search.Term = searchTerm2
|
||||
search.PrefixOnly = true
|
||||
remojis, resp, err = client.SearchEmoji(context.Background(), search)
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
|
||||
found = false
|
||||
for _, e := range remojis {
|
||||
if e.Name == emojis[1].Name {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.False(t, found)
|
||||
|
||||
search.PrefixOnly = false
|
||||
remojis, resp, err = client.SearchEmoji(context.Background(), search)
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
|
||||
found = false
|
||||
for _, e := range remojis {
|
||||
if e.Name == emojis[1].Name {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, found)
|
||||
})
|
||||
|
||||
t.Run("should return an empty array when no emojis match the query", func(t *testing.T) {
|
||||
search := &model.EmojiSearch{Term: model.NewId()}
|
||||
|
||||
remojis, _, err := client.SearchEmoji(context.Background(), search)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotEqual(t, nil, remojis)
|
||||
require.Equal(t, []*model.Emoji{}, remojis)
|
||||
})
|
||||
|
||||
t.Run("should return a 400 error when an empty term is passed", func(t *testing.T) {
|
||||
search := &model.EmojiSearch{Term: ""}
|
||||
|
||||
_, resp, err := client.SearchEmoji(context.Background(), search)
|
||||
require.Error(t, err)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("should return a 401 error when logged out", func(t *testing.T) {
|
||||
search := &model.EmojiSearch{Term: searchTerm1}
|
||||
|
||||
_, err := client.Logout(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, resp, err := client.SearchEmoji(context.Background(), search)
|
||||
require.Error(t, err)
|
||||
CheckUnauthorizedStatus(t, resp)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAutocompleteEmoji(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
client := th.Client
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
|
||||
|
||||
searchTerm1 := model.NewId()
|
||||
|
||||
emojis := []*model.Emoji{
|
||||
{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: searchTerm1,
|
||||
},
|
||||
{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: "blargh_" + searchTerm1,
|
||||
},
|
||||
}
|
||||
|
||||
for idx, emoji := range emojis {
|
||||
newEmoji, _, err := client.CreateEmoji(context.Background(), emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
|
||||
require.NoError(t, err)
|
||||
emojis[idx] = newEmoji
|
||||
}
|
||||
|
||||
t.Run("should return autocompleted emojis based on the search term", func(t *testing.T) {
|
||||
remojis, resp, err := client.AutocompleteEmoji(context.Background(), searchTerm1, "")
|
||||
require.NoErrorf(t, err, "AutocompleteEmoji failed with search term: %s", searchTerm1)
|
||||
CheckOKStatus(t, resp)
|
||||
|
||||
found1 := false
|
||||
found2 := false
|
||||
for _, e := range remojis {
|
||||
if e.Name == emojis[0].Name {
|
||||
found1 = true
|
||||
}
|
||||
|
||||
if e.Name == emojis[1].Name {
|
||||
found2 = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, found1)
|
||||
assert.False(t, found2)
|
||||
})
|
||||
|
||||
t.Run("should return an empty array when no emojis match the search term", func(t *testing.T) {
|
||||
remojis, resp, err := client.AutocompleteEmoji(context.Background(), model.NewId(), "")
|
||||
require.NoErrorf(t, err, "AutocompleteEmoji failed with search term: %s", searchTerm1)
|
||||
CheckOKStatus(t, resp)
|
||||
|
||||
require.NotEqual(t, nil, remojis)
|
||||
require.Equal(t, []*model.Emoji{}, remojis)
|
||||
})
|
||||
|
||||
t.Run("should return a 400 error when an empty term is passed", func(t *testing.T) {
|
||||
_, resp, err := client.AutocompleteEmoji(context.Background(), "", "")
|
||||
require.Error(t, err)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("should return a 401 error when logged out", func(t *testing.T) {
|
||||
_, err := client.Logout(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, resp, err := client.AutocompleteEmoji(context.Background(), searchTerm1, "")
|
||||
require.Error(t, err)
|
||||
CheckUnauthorizedStatus(t, resp)
|
||||
})
|
||||
}
|
||||
118
channels/api4/export.go
Normal file
118
channels/api4/export.go
Normal file
@ -0,0 +1,118 @@
|
||||
// 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/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitExport() {
|
||||
api.BaseRoutes.Exports.Handle("", api.APISessionRequired(listExports)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Export.Handle("", api.APISessionRequired(deleteExport)).Methods(http.MethodDelete)
|
||||
api.BaseRoutes.Export.Handle("", api.APISessionRequired(downloadExport)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Export.Handle("/presign-url", api.APISessionRequired(generatePresignURLExport)).Methods(http.MethodPost)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if _, err := w.Write(data); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func deleteExport(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventDeleteExport, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(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.ExportFileExists(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.ExportFileReader(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)
|
||||
}
|
||||
|
||||
func generatePresignURLExport(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventGeneratePresignURLExport, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
model.AddEventParameterToAuditRec(auditRec, "export_name", c.Params.ExportName)
|
||||
|
||||
if !c.IsSystemAdmin() {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
res, appErr := c.App.GeneratePresignURLForExport(c.Params.ExportName)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
data, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("generatePresignURLExport", "app.export.marshal.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
if _, err := w.Write(data); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
13
channels/api4/export_local.go
Normal file
13
channels/api4/export_local.go
Normal file
@ -0,0 +1,13 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import "net/http"
|
||||
|
||||
func (api *API) InitExportLocal() {
|
||||
api.BaseRoutes.Exports.Handle("", api.APILocal(listExports)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Export.Handle("", api.APILocal(deleteExport)).Methods(http.MethodDelete)
|
||||
api.BaseRoutes.Export.Handle("", api.APILocal(downloadExport)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Export.Handle("/presign-url", api.APILocal(generatePresignURLExport)).Methods(http.MethodPost)
|
||||
}
|
||||
235
channels/api4/export_test.go
Normal file
235
channels/api4/export_test.go
Normal file
@ -0,0 +1,235 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestListExports(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("no permissions", func(t *testing.T) {
|
||||
exports, _, err := th.Client.ListExports(context.Background())
|
||||
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(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, exports)
|
||||
}, "no exports")
|
||||
|
||||
dataDir := *th.App.Config().FileSettings.Directory
|
||||
|
||||
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 func() {
|
||||
err = os.RemoveAll(exportDir)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
f, err := os.Create(filepath.Join(exportDir, "export.zip"))
|
||||
require.NoError(t, err)
|
||||
err = f.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
exports, _, err := c.ListExports(context.Background())
|
||||
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) {
|
||||
originalExportDir := *th.App.Config().ExportSettings.Directory
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExportSettings.Directory = "new" })
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ExportSettings.Directory = originalExportDir
|
||||
})
|
||||
|
||||
exportDir := filepath.Join(dataDir, *th.App.Config().ExportSettings.Directory)
|
||||
err := os.Mkdir(exportDir, 0700)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
err = os.RemoveAll(exportDir)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
exports, _, err := c.ListExports(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, exports)
|
||||
|
||||
f, err := os.Create(filepath.Join(exportDir, "export.zip"))
|
||||
require.NoError(t, err)
|
||||
err = f.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
exports, _, err = c.ListExports(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, exports, 1)
|
||||
require.Equal(t, "export.zip", exports[0])
|
||||
}, "change export directory")
|
||||
}
|
||||
|
||||
func TestDeleteExport(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("no permissions", func(t *testing.T) {
|
||||
_, err := th.Client.DeleteExport(context.Background(), "export.zip")
|
||||
require.Error(t, err)
|
||||
CheckErrorID(t, err, "api.context.permissions.app_error")
|
||||
})
|
||||
|
||||
dataDir := *th.App.Config().FileSettings.Directory
|
||||
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 func() {
|
||||
err = os.RemoveAll(exportDir)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
exportName := "export.zip"
|
||||
f, err := os.Create(filepath.Join(exportDir, exportName))
|
||||
require.NoError(t, err)
|
||||
err = f.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
exports, _, err := c.ListExports(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, exports, 1)
|
||||
require.Equal(t, exports[0], exportName)
|
||||
|
||||
_, err = c.DeleteExport(context.Background(), exportName)
|
||||
require.NoError(t, err)
|
||||
|
||||
exports, _, err = c.ListExports(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, exports)
|
||||
|
||||
// verify idempotence
|
||||
_, err = c.DeleteExport(context.Background(), exportName)
|
||||
require.NoError(t, err)
|
||||
}, "successfully delete export")
|
||||
}
|
||||
|
||||
func TestDownloadExport(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("no permissions", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
n, _, err := th.Client.DownloadExport(context.Background(), "export.zip", &buf, 0)
|
||||
require.Error(t, err)
|
||||
CheckErrorID(t, err, "api.context.permissions.app_error")
|
||||
require.Zero(t, n)
|
||||
})
|
||||
|
||||
dataDir := *th.App.Config().FileSettings.Directory
|
||||
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(context.Background(), "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 func() {
|
||||
err = os.RemoveAll(exportDir)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
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(context.Background(), 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 func() {
|
||||
err = os.RemoveAll(exportDir)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
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(context.Background(), 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 := *th.App.Config().FileSettings.Directory
|
||||
exportDir := filepath.Join(dataDir, *th.App.Config().ExportSettings.Directory)
|
||||
|
||||
err := os.Mkdir(exportDir, 0700)
|
||||
require.NoError(b, err)
|
||||
defer func() {
|
||||
err = os.RemoveAll(exportDir)
|
||||
require.NoError(b, err)
|
||||
}()
|
||||
|
||||
exportName := "export.zip"
|
||||
f, err := os.Create(filepath.Join(exportDir, exportName))
|
||||
require.NoError(b, err)
|
||||
err = f.Close()
|
||||
require.NoError(b, err)
|
||||
|
||||
err = os.Truncate(filepath.Join(exportDir, exportName), 1024*1024*1024)
|
||||
require.NoError(b, err)
|
||||
|
||||
for i := 0; b.Loop(); i++ {
|
||||
outFilePath := filepath.Join(dataDir, fmt.Sprintf("export%d.zip", i))
|
||||
outFile, err := os.Create(outFilePath)
|
||||
require.NoError(b, err)
|
||||
_, _, err = th.SystemAdminClient.DownloadExport(context.Background(), exportName, outFile, 0)
|
||||
require.NoError(b, err)
|
||||
err = outFile.Close()
|
||||
require.NoError(b, err)
|
||||
err = os.Remove(outFilePath)
|
||||
require.NoError(b, err)
|
||||
}
|
||||
}
|
||||
907
channels/api4/file.go
Normal file
907
channels/api4/file.go
Normal file
@ -0,0 +1,907 @@
|
||||
// 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/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/shared/web"
|
||||
)
|
||||
|
||||
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, handlerParamFileAPI)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.File.Handle("", api.APISessionRequiredTrustRequester(getFile)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.File.Handle("/thumbnail", api.APISessionRequiredTrustRequester(getFileThumbnail)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.File.Handle("/link", api.APISessionRequired(getFileLink)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.File.Handle("/preview", api.APISessionRequiredTrustRequester(getFilePreview)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.File.Handle("/info", api.APISessionRequired(getFileInfo)).Methods(http.MethodGet)
|
||||
|
||||
api.BaseRoutes.Team.Handle("/files/search", api.APISessionRequiredDisableWhenBusy(searchFilesInTeam)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Files.Handle("/search", api.APISessionRequiredDisableWhenBusy(searchFilesInAllTeams)).Methods(http.MethodPost)
|
||||
|
||||
api.BaseRoutes.PublicFile.Handle("", api.APIHandler(getPublicFile)).Methods(http.MethodGet, http.MethodHead)
|
||||
}
|
||||
|
||||
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(model.AuditEventUploadFileSimple, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(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
|
||||
}
|
||||
|
||||
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("uploadFileSimple",
|
||||
"api.file.upload_file.get_channel.app_error",
|
||||
nil, err.Error(), http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
|
||||
restrictDM, appErr := c.App.CheckIfChannelIsRestrictedDM(c.AppContext, channel)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return nil
|
||||
}
|
||||
|
||||
if restrictDM {
|
||||
c.Err = model.NewAppError("uploadFileSimple", "api.file.upload_file.restricted_dm.error", nil, "", http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
|
||||
clientId := r.Form.Get("client_id")
|
||||
model.AddEventParameterToAuditRec(auditRec, "client_id", clientId)
|
||||
|
||||
creatorId := c.AppContext.Session().UserId
|
||||
if isBookmark, err := strconv.ParseBool(r.URL.Query().Get(model.BookmarkFileOwner)); err == nil && isBookmark {
|
||||
creatorId = model.BookmarkFileOwner
|
||||
model.AddEventParameterToAuditRec(auditRec, model.BookmarkFileOwner, true)
|
||||
}
|
||||
|
||||
info, appErr := c.App.UploadFileX(c.AppContext, c.Params.ChannelId, c.Params.Filename, r.Body,
|
||||
app.UploadFileSetTeamId(FileTeamId),
|
||||
app.UploadFileSetUserId(creatorId),
|
||||
app.UploadFileSetTimestamp(timestamp),
|
||||
app.UploadFileSetContentLength(r.ContentLength),
|
||||
app.UploadFileSetClientId(clientId))
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return nil
|
||||
}
|
||||
model.AddEventParameterAuditableToAuditRec(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
|
||||
}
|
||||
|
||||
isBookmark := false
|
||||
if val, queryErr := strconv.ParseBool(r.URL.Query().Get(model.BookmarkFileOwner)); queryErr == nil {
|
||||
isBookmark = val
|
||||
}
|
||||
|
||||
// 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, isBookmark)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
|
||||
if appErr != nil {
|
||||
c.Err = model.NewAppError("uploadFileMultipart",
|
||||
"api.file.upload_file.get_channel.app_error",
|
||||
nil, appErr.Error(), http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
|
||||
restrictDM, appErr := c.App.CheckIfChannelIsRestrictedDM(c.AppContext, channel)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return nil
|
||||
}
|
||||
|
||||
if restrictDM {
|
||||
c.Err = model.NewAppError("uploadFileSimple", "api.file.upload_file.restricted_dm.error", nil, "", http.StatusBadRequest)
|
||||
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(model.AuditEventUploadFileMultipart, model.AuditStatusFail)
|
||||
model.AddEventParameterToAuditRec(auditRec, "channel_id", c.Params.ChannelId)
|
||||
model.AddEventParameterToAuditRec(auditRec, "client_id", clientId)
|
||||
|
||||
creatorId := c.AppContext.Session().UserId
|
||||
if isBookmark {
|
||||
creatorId = model.BookmarkFileOwner
|
||||
model.AddEventParameterToAuditRec(auditRec, model.BookmarkFileOwner, true)
|
||||
}
|
||||
|
||||
info, appErr := c.App.UploadFileX(c.AppContext, c.Params.ChannelId, filename, part,
|
||||
app.UploadFileSetTeamId(FileTeamId),
|
||||
app.UploadFileSetUserId(creatorId),
|
||||
app.UploadFileSetTimestamp(timestamp),
|
||||
app.UploadFileSetContentLength(-1),
|
||||
app.UploadFileSetClientId(clientId))
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
c.LogAuditRec(auditRec)
|
||||
return nil
|
||||
}
|
||||
model.AddEventParameterAuditableToAuditRec(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, isBookmark bool) *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(model.AuditEventUploadFileMultipartLegacy, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "channel_id", channelId)
|
||||
model.AddEventParameterToAuditRec(auditRec, "client_id", clientId)
|
||||
|
||||
creatorId := c.AppContext.Session().UserId
|
||||
if isBookmark {
|
||||
creatorId = model.BookmarkFileOwner
|
||||
model.AddEventParameterToAuditRec(auditRec, model.BookmarkFileOwner, true)
|
||||
}
|
||||
|
||||
info, appErr := c.App.UploadFileX(c.AppContext, c.Params.ChannelId, fileHeader.Filename, f,
|
||||
app.UploadFileSetTeamId(FileTeamId),
|
||||
app.UploadFileSetUserId(creatorId),
|
||||
app.UploadFileSetTimestamp(timestamp),
|
||||
app.UploadFileSetContentLength(-1),
|
||||
app.UploadFileSetClientId(clientId))
|
||||
f.Close()
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
c.LogAuditRec(auditRec)
|
||||
return nil
|
||||
}
|
||||
model.AddEventParameterAuditableToAuditRec(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(model.AuditEventGetFile, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "force_download", forceDownload)
|
||||
|
||||
fileInfos, storeErr := c.App.Srv().Store().FileInfo().GetByIds([]string{c.Params.FileId}, true, true)
|
||||
if storeErr != nil {
|
||||
c.Err = model.NewAppError("getFile", "api.file.get_file_info.app_error", nil, "", http.StatusInternalServerError)
|
||||
setInaccessibleFileHeader(w, c.Err)
|
||||
return
|
||||
} else if len(fileInfos) == 0 {
|
||||
c.Err = model.NewAppError("getFile", "api.file.get_file_info.app_error", nil, "", http.StatusNotFound)
|
||||
setInaccessibleFileHeader(w, c.Err)
|
||||
return
|
||||
}
|
||||
|
||||
fileInfo := fileInfos[0]
|
||||
|
||||
channel, err := c.App.GetChannel(c.AppContext, fileInfo.ChannelId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
isContentReviewer := false
|
||||
asContentReviewer, _ := strconv.ParseBool(r.URL.Query().Get(model.AsContentReviewerParam))
|
||||
if asContentReviewer {
|
||||
requireContentFlaggingEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
flaggedPostId := r.URL.Query().Get("flagged_post_id")
|
||||
requireFlaggedPost(c, flaggedPostId)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if flaggedPostId != fileInfo.PostId {
|
||||
c.Err = model.NewAppError("getFile", "api.file.get_file.invalid_flagged_post.app_error", nil, "file_id="+fileInfo.Id+", flagged_post_id="+flaggedPostId, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
requireTeamContentReviewer(c, c.AppContext.Session().UserId, channel.TeamId)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
isContentReviewer = true
|
||||
}
|
||||
|
||||
// at this point we may have fetched a deleted file info and
|
||||
// if the user is not a content reviewer, the request should fail as
|
||||
// fetching deleted file info is only allowed for content reviewers of the specific post
|
||||
if fileInfo.DeleteAt != 0 && !isContentReviewer {
|
||||
c.Err = model.NewAppError("getFile", "app.file_info.get.app_error", nil, "", http.StatusNotFound)
|
||||
setInaccessibleFileHeader(w, c.Err)
|
||||
return
|
||||
}
|
||||
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "file", fileInfo)
|
||||
|
||||
if !isContentReviewer {
|
||||
perm := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
||||
if fileInfo.CreatorId == model.BookmarkFileOwner {
|
||||
if !perm {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
} else if fileInfo.CreatorId != c.AppContext.Session().UserId && !perm {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fileReader, err := c.App.FileReader(fileInfo.Path)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
c.Err.StatusCode = http.StatusNotFound
|
||||
return
|
||||
}
|
||||
defer fileReader.Close()
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
web.WriteFileResponse(fileInfo.Name, fileInfo.MimeType, fileInfo.Size, time.Unix(0, fileInfo.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.AppContext, c.Params.FileId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
setInaccessibleFileHeader(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
channel, err := c.App.GetChannel(c.AppContext, info.ChannelId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
perm := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
||||
if info.CreatorId == model.BookmarkFileOwner {
|
||||
if !perm {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
} else if info.CreatorId != c.AppContext.Session().UserId && !perm {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
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(model.AuditEventGetFileLink, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
info, err := c.App.GetFileInfo(c.AppContext, c.Params.FileId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
setInaccessibleFileHeader(w, err)
|
||||
return
|
||||
}
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "file", info)
|
||||
|
||||
channel, err := c.App.GetChannel(c.AppContext, info.ChannelId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
perm := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
||||
if info.CreatorId == model.BookmarkFileOwner {
|
||||
if !perm {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
} else if info.CreatorId != c.AppContext.Session().UserId && !perm {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
|
||||
if info.PostId == "" && info.CreatorId != model.BookmarkFileOwner {
|
||||
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()
|
||||
|
||||
if _, err := w.Write([]byte(model.MapToJSON(resp))); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
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.AppContext, c.Params.FileId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
setInaccessibleFileHeader(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
channel, err := c.App.GetChannel(c.AppContext, info.ChannelId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
perm := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
||||
if info.CreatorId == model.BookmarkFileOwner {
|
||||
if !perm {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
} else if info.CreatorId != c.AppContext.Session().UserId && !perm {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
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.AppContext, c.Params.FileId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
setInaccessibleFileHeader(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
channel, err := c.App.GetChannel(c.AppContext, info.ChannelId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
perm := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
||||
if info.CreatorId == model.BookmarkFileOwner {
|
||||
if !perm {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
} else if info.CreatorId != c.AppContext.Session().UserId && !perm {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
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.AppContext, 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 searchFilesInAllTeams(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
results, err := c.App.SearchFilesInTeamForUser(c.AppContext, terms, c.AppContext.Session().UserId, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage)
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
1628
channels/api4/file_test.go
Normal file
1628
channels/api4/file_test.go
Normal file
File diff suppressed because it is too large
Load Diff
1528
channels/api4/group.go
Normal file
1528
channels/api4/group.go
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user