1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-01-20 18:28:25 +02:00

Merge branch 'main' of github.com:mattermost/focalboard into main

This commit is contained in:
Chen-I Lim 2021-05-24 12:02:06 -07:00
commit 3af92afadd
81 changed files with 48688 additions and 194 deletions

2
.gitignore vendored
View File

@ -65,6 +65,8 @@ webapp/cypress/screenshots
webapp/cypress/videos
server/swagger/clients
server/vendor
mattermost-plugin/vendor
mattermost-plugin/dist
.idea
docker/certs
docker/data

View File

@ -8,18 +8,15 @@
"test_dbconfig": "file::memory:?cache=shared",
"useSSL": false,
"webpath": "./webapp/pack",
"filesdriver": "local",
"filespath": "./files",
"telemetry": true,
"prometheus_address": ":9092",
"webhook_update": [],
"secret": "this-is-a-secret-string",
"session_expire_time": 2592000,
"session_refresh_time": 18000,
"localOnly": false,
"enableLocalMode": true,
"localModeSocketLocation": "/var/tmp/focalboard_local.socket",
"authMode": "native",
"mattermostURL": "",
"mattermostClientID": "",
"mattermostClientSecret": ""
"authMode": "native"
}

View File

@ -38,19 +38,16 @@ func runServer(port int) (*server.Server, error) {
UseSSL: false,
SecureCookie: true,
WebPath: "./pack",
FilesDriver: "local",
FilesPath: "./focalboard_files",
Telemetry: true,
WebhookUpdate: []string{},
Secret: "",
SessionExpireTime: 259200000000,
SessionRefreshTime: 18000,
LocalOnly: false,
EnableLocalMode: false,
LocalModeSocketLocation: "",
AuthMode: "native",
MattermostURL: "",
MattermostClientID: "",
MattermostClientSecret: "",
}, sessionToken)
if err != nil {
fmt.Println("ERROR INITIALIZING THE SERVER", err)

View File

@ -0,0 +1,12 @@
version: 2.1
orbs:
plugin-ci: mattermost/plugin-ci@0.1.0
workflows:
version: 2
ci:
jobs:
- plugin-ci/lint
- plugin-ci/test
- plugin-ci/build

View File

@ -0,0 +1,27 @@
# http://editorconfig.org/
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
[*.go]
indent_style = tab
[*.{js, jsx, ts, tsx, json, html}]
indent_style = space
indent_size = 4
[webapp/package.json]
indent_size = 2
[{Makefile, *.mk}]
indent_style = tab
[*.md]
indent_style = space
indent_size = 4
trim_trailing_whitespace = false

1
mattermost-plugin/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
server/manifest.go linguist-generated=true

View File

@ -0,0 +1,56 @@
run:
timeout: 5m
modules-download-mode: readonly
linters-settings:
gofmt:
simplify: true
goimports:
local-prefixes: github.com/mattermost/mattermost-starter-template
golint:
min-confidence: 0
govet:
check-shadowing: true
enable-all: true
misspell:
locale: US
linters:
disable-all: true
enable:
- bodyclose
- deadcode
- errcheck
- gocritic
- gofmt
- goimports
- golint
- gosec
- gosimple
- govet
- ineffassign
- misspell
- nakedret
- staticcheck
- structcheck
- stylecheck
- typecheck
- unconvert
- unused
- varcheck
- whitespace
issues:
exclude-rules:
- path: server/manifest.go
linters:
- deadcode
- unused
- varcheck
- path: server/configuration.go
linters:
- unused
- path: _test\.go
linters:
- bodyclose
- scopelint # https://github.com/kyoh86/scopelint/issues/4

201
mattermost-plugin/LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

267
mattermost-plugin/Makefile Normal file
View File

@ -0,0 +1,267 @@
GO ?= $(shell command -v go 2> /dev/null)
NPM ?= $(shell command -v npm 2> /dev/null)
CURL ?= $(shell command -v curl 2> /dev/null)
MM_DEBUG ?=
MANIFEST_FILE ?= plugin.json
GOPATH ?= $(shell go env GOPATH)
GO_TEST_FLAGS ?= -race
GO_BUILD_FLAGS ?=
MM_UTILITIES_DIR ?= ../mattermost-utilities
DLV_DEBUG_PORT := 2346
export GO111MODULE=on
# You can include assets this directory into the bundle. This can be e.g. used to include profile pictures.
ASSETS_DIR ?= assets
## Define the default target (make all)
.PHONY: default
default: all
# Verify environment, and define PLUGIN_ID, PLUGIN_VERSION, HAS_SERVER and HAS_WEBAPP as needed.
include build/setup.mk
BUNDLE_NAME ?= $(PLUGIN_ID)-$(PLUGIN_VERSION).tar.gz
# Include custom makefile, if present
ifneq ($(wildcard build/custom.mk),)
include build/custom.mk
endif
## Checks the code style, tests, builds and bundles the plugin.
.PHONY: all
all: check-style test dist
## Propagates plugin manifest information into the server/ and webapp/ folders.
.PHONY: apply
apply:
./build/bin/manifest apply
## Runs eslint and golangci-lint
.PHONY: check-style
check-style: webapp/node_modules
@echo Checking for style guide compliance
ifneq ($(HAS_WEBAPP),)
cd webapp && npm run lint
cd webapp && npm run check-types
endif
ifneq ($(HAS_SERVER),)
@if ! [ -x "$$(command -v golangci-lint)" ]; then \
echo "golangci-lint is not installed. Please see https://github.com/golangci/golangci-lint#install for installation instructions."; \
exit 1; \
fi; \
@echo Running golangci-lint
golangci-lint run ./...
endif
## Builds the server, if it exists, for all supported architectures.
.PHONY: server
server:
ifneq ($(HAS_SERVER),)
mkdir -p server/dist;
ifeq ($(MM_DEBUG),)
cd server && env GOOS=linux GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) -o dist/plugin-linux-amd64;
cd server && env GOOS=darwin GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) -o dist/plugin-darwin-amd64;
cd server && env GOOS=windows GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) -o dist/plugin-windows-amd64.exe;
else
$(info DEBUG mode is on; to disable, unset MM_DEBUG)
cd server && env GOOS=darwin GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) -gcflags "all=-N -l" -o dist/plugin-darwin-amd64;
cd server && env GOOS=linux GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) -gcflags "all=-N -l" -o dist/plugin-linux-amd64;
cd server && env GOOS=windows GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) -gcflags "all=-N -l" -o dist/plugin-windows-amd64.exe;
endif
endif
## Ensures NPM dependencies are installed without having to run this all the time.
webapp/node_modules: $(wildcard webapp/package.json)
ifneq ($(HAS_WEBAPP),)
cd webapp && $(NPM) install
touch $@
endif
## Builds the webapp, if it exists.
.PHONY: webapp
webapp: webapp/node_modules
ifneq ($(HAS_WEBAPP),)
ifeq ($(MM_DEBUG),)
cd webapp && $(NPM) run build;
else
cd webapp && $(NPM) run debug;
endif
endif
## Generates a tar bundle of the plugin for install.
.PHONY: bundle
bundle:
rm -rf dist/
mkdir -p dist/$(PLUGIN_ID)
cp $(MANIFEST_FILE) dist/$(PLUGIN_ID)/
cp -r ../webapp/pack dist/$(PLUGIN_ID)/
ifneq ($(wildcard $(ASSETS_DIR)/.),)
cp -r $(ASSETS_DIR) dist/$(PLUGIN_ID)/
endif
ifneq ($(HAS_PUBLIC),)
cp -r public dist/$(PLUGIN_ID)/
endif
ifneq ($(HAS_SERVER),)
mkdir -p dist/$(PLUGIN_ID)/server
cp -r server/dist dist/$(PLUGIN_ID)/server/
endif
ifneq ($(HAS_WEBAPP),)
mkdir -p dist/$(PLUGIN_ID)/webapp
cp -r webapp/dist dist/$(PLUGIN_ID)/webapp/
endif
cd dist && tar -cvzf $(BUNDLE_NAME) $(PLUGIN_ID)
@echo plugin built at: dist/$(BUNDLE_NAME)
## Builds and bundles the plugin.
.PHONY: dist
dist: apply server webapp bundle
## Builds and installs the plugin to a server.
.PHONY: deploy
deploy: dist
./build/bin/pluginctl deploy $(PLUGIN_ID) dist/$(BUNDLE_NAME)
## Builds and installs the plugin to a server, updating the webapp automatically when changed.
.PHONY: watch
watch: apply server bundle
ifeq ($(MM_DEBUG),)
cd webapp && $(NPM) run build:watch
else
cd webapp && $(NPM) run debug:watch
endif
## Installs a previous built plugin with updated webpack assets to a server.
.PHONY: deploy-from-watch
deploy-from-watch: bundle
./build/bin/pluginctl deploy $(PLUGIN_ID) dist/$(BUNDLE_NAME)
## Setup dlv for attaching, identifying the plugin PID for other targets.
.PHONY: setup-attach
setup-attach:
$(eval PLUGIN_PID := $(shell ps aux | grep "plugins/${PLUGIN_ID}" | grep -v "grep" | awk -F " " '{print $$2}'))
$(eval NUM_PID := $(shell echo -n ${PLUGIN_PID} | wc -w))
@if [ ${NUM_PID} -gt 2 ]; then \
echo "** There is more than 1 plugin process running. Run 'make kill reset' to restart just one."; \
exit 1; \
fi
## Check if setup-attach succeeded.
.PHONY: check-attach
check-attach:
@if [ -z ${PLUGIN_PID} ]; then \
echo "Could not find plugin PID; the plugin is not running. Exiting."; \
exit 1; \
else \
echo "Located Plugin running with PID: ${PLUGIN_PID}"; \
fi
## Attach dlv to an existing plugin instance.
.PHONY: attach
attach: setup-attach check-attach
dlv attach ${PLUGIN_PID}
## Attach dlv to an existing plugin instance, exposing a headless instance on $DLV_DEBUG_PORT.
.PHONY: attach-headless
attach-headless: setup-attach check-attach
dlv attach ${PLUGIN_PID} --listen :$(DLV_DEBUG_PORT) --headless=true --api-version=2 --accept-multiclient
## Detach dlv from an existing plugin instance, if previously attached.
.PHONY: detach
detach: setup-attach
@DELVE_PID=$(shell ps aux | grep "dlv attach ${PLUGIN_PID}" | grep -v "grep" | awk -F " " '{print $$2}') && \
if [ "$$DELVE_PID" -gt 0 ] > /dev/null 2>&1 ; then \
echo "Located existing delve process running with PID: $$DELVE_PID. Killing." ; \
kill -9 $$DELVE_PID ; \
fi
## Runs any lints and unit tests defined for the server and webapp, if they exist.
.PHONY: test
test: webapp/node_modules
ifneq ($(HAS_SERVER),)
$(GO) test -v $(GO_TEST_FLAGS) ./server/...
endif
ifneq ($(HAS_WEBAPP),)
cd webapp && $(NPM) run test;
endif
ifneq ($(wildcard ./build/sync/plan/.),)
cd ./build/sync && $(GO) test -v $(GO_TEST_FLAGS) ./...
endif
## Creates a coverage report for the server code.
.PHONY: coverage
coverage: webapp/node_modules
ifneq ($(HAS_SERVER),)
$(GO) test $(GO_TEST_FLAGS) -coverprofile=server/coverage.txt ./server/...
$(GO) tool cover -html=server/coverage.txt
endif
## Extract strings for translation from the source code.
.PHONY: i18n-extract
i18n-extract:
ifneq ($(HAS_WEBAPP),)
ifeq ($(HAS_MM_UTILITIES),)
@echo "You must clone github.com/mattermost/mattermost-utilities repo in .. to use this command"
else
cd $(MM_UTILITIES_DIR) && npm install && npm run babel && node mmjstool/build/index.js i18n extract-webapp --webapp-dir $(PWD)/webapp
endif
endif
## Disable the plugin.
.PHONY: disable
disable: detach
./build/bin/pluginctl disable $(PLUGIN_ID)
## Enable the plugin.
.PHONY: enable
enable:
./build/bin/pluginctl enable $(PLUGIN_ID)
## Reset the plugin, effectively disabling and re-enabling it on the server.
.PHONY: reset
reset: detach
./build/bin/pluginctl reset $(PLUGIN_ID)
## Kill all instances of the plugin, detaching any existing dlv instance.
.PHONY: kill
kill: detach
$(eval PLUGIN_PID := $(shell ps aux | grep "plugins/${PLUGIN_ID}" | grep -v "grep" | awk -F " " '{print $$2}'))
@for PID in ${PLUGIN_PID}; do \
echo "Killing plugin pid $$PID"; \
kill -9 $$PID; \
done; \
## Clean removes all build artifacts.
.PHONY: clean
clean:
rm -fr dist/
ifneq ($(HAS_SERVER),)
rm -fr server/coverage.txt
rm -fr server/dist
endif
ifneq ($(HAS_WEBAPP),)
rm -fr webapp/junit.xml
rm -fr webapp/dist
rm -fr webapp/node_modules
endif
rm -fr build/bin/
## Sync directory with a starter template
sync:
ifndef STARTERTEMPLATE_PATH
@echo STARTERTEMPLATE_PATH is not set.
@echo Set STARTERTEMPLATE_PATH to a local clone of https://github.com/mattermost/mattermost-plugin-starter-template and retry.
@exit 1
endif
cd ${STARTERTEMPLATE_PATH} && go run ./build/sync/main.go ./build/sync/plan.yml $(PWD)
# Help documentation à la https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
help:
@cat Makefile build/*.mk | grep -v '\.PHONY' | grep -v '\help:' | grep -B1 -E '^[a-zA-Z0-9_.-]+:.*' | sed -e "s/:.*//" | sed -e "s/^## //" | grep -v '\-\-' | sed '1!G;h;$$!d' | awk 'NR%2{printf "\033[36m%-30s\033[0m",$$0;next;}1' | sort

View File

@ -0,0 +1,3 @@
# Focalboard Plugin for Mattermost
This plugin allows to run focalboard inside your mattermost instance as a plugin.

View File

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="241px" height="240px" viewBox="0 0 241 240" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
<title>blue-icon</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="06" transform="translate(-681.000000, -572.000000)" fill="#1875F0">
<g id="Group-2" transform="translate(626.000000, 517.000000)">
<path d="M216.908181,153.127705 C216.908181,153.127705 217.280588,169.452526 205.928754,180.543035 C194.57546,191.633544 180.631383,190.619887 171.560722,187.557072 C162.488602,184.494256 150.79503,176.85251 148.531381,161.16705 C146.269193,145.480133 156.508188,132.736607 156.508188,132.736607 L178.820463,105.066407 L191.815268,89.2629779 L202.969946,75.4912313 C202.969946,75.4912313 208.088713,68.6534193 209.547671,67.2421648 C209.836834,66.9625354 210.133299,66.7790286 210.423923,66.6377576 L210.635683,66.5299837 L210.673654,66.5154197 C211.28703,66.2518108 211.993873,66.195011 212.675888,66.4251227 C213.343299,66.6508652 213.860288,67.1081757 214.187421,67.6718037 L214.256061,67.7810339 L214.315938,67.9062846 C214.475124,68.2063036 214.608022,68.5485583 214.67082,68.9709151 C214.968745,70.976382 214.870897,79.5094471 214.870897,79.5094471 L215.342613,97.2047434 L216.039232,117.630795 L216.908181,153.127705 Z M245.790587,78.2043261 C287.057212,108.155253 305.982915,162.509669 288.774288,213.346872 C267.594104,275.911031 199.706245,309.46073 137.142925,288.281718 C74.5796048,267.10125 41.031812,199.213937 62.2105402,136.649778 C79.4482947,85.7295603 127.625459,54.0324057 178.690632,55.4145322 L162.322339,74.7541074 C132.028106,80.231639 105.87146,100.919843 95.5908489,131.290215 C80.2944535,176.475117 105.932628,225.982624 152.855846,241.866155 C199.777608,257.751142 250.216536,233.998666 265.512932,188.813764 C275.760046,158.543884 267.634882,126.336988 247.050359,103.595256 L245.790587,78.2043261 Z" id="blue-icon"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

1
mattermost-plugin/build/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
bin

View File

@ -0,0 +1 @@
# Include custom targets and environment variables here

View File

@ -0,0 +1,11 @@
module github.com/mattermost/mattermost-plugin-starter-template/build
go 1.12
require (
github.com/go-git/go-git/v5 v5.1.0
github.com/mattermost/mattermost-server/v5 v5.3.2-0.20200924100636-e726b0426826
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.6.1
sigs.k8s.io/yaml v1.2.0
)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,126 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/pkg/errors"
)
const pluginIDGoFileTemplate = `// This file is automatically generated. Do not modify it manually.
package main
import (
"strings"
"github.com/mattermost/mattermost-server/v5/model"
)
var manifest *model.Manifest
const manifestStr = ` + "`" + `
%s
` + "`" + `
func init() {
manifest = model.ManifestFromJson(strings.NewReader(manifestStr))
}
`
func main() {
if len(os.Args) <= 1 {
panic("no cmd specified")
}
manifest, err := findManifest()
if err != nil {
panic("failed to find manifest: " + err.Error())
}
cmd := os.Args[1]
switch cmd {
case "id":
dumpPluginID(manifest)
case "version":
dumpPluginVersion(manifest)
case "has_server":
if manifest.HasServer() {
fmt.Printf("true")
}
case "has_webapp":
if manifest.HasWebapp() {
fmt.Printf("true")
}
case "apply":
if err := applyManifest(manifest); err != nil {
panic("failed to apply manifest: " + err.Error())
}
default:
panic("unrecognized command: " + cmd)
}
}
func findManifest() (*model.Manifest, error) {
_, manifestFilePath, err := model.FindManifest(".")
if err != nil {
return nil, errors.Wrap(err, "failed to find manifest in current working directory")
}
manifestFile, err := os.Open(manifestFilePath)
if err != nil {
return nil, errors.Wrapf(err, "failed to open %s", manifestFilePath)
}
defer manifestFile.Close()
// Re-decode the manifest, disallowing unknown fields. When we write the manifest back out,
// we don't want to accidentally clobber anything we won't preserve.
var manifest model.Manifest
decoder := json.NewDecoder(manifestFile)
decoder.DisallowUnknownFields()
if err = decoder.Decode(&manifest); err != nil {
return nil, errors.Wrap(err, "failed to parse manifest")
}
return &manifest, nil
}
// dumpPluginId writes the plugin id from the given manifest to standard out
func dumpPluginID(manifest *model.Manifest) {
fmt.Printf("%s", manifest.Id)
}
// dumpPluginVersion writes the plugin version from the given manifest to standard out
func dumpPluginVersion(manifest *model.Manifest) {
fmt.Printf("%s", manifest.Version)
}
// applyManifest propagates the plugin_id into the server and webapp folders, as necessary
func applyManifest(manifest *model.Manifest) error {
if manifest.HasServer() {
// generate JSON representation of Manifest.
manifestBytes, err := json.MarshalIndent(manifest, "", " ")
if err != nil {
return err
}
manifestStr := string(manifestBytes)
// write generated code to file by using Go file template.
if err := ioutil.WriteFile(
"server/manifest.go",
[]byte(fmt.Sprintf(pluginIDGoFileTemplate, manifestStr)),
0600,
); err != nil {
return errors.Wrap(err, "failed to write server/manifest.go")
}
}
return nil
}

View File

@ -0,0 +1,172 @@
// main handles deployment of the plugin to a development server using the Client4 API.
package main
import (
"errors"
"fmt"
"log"
"net"
"os"
"github.com/mattermost/mattermost-server/v5/model"
)
const helpText = `
Usage:
pluginctl deploy <plugin id> <bundle path>
pluginctl disable <plugin id>
pluginctl enable <plugin id>
pluginctl reset <plugin id>
`
func main() {
err := pluginctl()
if err != nil {
fmt.Printf("Failed: %s\n", err.Error())
fmt.Print(helpText)
os.Exit(1)
}
}
func pluginctl() error {
if len(os.Args) < 3 {
return errors.New("invalid number of arguments")
}
client, err := getClient()
if err != nil {
return err
}
switch os.Args[1] {
case "deploy":
if len(os.Args) < 4 {
return errors.New("invalid number of arguments")
}
return deploy(client, os.Args[2], os.Args[3])
case "disable":
return disablePlugin(client, os.Args[2])
case "enable":
return enablePlugin(client, os.Args[2])
case "reset":
return resetPlugin(client, os.Args[2])
default:
return errors.New("invalid second argument")
}
}
func getClient() (*model.Client4, error) {
socketPath := os.Getenv("MM_LOCALSOCKETPATH")
if socketPath == "" {
socketPath = model.LOCAL_MODE_SOCKET_PATH
}
client, connected := getUnixClient(socketPath)
if connected {
log.Printf("Connecting using local mode over %s", socketPath)
return client, nil
}
if os.Getenv("MM_LOCALSOCKETPATH") != "" {
log.Printf("No socket found at %s for local mode deployment. Attempting to authenticate with credentials.", socketPath)
}
siteURL := os.Getenv("MM_SERVICESETTINGS_SITEURL")
adminToken := os.Getenv("MM_ADMIN_TOKEN")
adminUsername := os.Getenv("MM_ADMIN_USERNAME")
adminPassword := os.Getenv("MM_ADMIN_PASSWORD")
if siteURL == "" {
return nil, errors.New("MM_SERVICESETTINGS_SITEURL is not set")
}
client = model.NewAPIv4Client(siteURL)
if adminToken != "" {
log.Printf("Authenticating using token against %s.", siteURL)
client.SetToken(adminToken)
return client, nil
}
if adminUsername != "" && adminPassword != "" {
client := model.NewAPIv4Client(siteURL)
log.Printf("Authenticating as %s against %s.", adminUsername, siteURL)
_, resp := client.Login(adminUsername, adminPassword)
if resp.Error != nil {
return nil, fmt.Errorf("failed to login as %s: %w", adminUsername, resp.Error)
}
return client, nil
}
return nil, errors.New("one of MM_ADMIN_TOKEN or MM_ADMIN_USERNAME/MM_ADMIN_PASSWORD must be defined")
}
func getUnixClient(socketPath string) (*model.Client4, bool) {
_, err := net.Dial("unix", socketPath)
if err != nil {
return nil, false
}
return model.NewAPIv4SocketClient(socketPath), true
}
// deploy attempts to upload and enable a plugin via the Client4 API.
// It will fail if plugin uploads are disabled.
func deploy(client *model.Client4, pluginID, bundlePath string) error {
pluginBundle, err := os.Open(bundlePath)
if err != nil {
return fmt.Errorf("failed to open %s: %w", bundlePath, err)
}
defer pluginBundle.Close()
log.Print("Uploading plugin via API.")
_, resp := client.UploadPluginForced(pluginBundle)
if resp.Error != nil {
return fmt.Errorf("failed to upload plugin bundle: %s", resp.Error.Error())
}
log.Print("Enabling plugin.")
_, resp = client.EnablePlugin(pluginID)
if resp.Error != nil {
return fmt.Errorf("failed to enable plugin: %s", resp.Error.Error())
}
return nil
}
// disablePlugin attempts to disable the plugin via the Client4 API.
func disablePlugin(client *model.Client4, pluginID string) error {
log.Print("Disabling plugin.")
_, resp := client.DisablePlugin(pluginID)
if resp.Error != nil {
return fmt.Errorf("failed to disable plugin: %w", resp.Error)
}
return nil
}
// enablePlugin attempts to enable the plugin via the Client4 API.
func enablePlugin(client *model.Client4, pluginID string) error {
log.Print("Enabling plugin.")
_, resp := client.EnablePlugin(pluginID)
if resp.Error != nil {
return fmt.Errorf("failed to enable plugin: %w", resp.Error)
}
return nil
}
// resetPlugin attempts to reset the plugin via the Client4 API.
func resetPlugin(client *model.Client4, pluginID string) error {
err := disablePlugin(client, pluginID)
if err != nil {
return err
}
err = enablePlugin(client, pluginID)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,45 @@
# Ensure that go is installed. Note that this is independent of whether or not a server is being
# built, since the build script itself uses go.
ifeq ($(GO),)
$(error "go is not available: see https://golang.org/doc/install")
endif
# Ensure that the build tools are compiled. Go's caching makes this quick.
$(shell cd build/manifest && $(GO) build -o ../bin/manifest)
# Ensure that the deployment tools are compiled. Go's caching makes this quick.
$(shell cd build/pluginctl && $(GO) build -o ../bin/pluginctl)
# Extract the plugin id from the manifest.
PLUGIN_ID ?= $(shell build/bin/manifest id)
ifeq ($(PLUGIN_ID),)
$(error "Cannot parse id from $(MANIFEST_FILE)")
endif
# Extract the plugin version from the manifest.
PLUGIN_VERSION ?= $(shell build/bin/manifest version)
ifeq ($(PLUGIN_VERSION),)
$(error "Cannot parse version from $(MANIFEST_FILE)")
endif
# Determine if a server is defined in the manifest.
HAS_SERVER ?= $(shell build/bin/manifest has_server)
# Determine if a webapp is defined in the manifest.
HAS_WEBAPP ?= $(shell build/bin/manifest has_webapp)
# Determine if a /public folder is in use
HAS_PUBLIC ?= $(wildcard public/.)
# Determine if the mattermost-utilities repo is present
HAS_MM_UTILITIES ?= $(wildcard $(MM_UTILITIES_DIR)/.)
# Store the current path for later use
PWD ?= $(shell pwd)
# Ensure that npm (and thus node) is installed.
ifneq ($(HAS_WEBAPP),)
ifeq ($(NPM),)
$(error "npm is not available: see https://www.npmjs.com/get-npm")
endif
endif

View File

@ -0,0 +1,113 @@
sync
====
The sync tool is a proof-of-concept implementation of a tool for synchronizing mattermost plugin
repositories with the mattermost-plugin-starter-template repo.
Overview
--------
At its core the tool is just a collection of checks and actions that are executed according to a
synchronization plan (see [./build/sync/plan.yml](https://github.com/mattermost/mattermost-plugin-starter-template/blob/sync/build/sync/plan.yml)
for an example). The plan defines a set of files
and/or directories that need to be kept in sync between the plugin repository and the template (this
repo).
For each set of paths, a set of actions to be performed is outlined. No more than one action of that set
will be executed - the first one whose checks pass. Other actions are meant to act as fallbacks.
The idea is to be able to e.g. overwrite a file if it has no local changes or apply a format-specific
merge algorithm otherwise.
Before running each action, the tool will check if any checks are defined for that action. If there
are any, they will be executed and their results examined. If all checks pass, the action will be executed.
If there is a check failure, the tool will locate the next applicable action according to the plan and
start over with it.
The synchronization plan can also run checks before running any actions, e.g. to check if the source and
target worktrees are clean.
Running
-------
The tool can be executed from the root of this repository with a command:
```
$ go run ./build/sync/main.go ./build/sync/plan.yml ../mattermost-plugin-github
```
(assuming `mattermost-plugin-github` is the target repository we want to synchronize with the source).
plan.yml
---------
The `plan.yml` file (located in `build/sync/plan.yml`) consists of two parts:
- checks
- actions
The `checks` section defines tests to run before executing the plan itself. Currently the only available such check is `repo_is_clean` defined as:
```
type: repo_is_clean
params:
repo: source
```
The `repo` parameter takes one of two values:
- `source` - the `mattermost-plugin-starter-template` repository
- `target` - the repository of the plugin being updated.
The `actions` section defines actions to be run as part of the synchronization.
Each entry in this section has the form:
```
paths:
- path1
- path2
actions:
- type: action_type
params:
action_parameter: value
conditions:
- type: check_type
params:
check_parameter: value
```
`paths` is a list of file or directory paths (relative to the root of the repository)
synchronization should be performed on.
Each action in the `actions` section is defined by its type. Currently supported action types are:
- `overwrite_file` - overwrite the specified file in the `target` repository with the file in the `source` repository.
- `overwrite_directory` - overwrite a directory.
Both actions accept a parameter called `create` which determines if the file or directory should be created if it does not exist in the target repository.
The `conditions` part of an action definition defines tests that need to pass for the
action to be run. Available checks are:
- `exists`
- `file_unaltered`
The `exists` check takes a single parameter - `repo` (referencing either the source or target repository) and it passes only if the file or directory the action is about to be run on exists. If the repo parameter is not specified, it will default to `target`.
The `file_unaltered` check is only applicable to file paths. It passes if the file
has not been altered - i.e. it is identical to some version of that same file in the reference repository (usually `source`). This check takes two parameters:
- `in` - repository to check the file in, default `target`
- `compared-to` - repository to check the file against, default `source`.
When multiple actions are specified for a set of paths, the `sync` tool will only
execute a single action for each path. The first action in the list, whose conditions
are all satisfied will be executed.
If an acton fails due to an error, the synchronization run will be aborted.
Caveat emptor
-------------
This is a very basic proof-of-concept and there are many things that should be improved/implemented:
(in no specific order)
1. Format-specific merge actions for `go.mod`, `go.sum`, `webapp/package.json` and other files should
be implemented.
2. Better logging should be implemented.
3. Handling action dependencies should be investigated.
e.g. if the `build` directory is overwritten, that will in some cases mean that the go.mod file also needs
to be updated.
4. Storing the tree-hash of the template repository that the plugin was synchronized with would allow
improving the performance of the tool by restricting the search space when examining if a file
has been altered in the plugin repository.

View File

@ -0,0 +1,85 @@
package main
import (
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sigs.k8s.io/yaml"
"github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan"
)
func main() {
verbose := flag.Bool("verbose", false, "enable verbose output")
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "Update a pluging directory with /mattermost-plugin-starter-template/.\n")
fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args[0])
fmt.Fprintf(flag.CommandLine.Output(), "%s <plan.yml> <plugin_directory>\n", os.Args[0])
flag.PrintDefaults()
}
flag.Parse()
// TODO: implement proper command line parameter parsing.
if len(os.Args) != 3 {
fmt.Fprintf(os.Stderr, "running: \n $ sync [plan.yaml] [plugin path]\n")
os.Exit(1)
}
syncPlan, err := readPlan(os.Args[1])
if err != nil {
fmt.Fprintf(os.Stderr, "coud not load plan: %s\n", err)
os.Exit(1)
}
srcDir, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to get current directory: %s\n", err)
os.Exit(1)
}
trgDir, err := filepath.Abs(os.Args[2])
if err != nil {
fmt.Fprintf(os.Stderr, "could not determine target directory: %s\n", err)
os.Exit(1)
}
srcRepo, err := plan.GetRepoSetup(srcDir)
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1)
}
trgRepo, err := plan.GetRepoSetup(trgDir)
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1)
}
planSetup := plan.Setup{
Source: srcRepo,
Target: trgRepo,
VerboseLogging: *verbose,
}
err = syncPlan.Execute(planSetup)
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1)
}
}
func readPlan(path string) (*plan.Plan, error) {
raw, err := ioutil.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read plan file %q: %v", path, err)
}
var p plan.Plan
err = yaml.Unmarshal(raw, &p)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal plan yaml: %v", err)
}
return &p, err
}

View File

@ -0,0 +1,44 @@
checks:
- type: repo_is_clean
params:
repo: source
- type: repo_is_clean
params:
repo: target
actions:
- paths:
- build/pluginctl
- build/manifest
actions:
- type: overwrite_directory
params:
create: true
- paths:
- Makefile
actions:
- type: overwrite_file
params:
create: true
- paths:
- .editorconfig
- .gitattributes
- .gitignore
- build/.gitignore
- build/go.mod
- build/go.sum
- build/setup.mk
- server/.gitignore
- webapp/.eslintrc.json
- webapp/.npmrc
- webapp/babel.config.js
- webapp/package.json
- webapp/tsconfig.json
- webapp/webpack.config.js
- webapp/src/manifest.test.tsx
- webapp/tests/setup.tsx
actions:
- type: overwrite_file
params:
create: true
conditions:
- type: file_unaltered

View File

@ -0,0 +1,214 @@
package plan
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// ActionConditions adds condition support to actions.
type ActionConditions struct {
// Conditions are checkers run before executing the
// action. If any one fails (returns an error), the action
// itself is not executed.
Conditions []Check
}
// Check runs the conditions associated with the action and returns
// the first error (if any).
func (c ActionConditions) Check(path string, setup Setup) error {
if len(c.Conditions) > 0 {
setup.Logf("checking action conditions")
}
for _, condition := range c.Conditions {
err := condition.Check(path, setup)
if err != nil {
return err
}
}
return nil
}
// OverwriteFileAction is used to overwrite a file.
type OverwriteFileAction struct {
ActionConditions
Params struct {
// Create determines whether the target directory
// will be created if it does not exist.
Create bool `json:"create"`
}
}
// Run implements plan.Action.Run.
func (a OverwriteFileAction) Run(path string, setup Setup) error {
setup.Logf("overwriting file %q", path)
src := setup.PathInRepo(SourceRepo, path)
dst := setup.PathInRepo(TargetRepo, path)
dstInfo, err := os.Stat(dst)
switch {
case os.IsNotExist(err):
if !a.Params.Create {
return fmt.Errorf("path %q does not exist, not creating", dst)
}
case err != nil:
return fmt.Errorf("failed to check path %q: %v", dst, err)
case dstInfo.IsDir():
return fmt.Errorf("path %q is a directory", dst)
}
srcInfo, err := os.Stat(src)
if os.IsNotExist(err) {
return fmt.Errorf("file %q does not exist", src)
} else if err != nil {
return fmt.Errorf("failed to check path %q: %v", src, err)
}
if srcInfo.IsDir() {
return fmt.Errorf("path %q is a directory", src)
}
srcF, err := os.Open(src)
if err != nil {
return fmt.Errorf("failed to open %q: %v", src, err)
}
defer srcF.Close()
dstF, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, srcInfo.Mode())
if err != nil {
return fmt.Errorf("failed to open %q: %v", src, err)
}
defer dstF.Close()
_, err = io.Copy(dstF, srcF)
if err != nil {
return fmt.Errorf("failed to copy file %q: %v", path, err)
}
return nil
}
// OverwriteDirectoryAction is used to completely overwrite directories.
// If the target directory exists, it will be removed first.
type OverwriteDirectoryAction struct {
ActionConditions
Params struct {
// Create determines whether the target directory
// will be created if it does not exist.
Create bool `json:"create"`
}
}
// Run implements plan.Action.Run.
func (a OverwriteDirectoryAction) Run(path string, setup Setup) error {
setup.Logf("overwriting directory %q", path)
src := setup.PathInRepo(SourceRepo, path)
dst := setup.PathInRepo(TargetRepo, path)
dstInfo, err := os.Stat(dst)
switch {
case os.IsNotExist(err):
if !a.Params.Create {
return fmt.Errorf("path %q does not exist, not creating", dst)
}
case err != nil:
return fmt.Errorf("failed to check path %q: %v", dst, err)
default:
if !dstInfo.IsDir() {
return fmt.Errorf("path %q is not a directory", dst)
}
err = os.RemoveAll(dst)
if err != nil {
return fmt.Errorf("failed to remove directory %q: %v", dst, err)
}
}
srcInfo, err := os.Stat(src)
if os.IsNotExist(err) {
return fmt.Errorf("directory %q does not exist", src)
} else if err != nil {
return fmt.Errorf("failed to check path %q: %v", src, err)
}
if !srcInfo.IsDir() {
return fmt.Errorf("path %q is not a directory", src)
}
err = CopyDirectory(src, dst)
if err != nil {
return fmt.Errorf("failed to copy path %q: %v", path, err)
}
return nil
}
// CopyDirectory copies the directory src to dst so that after
// a successful operation the contents of src and dst are equal.
func CopyDirectory(src, dst string) error {
copier := dirCopier{dst: dst, src: src}
return filepath.Walk(src, copier.Copy)
}
type dirCopier struct {
dst string
src string
}
// Convert a path in the source directory to a path in the destination
// directory.
func (d dirCopier) srcToDst(path string) (string, error) {
suff := strings.TrimPrefix(path, d.src)
if suff == path {
return "", fmt.Errorf("path %q is not in %q", path, d.src)
}
return filepath.Join(d.dst, suff), nil
}
// Copy is an implementation of filepatch.WalkFunc that copies the
// source directory to target with all subdirectories.
func (d dirCopier) Copy(srcPath string, info os.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("failed to copy directory: %v", err)
}
trgPath, err := d.srcToDst(srcPath)
if err != nil {
return err
}
if info.IsDir() {
err = os.MkdirAll(trgPath, info.Mode())
if err != nil {
return fmt.Errorf("failed to create directory %q: %v", trgPath, err)
}
err = os.Chtimes(trgPath, info.ModTime(), info.ModTime())
if err != nil {
return fmt.Errorf("failed to create directory %q: %v", trgPath, err)
}
return nil
}
err = copyFile(srcPath, trgPath, info)
if err != nil {
return fmt.Errorf("failed to copy file %q: %v", srcPath, err)
}
return nil
}
func copyFile(src, dst string, info os.FileInfo) error {
srcF, err := os.Open(src)
if err != nil {
return fmt.Errorf("failed to open source file %q: %v", src, err)
}
defer srcF.Close()
dstF, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, info.Mode())
if err != nil {
return fmt.Errorf("failed to open destination file %q: %v", dst, err)
}
_, err = io.Copy(dstF, srcF)
if err != nil {
dstF.Close()
return fmt.Errorf("failed to copy file %q: %v", src, err)
}
if err = dstF.Close(); err != nil {
return fmt.Errorf("failed to close file %q: %v", dst, err)
}
err = os.Chtimes(dst, info.ModTime(), info.ModTime())
if err != nil {
return fmt.Errorf("failed to adjust file modification time for %q: %v", dst, err)
}
return nil
}

View File

@ -0,0 +1,112 @@
package plan_test
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan"
)
func TestCopyDirectory(t *testing.T) {
assert := assert.New(t)
// Create a temporary directory to copy to.
dir, err := ioutil.TempDir("", "test")
assert.Nil(err)
defer os.RemoveAll(dir)
wd, err := os.Getwd()
assert.Nil(err)
srcDir := filepath.Join(wd, "testdata")
err = plan.CopyDirectory(srcDir, dir)
assert.Nil(err)
compareDirectories(t, dir, srcDir)
}
func TestOverwriteFileAction(t *testing.T) {
assert := assert.New(t)
// Create a temporary directory to copy to.
dir, err := ioutil.TempDir("", "test")
assert.Nil(err)
defer os.RemoveAll(dir)
wd, err := os.Getwd()
assert.Nil(err)
setup := plan.Setup{
Source: plan.RepoSetup{
Git: nil,
Path: filepath.Join(wd, "testdata", "b"),
},
Target: plan.RepoSetup{
Git: nil,
Path: dir,
},
}
action := plan.OverwriteFileAction{}
action.Params.Create = true
err = action.Run("c", setup)
assert.Nil(err)
compareDirectories(t, dir, filepath.Join(wd, "testdata", "b"))
}
func TestOverwriteDirectoryAction(t *testing.T) {
assert := assert.New(t)
// Create a temporary directory to copy to.
dir, err := ioutil.TempDir("", "test")
assert.Nil(err)
defer os.RemoveAll(dir)
wd, err := os.Getwd()
assert.Nil(err)
setup := plan.Setup{
Source: plan.RepoSetup{
Git: nil,
Path: wd,
},
Target: plan.RepoSetup{
Git: nil,
Path: dir,
},
}
action := plan.OverwriteDirectoryAction{}
action.Params.Create = true
err = action.Run("testdata", setup)
assert.Nil(err)
destDir := filepath.Join(dir, "testdata")
srcDir := filepath.Join(wd, "testdata")
compareDirectories(t, destDir, srcDir)
}
func compareDirectories(t *testing.T, pathA, pathB string) {
assert := assert.New(t)
t.Helper()
aContents, err := ioutil.ReadDir(pathA)
assert.Nil(err)
bContents, err := ioutil.ReadDir(pathB)
assert.Nil(err)
assert.Len(aContents, len(bContents))
// Check the directory contents are equal.
for i, aFInfo := range aContents {
bFInfo := bContents[i]
assert.Equal(aFInfo.Name(), bFInfo.Name())
assert.Equal(aFInfo.Mode(), bFInfo.Mode())
assert.Equal(aFInfo.IsDir(), bFInfo.IsDir())
if !aFInfo.IsDir() {
assert.Equal(aFInfo.Size(), bFInfo.Size())
}
}
}

View File

@ -0,0 +1,176 @@
package plan
import (
"fmt"
"os"
"sort"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan/git"
)
// CheckFail is a custom error type used to indicate a
// check that did not pass (but did not fail due to external
// causes.
// Use `IsCheckFail` to check if an error is a check failure.
type CheckFail string
func (e CheckFail) Error() string {
return string(e)
}
// CheckFailf creates an error with the specified message string.
// The error will pass the IsCheckFail filter.
func CheckFailf(msg string, args ...interface{}) CheckFail {
if len(args) > 0 {
msg = fmt.Sprintf(msg, args...)
}
return CheckFail(msg)
}
// IsCheckFail determines if an error is a check fail error.
func IsCheckFail(err error) bool {
if err == nil {
return false
}
_, ok := err.(CheckFail)
return ok
}
// RepoIsCleanChecker checks whether the git repository is clean.
type RepoIsCleanChecker struct {
Params struct {
Repo RepoID
}
}
// Check implements the Checker interface.
// The path parameter is ignored because this checker checks the state of a repository.
func (r RepoIsCleanChecker) Check(_ string, ctx Setup) error {
ctx.Logf("checking if repository %q is clean", r.Params.Repo)
rc := ctx.GetRepo(r.Params.Repo)
repo := rc.Git
worktree, err := repo.Worktree()
if err != nil {
return fmt.Errorf("failed to get worktree: %v", err)
}
status, err := worktree.Status()
if err != nil {
return fmt.Errorf("failed to get worktree status: %v", err)
}
if !status.IsClean() {
return CheckFailf("%q repository is not clean", r.Params.Repo)
}
return nil
}
// PathExistsChecker checks whether the fle or directory with the
// path exists. If it does not, an error is returned.
type PathExistsChecker struct {
Params struct {
Repo RepoID
}
}
// Check implements the Checker interface.
func (r PathExistsChecker) Check(path string, ctx Setup) error {
repo := r.Params.Repo
if repo == "" {
repo = TargetRepo
}
ctx.Logf("checking if path %q exists in repo %q", path, repo)
absPath := ctx.PathInRepo(repo, path)
_, err := os.Stat(absPath)
if os.IsNotExist(err) {
return CheckFailf("path %q does not exist", path)
} else if err != nil {
return fmt.Errorf("failed to stat path %q: %v", absPath, err)
}
return nil
}
// FileUnalteredChecker checks whether the file in Repo is
// an unaltered version of that same file in ReferenceRepo.
//
// Its purpose is to check that a file has not been changed after forking a repository.
// It could be an old unaltered version, so the git history of the file is traversed
// until a matching version is found.
//
// If the repositories in the parameters are not specified,
// reference will default to the source repository and repo - to the target.
type FileUnalteredChecker struct {
Params struct {
SourceRepo RepoID `json:"compared-to"`
TargetRepo RepoID `json:"in"`
}
}
// Check implements the Checker interface.
func (f FileUnalteredChecker) Check(path string, setup Setup) error {
setup.Logf("checking if file %q has not been altered", path)
repo := f.Params.TargetRepo
if repo == "" {
repo = TargetRepo
}
source := f.Params.SourceRepo
if source == "" {
source = SourceRepo
}
trgPath := setup.PathInRepo(repo, path)
srcPath := setup.PathInRepo(source, path)
fileHashes, err := git.FileHistory(path, setup.GetRepo(source).Git)
if err != nil {
return err
}
var srcDeleted bool
srcInfo, err := os.Stat(srcPath)
if err != nil {
if os.IsNotExist(err) {
srcDeleted = true
} else {
return fmt.Errorf("failed to get stat for %q: %v", trgPath, err)
}
} else if srcInfo.IsDir() {
return fmt.Errorf("%q is a directory in source repository", path)
}
trgInfo, err := os.Stat(trgPath)
if os.IsNotExist(err) {
if srcDeleted {
// File has been deleted in target and source repositories.
// Consider it unaltered.
return nil
}
// Check if the file was ever in git history.
_, err := git.FileHistory(path, setup.GetRepo(repo).Git)
if errors.Is(err, git.ErrNotFound) {
// This is a new file being introduced to the target repo.
// Consider it unaltered.
return nil
} else if err != nil {
return err
}
return CheckFailf("file %q has been deleted", trgPath)
}
if err != nil {
return fmt.Errorf("failed to get stat for %q: %v", trgPath, err)
}
if trgInfo.IsDir() {
return fmt.Errorf("%q is a directory", trgPath)
}
currentHash, err := git.GetFileHash(trgPath)
if err != nil {
return err
}
sort.Strings(fileHashes)
idx := sort.SearchStrings(fileHashes, currentHash)
if idx < len(fileHashes) && fileHashes[idx] == currentHash {
return nil
}
return CheckFailf("file %q has been altered", trgPath)
}

View File

@ -0,0 +1,213 @@
package plan_test
import (
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"testing"
"time"
git "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan"
)
// Tests for the RepoIsClean checker.
func TestRepoIsCleanChecker(t *testing.T) {
assert := assert.New(t)
// Create a git repository in a temporary dir.
dir, err := ioutil.TempDir("", "test")
assert.Nil(err)
defer os.RemoveAll(dir)
repo, err := git.PlainInit(dir, false)
assert.Nil(err)
// Repo should be clean.
checker := plan.RepoIsCleanChecker{}
checker.Params.Repo = plan.TargetRepo
ctx := plan.Setup{
Target: plan.RepoSetup{
Path: dir,
Git: repo,
},
}
assert.Nil(checker.Check("", ctx))
// Create a file in the repository.
err = ioutil.WriteFile(path.Join(dir, "data.txt"), []byte("lorem ipsum"), 0600)
assert.Nil(err)
err = checker.Check("", ctx)
assert.EqualError(err, "\"target\" repository is not clean")
assert.True(plan.IsCheckFail(err))
}
func TestPathExistsChecker(t *testing.T) {
assert := assert.New(t)
// Set up a working directory.
wd, err := ioutil.TempDir("", "repo")
assert.Nil(err)
defer os.RemoveAll(wd)
err = os.Mkdir(filepath.Join(wd, "t"), 0755)
assert.Nil(err)
err = ioutil.WriteFile(filepath.Join(wd, "t", "test"), []byte("lorem ipsum"), 0644)
assert.Nil(err)
checker := plan.PathExistsChecker{}
checker.Params.Repo = plan.SourceRepo
ctx := plan.Setup{
Source: plan.RepoSetup{
Path: wd,
},
}
// Check with existing directory.
assert.Nil(checker.Check("t", ctx))
// Check with existing file.
assert.Nil(checker.Check("t/test", ctx))
err = checker.Check("nosuchpath", ctx)
assert.NotNil(err)
assert.True(plan.IsCheckFail(err))
}
func tempGitRepo(assert *assert.Assertions) (string, *git.Repository, func()) {
// Setup repository.
wd, err := ioutil.TempDir("", "repo")
assert.Nil(err)
// Initialize a repository.
repo, err := git.PlainInit(wd, false)
assert.Nil(err)
w, err := repo.Worktree()
assert.Nil(err)
// Create repository files.
err = ioutil.WriteFile(filepath.Join(wd, "test"),
[]byte("lorem ipsum"), 0644)
assert.Nil(err)
sig := &object.Signature{
Name: "test",
Email: "test@example.com",
When: time.Now(),
}
_, err = w.Commit("initial commit", &git.CommitOptions{Author: sig})
assert.Nil(err)
pathA := "a.txt"
err = ioutil.WriteFile(filepath.Join(wd, pathA),
[]byte("lorem ipsum"), 0644)
assert.Nil(err)
_, err = w.Add(pathA)
assert.Nil(err)
_, err = w.Commit("add files", &git.CommitOptions{Author: sig})
assert.Nil(err)
return wd, repo, func() { os.RemoveAll(wd) }
}
func TestUnalteredCheckerSameFile(t *testing.T) {
assert := assert.New(t)
wd, repo, cleanup := tempGitRepo(assert)
defer cleanup()
ctx := plan.Setup{
Source: plan.RepoSetup{
Path: wd,
Git: repo,
},
Target: plan.RepoSetup{
Path: wd,
},
}
checker := plan.FileUnalteredChecker{}
checker.Params.SourceRepo = plan.SourceRepo
checker.Params.TargetRepo = plan.TargetRepo
// Check with the same file - check should succeed
hashPath := "a.txt"
err := checker.Check(hashPath, ctx)
assert.Nil(err)
}
func TestUnalteredCheckerDifferentContents(t *testing.T) {
assert := assert.New(t)
wd, repo, cleanup := tempGitRepo(assert)
defer cleanup()
ctx := plan.Setup{
Source: plan.RepoSetup{
Path: wd,
Git: repo,
},
Target: plan.RepoSetup{
Path: wd,
},
}
checker := plan.FileUnalteredChecker{}
checker.Params.SourceRepo = plan.SourceRepo
checker.Params.TargetRepo = plan.TargetRepo
// Create a file with the same suffix path, but different contents.
tmpDir, err := ioutil.TempDir("", "test")
assert.Nil(err)
defer os.RemoveAll(tmpDir)
err = ioutil.WriteFile(filepath.Join(tmpDir, "a.txt"),
[]byte("not lorem ipsum"), 0644)
assert.Nil(err)
// Set the plugin path to the temporary directory.
ctx.Target.Path = tmpDir
err = checker.Check("a.txt", ctx)
assert.True(plan.IsCheckFail(err))
assert.EqualError(err, fmt.Sprintf("file %q has been altered", filepath.Join(tmpDir, "a.txt")))
}
// TestUnalteredCheckerNonExistant tests running the unaltered file checker
// in the case where the target file does not exist. If the files has no history,
// the checker should pass.
func TestUnalteredCheckerNonExistant(t *testing.T) {
assert := assert.New(t)
hashPath := "a.txt"
wd, repo, cleanup := tempGitRepo(assert)
defer cleanup()
// Temporary repo.
tmpDir, err := ioutil.TempDir("", "test")
assert.Nil(err)
defer os.RemoveAll(tmpDir)
trgRepo, err := git.PlainInit(tmpDir, false)
assert.Nil(err)
ctx := plan.Setup{
Source: plan.RepoSetup{
Path: wd,
Git: repo,
},
Target: plan.RepoSetup{
Path: tmpDir,
Git: trgRepo,
},
}
checker := plan.FileUnalteredChecker{}
checker.Params.SourceRepo = plan.SourceRepo
checker.Params.TargetRepo = plan.TargetRepo
err = checker.Check(hashPath, ctx)
assert.Nil(err)
}

View File

@ -0,0 +1,111 @@
package git
import (
"crypto/sha1" //nolint
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
git "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/pkg/errors"
)
// ErrNotFound signifies the file was not found.
var ErrNotFound = fmt.Errorf("not found")
// FileHistory will trace all the versions of a file in the git repository
// and return a list of sha1 hashes of that file.
func FileHistory(path string, repo *git.Repository) ([]string, error) {
logOpts := git.LogOptions{
FileName: &path,
All: true,
}
commits, err := repo.Log(&logOpts)
if errors.Is(err, plumbing.ErrReferenceNotFound) {
return nil, ErrNotFound
}
if err != nil {
return nil, fmt.Errorf("failed to get commits for path %q: %v", path, err)
}
defer commits.Close()
hashHistory := []string{}
cerr := commits.ForEach(func(c *object.Commit) error {
root, err := repo.TreeObject(c.TreeHash)
if err != nil {
return fmt.Errorf("failed to get commit tree: %v", err)
}
f, err := traverseTree(root, path)
if err == object.ErrFileNotFound || err == object.ErrDirectoryNotFound {
// Ignoring file not found errors.
return nil
} else if err != nil {
return err
}
sum, err := getReaderHash(f)
f.Close()
if err != nil {
return err
}
hashHistory = append(hashHistory, sum)
return nil
})
if cerr != nil && cerr != io.EOF {
return nil, cerr
}
if len(hashHistory) == 0 {
return nil, ErrNotFound
}
return hashHistory, nil
}
func traverseTree(root *object.Tree, path string) (io.ReadCloser, error) {
dirName, fileName := filepath.Split(path)
var err error
t := root
if dirName != "" {
t, err = root.Tree(filepath.Clean(dirName))
if err == object.ErrDirectoryNotFound {
return nil, err
} else if err != nil {
return nil, fmt.Errorf("failed to traverse tree to %q: %v", dirName, err)
}
}
f, err := t.File(fileName)
if err == object.ErrFileNotFound {
return nil, err
} else if err != nil {
return nil, fmt.Errorf("failed to lookup file %q: %v", fileName, err)
}
reader, err := f.Reader()
if err != nil {
return nil, fmt.Errorf("failed to open %q: %v", path, err)
}
return reader, nil
}
func getReaderHash(r io.Reader) (string, error) {
h := sha1.New() // nolint
_, err := io.Copy(h, r)
if err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
// GetFileHash calculates the sha1 hash sum of the file.
func GetFileHash(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
sum, err := getReaderHash(f)
if err != nil {
return "", err
}
return sum, nil
}

View File

@ -0,0 +1,80 @@
package git_test
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"time"
git "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/stretchr/testify/assert"
gitutil "github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan/git"
)
var fileContents = []byte("abcdefg")
func TestFileHistory(t *testing.T) {
assert := assert.New(t)
dir, err := ioutil.TempDir("", "repo")
assert.Nil(err)
defer os.RemoveAll(dir)
// Initialize a repository.
repo, err := git.PlainInit(dir, false)
assert.Nil(err)
w, err := repo.Worktree()
assert.Nil(err)
// Create repository files.
err = ioutil.WriteFile(filepath.Join(dir, "test"), fileContents, 0644)
assert.Nil(err)
_, err = w.Add("test")
assert.Nil(err)
sig := &object.Signature{
Name: "test",
Email: "test@example.com",
When: time.Now(),
}
_, err = w.Commit("initial commit", &git.CommitOptions{Author: sig})
assert.Nil(err)
pathA := "a.txt"
err = ioutil.WriteFile(filepath.Join(dir, pathA), fileContents, 0644)
assert.Nil(err)
pathB := "b.txt"
err = ioutil.WriteFile(filepath.Join(dir, pathB), fileContents, 0644)
assert.Nil(err)
_, err = w.Add(pathA)
assert.Nil(err)
_, err = w.Add(pathB)
assert.Nil(err)
_, err = w.Commit("add files", &git.CommitOptions{Author: sig})
assert.Nil(err)
// Delete one of the files.
_, err = w.Remove(pathB)
assert.Nil(err)
_, err = w.Commit("remove file b.txt", &git.CommitOptions{
Author: sig,
All: true,
})
assert.Nil(err)
repo, err = git.PlainOpen(dir)
assert.Nil(err)
// Call file history on an existing file.
sums, err := gitutil.FileHistory("a.txt", repo)
assert.Nil(err)
assert.Equal([]string{"2fb5e13419fc89246865e7a324f476ec624e8740"}, sums)
// Calling with a non-existent file returns error.
sums, err = gitutil.FileHistory(filepath.Join(dir, "nosuch_testfile.txt"), repo)
assert.Equal(gitutil.ErrNotFound, err)
assert.Nil(sums)
// Calling with a non-existent file that was in git history returns no error.
_, err = gitutil.FileHistory(pathB, repo)
assert.Nil(err)
}

View File

@ -0,0 +1,245 @@
// Package plan handles the synchronization plan.
//
// Each synchronization plan is a set of checks and actions to perform on specified paths
// that will result in the "plugin" repository being updated.
package plan
import (
"encoding/json"
"fmt"
"os"
"sort"
)
// Plan defines the plan for synchronizing a target and a source directory.
type Plan struct {
Checks []Check `json:"checks"`
// Each set of paths has multiple actions associated, each a fallback for the one
// previous to it.
Actions []ActionSet
}
// UnmarshalJSON implements the `json.Unmarshaler` interface.
func (p *Plan) UnmarshalJSON(raw []byte) error {
var t jsonPlan
if err := json.Unmarshal(raw, &t); err != nil {
return err
}
p.Checks = make([]Check, len(t.Checks))
for i, check := range t.Checks {
c, err := parseCheck(check.Type, check.Params)
if err != nil {
return fmt.Errorf("failed to parse check %q: %v", check.Type, err)
}
p.Checks[i] = c
}
if len(t.Actions) > 0 {
p.Actions = make([]ActionSet, len(t.Actions))
}
for i, actionSet := range t.Actions {
var err error
pathActions := make([]Action, len(actionSet.Actions))
for i, action := range actionSet.Actions {
var actionConditions []Check
if len(action.Conditions) > 0 {
actionConditions = make([]Check, len(action.Conditions))
}
for j, check := range action.Conditions {
actionConditions[j], err = parseCheck(check.Type, check.Params)
if err != nil {
return err
}
}
pathActions[i], err = parseAction(action.Type, action.Params, actionConditions)
if err != nil {
return err
}
}
p.Actions[i] = ActionSet{
Paths: actionSet.Paths,
Actions: pathActions,
}
}
return nil
}
// Execute executes the synchronization plan.
func (p *Plan) Execute(c Setup) error {
c.Logf("running pre-checks")
for _, check := range p.Checks {
err := check.Check("", c) // For pre-sync checks, the path is ignored.
if err != nil {
return fmt.Errorf("failed check: %v", err)
}
}
result := []pathResult{}
c.Logf("running actions")
for _, actions := range p.Actions {
PATHS_LOOP:
for _, path := range actions.Paths {
c.Logf("syncing path %q", path)
ACTIONS_LOOP:
for i, action := range actions.Actions {
c.Logf("running action for path %q", path)
err := action.Check(path, c)
if IsCheckFail(err) {
c.Logf("check failed, not running action: %v", err)
// If a check for an action fails, we switch to
// the next action associated with the path.
if i == len(actions.Actions)-1 { // no actions to fallback to.
c.Logf("path %q not handled - no more fallbacks", path)
result = append(result,
pathResult{
Path: path,
Status: statusFailed,
Message: fmt.Sprintf("check failed, %s", err.Error()),
})
}
continue ACTIONS_LOOP
} else if err != nil {
c.LogErrorf("unexpected error when running check: %v", err)
return fmt.Errorf("failed to run checks for action: %v", err)
}
err = action.Run(path, c)
if err != nil {
c.LogErrorf("action failed: %v", err)
return fmt.Errorf("action failed: %v", err)
}
c.Logf("path %q sync'ed successfully", path)
result = append(result,
pathResult{
Path: path,
Status: statusUpdated,
})
continue PATHS_LOOP
}
}
}
// Print execution result.
sort.SliceStable(result, func(i, j int) bool { return result[i].Path < result[j].Path })
for _, res := range result {
if res.Message != "" {
fmt.Fprintf(os.Stdout, "%s\t%s: %s\n", res.Status, res.Path, res.Message)
} else {
fmt.Fprintf(os.Stdout, "%s\t%s\n", res.Status, res.Path)
}
}
return nil
}
// Check returns an error if the condition fails.
type Check interface {
Check(string, Setup) error
}
// ActionSet is a set of actions along with a set of paths to
// perform those actions on.
type ActionSet struct {
Paths []string
Actions []Action
}
// Action runs the defined action.
type Action interface {
// Run performs the action on the specified path.
Run(string, Setup) error
// Check runs checks associated with the action
// before running it.
Check(string, Setup) error
}
// jsonPlan is used to unmarshal Plan structures.
type jsonPlan struct {
Checks []struct {
Type string `json:"type"`
Params json.RawMessage `json:"params,omitempty"`
}
Actions []struct {
Paths []string `json:"paths"`
Actions []struct {
Type string `json:"type"`
Params json.RawMessage `json:"params,omitempty"`
Conditions []struct {
Type string `json:"type"`
Params json.RawMessage `json:"params"`
}
}
}
}
func parseCheck(checkType string, rawParams json.RawMessage) (Check, error) {
var c Check
var params interface{}
switch checkType {
case "repo_is_clean":
tc := RepoIsCleanChecker{}
params = &tc.Params
c = &tc
case "exists":
tc := PathExistsChecker{}
params = &tc.Params
c = &tc
case "file_unaltered":
tc := FileUnalteredChecker{}
params = &tc.Params
c = &tc
default:
return nil, fmt.Errorf("unknown checker type %q", checkType)
}
if len(rawParams) > 0 {
err := json.Unmarshal(rawParams, params)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal params for %s: %v", checkType, err)
}
}
return c, nil
}
func parseAction(actionType string, rawParams json.RawMessage, checks []Check) (Action, error) {
var a Action
var params interface{}
switch actionType {
case "overwrite_file":
ta := OverwriteFileAction{}
ta.Conditions = checks
params = &ta.Params
a = &ta
case "overwrite_directory":
ta := OverwriteDirectoryAction{}
ta.Conditions = checks
params = &ta.Params
a = &ta
default:
return nil, fmt.Errorf("unknown action type %q", actionType)
}
if len(rawParams) > 0 {
err := json.Unmarshal(rawParams, params)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal params for %s: %v", actionType, err)
}
}
return a, nil
}
// pathResult contains the result of synchronizing a path.
type pathResult struct {
Path string
Status status
Message string
}
type status string
const (
statusUpdated status = "UPDATED"
statusFailed status = "FAILED"
)

View File

@ -0,0 +1,253 @@
package plan_test
import (
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan"
)
func TestUnmarshalPlan(t *testing.T) {
assert := assert.New(t)
rawJSON := []byte(`
{
"checks": [
{"type": "repo_is_clean", "params": {"repo": "template"}}
],
"actions": [
{
"paths": ["abc"],
"actions": [{
"type": "overwrite_file",
"params": {"create": true},
"conditions": [{
"type": "exists",
"params": {"repo": "plugin"}
}]
}]
}
]
}`)
var p plan.Plan
err := json.Unmarshal(rawJSON, &p)
assert.Nil(err)
expectedCheck := plan.RepoIsCleanChecker{}
expectedCheck.Params.Repo = "template"
expectedAction := plan.OverwriteFileAction{}
expectedAction.Params.Create = true
expectedActionCheck := plan.PathExistsChecker{}
expectedActionCheck.Params.Repo = "plugin"
expectedAction.Conditions = []plan.Check{&expectedActionCheck}
expected := plan.Plan{
Checks: []plan.Check{&expectedCheck},
Actions: []plan.ActionSet{{
Paths: []string{"abc"},
Actions: []plan.Action{
&expectedAction,
},
}},
}
assert.Equal(expected, p)
}
type mockCheck struct {
returnErr error
calledWith string // Path parameter the check was called with.
}
// Check implements the plan.Check interface.
func (m *mockCheck) Check(path string, c plan.Setup) error {
m.calledWith = path
return m.returnErr
}
type mockAction struct {
runErr error // Error to be returned by Run.
checkErr error // Error to be returned by Check.
calledWith string
}
// Check implements plan.Action interface.
func (m *mockAction) Check(path string, c plan.Setup) error {
return m.checkErr
}
// Run implements plan.Action interface.
func (m *mockAction) Run(path string, c plan.Setup) error {
m.calledWith = path
return m.runErr
}
// TestRunPlanSuccessfully tests a successful execution of a sync plan.
func TestRunPlanSuccessfully(t *testing.T) {
assert := assert.New(t)
setup := plan.Setup{} // mocked actions and checks won't be accessing the setup.
preCheck := &mockCheck{}
action1 := &mockAction{}
action2 := &mockAction{}
p := &plan.Plan{
Checks: []plan.Check{preCheck},
Actions: []plan.ActionSet{{
Paths: []string{"somepath"},
Actions: []plan.Action{
action1,
action2,
},
}},
}
err := p.Execute(setup)
assert.Nil(err)
assert.Equal("", preCheck.calledWith)
assert.Equal("somepath", action1.calledWith)
assert.Equal("", action2.calledWith) // second action was not called.
}
// TestRunPlanPreCheckFail checks the scenario where a sync plan precheck
// fails, aborting the whole operation.
func TestRunPlanPreCheckFail(t *testing.T) {
assert := assert.New(t)
setup := plan.Setup{} // mocked actions and checks won't be accessing the setup.
preCheck := &mockCheck{returnErr: plan.CheckFailf("check failed")}
action1 := &mockAction{}
action2 := &mockAction{}
p := &plan.Plan{
Checks: []plan.Check{preCheck},
Actions: []plan.ActionSet{{
Paths: []string{"somepath"},
Actions: []plan.Action{
action1,
action2,
},
}},
}
err := p.Execute(setup)
assert.EqualError(err, "failed check: check failed")
assert.Equal("", preCheck.calledWith)
// None of the actions were executed.
assert.Equal("", action1.calledWith)
assert.Equal("", action2.calledWith)
}
// TestRunPlanActionCheckFails tests the situation where an action's
// check returns a recoverable error, forcing the plan to execute the fallback action.
func TestRunPlanActionCheckFails(t *testing.T) {
assert := assert.New(t)
setup := plan.Setup{} // mocked actions and checks won't be accessing the setup.
action1 := &mockAction{checkErr: plan.CheckFailf("action check failed")}
action2 := &mockAction{}
p := &plan.Plan{
Actions: []plan.ActionSet{{
Paths: []string{"somepath"},
Actions: []plan.Action{
action1,
action2,
},
}},
}
err := p.Execute(setup)
assert.Nil(err)
assert.Equal("", action1.calledWith) // First action was not run.
assert.Equal("somepath", action2.calledWith) // Second action was run.
}
// TestRunPlanNoFallbacks tests the case where an action's check fails,
// but there are not more fallback actions for that path.
func TestRunPlanNoFallbacks(t *testing.T) {
assert := assert.New(t)
setup := plan.Setup{} // mocked actions and checks won't be accessing the setup.
action1 := &mockAction{checkErr: plan.CheckFailf("fail")}
action2 := &mockAction{checkErr: plan.CheckFailf("fail")}
p := &plan.Plan{
Actions: []plan.ActionSet{{
Paths: []string{"somepath"},
Actions: []plan.Action{
action1,
action2,
},
}},
}
err := p.Execute(setup)
assert.Nil(err)
// both actions were not executed.
assert.Equal("", action1.calledWith)
assert.Equal("", action2.calledWith)
}
// TestRunPlanCheckError tests the scenario where a plan check fails with
// an unexpected error. Plan execution is aborted.
func TestRunPlanCheckError(t *testing.T) {
assert := assert.New(t)
setup := plan.Setup{} // mocked actions and checks won't be accessing the setup.
preCheck := &mockCheck{returnErr: fmt.Errorf("fail")}
action1 := &mockAction{}
action2 := &mockAction{}
p := &plan.Plan{
Checks: []plan.Check{preCheck},
Actions: []plan.ActionSet{{
Paths: []string{"somepath"},
Actions: []plan.Action{
action1,
action2,
},
}},
}
err := p.Execute(setup)
assert.EqualError(err, "failed check: fail")
assert.Equal("", preCheck.calledWith)
// Actions were not run.
assert.Equal("", action1.calledWith)
assert.Equal("", action2.calledWith)
}
// TestRunPlanActionError tests the scenario where an action fails,
// aborting the whole sync process.
func TestRunPlanActionError(t *testing.T) {
assert := assert.New(t)
setup := plan.Setup{} // mocked actions and checks won't be accessing the setup.
preCheck := &mockCheck{}
action1 := &mockAction{runErr: fmt.Errorf("fail")}
action2 := &mockAction{}
p := &plan.Plan{
Checks: []plan.Check{preCheck},
Actions: []plan.ActionSet{{
Paths: []string{"somepath"},
Actions: []plan.Action{
action1,
action2,
},
}},
}
err := p.Execute(setup)
assert.EqualError(err, "action failed: fail")
assert.Equal("", preCheck.calledWith)
assert.Equal("somepath", action1.calledWith)
assert.Equal("", action2.calledWith) // second action was not called.
}

View File

@ -0,0 +1,80 @@
package plan
import (
"fmt"
"os"
"path/filepath"
git "github.com/go-git/go-git/v5"
)
// RepoID identifies a repository - either plugin or template.
type RepoID string
const (
// SourceRepo is the id of the template repository (source).
SourceRepo RepoID = "source"
// TargetRepo is the id of the plugin repository (target).
TargetRepo RepoID = "target"
)
// Setup contains information about both parties
// in the sync: the plugin repository being updated
// and the source of the update - the template repo.
type Setup struct {
Source RepoSetup
Target RepoSetup
VerboseLogging bool
}
// Logf logs the provided message.
// If verbose output is not enabled, the message will not be printed.
func (c Setup) Logf(tpl string, args ...interface{}) {
if c.VerboseLogging {
fmt.Fprintf(os.Stderr, tpl+"\n", args...)
}
}
// LogErrorf logs the provided error message.
func (c Setup) LogErrorf(tpl string, args ...interface{}) {
fmt.Fprintf(os.Stderr, tpl+"\n", args...)
}
// GetRepo is a helper to get the required repo setup.
// If the target parameter is not one of "plugin" or "template",
// the function panics.
func (c Setup) GetRepo(r RepoID) RepoSetup {
switch r {
case TargetRepo:
return c.Target
case SourceRepo:
return c.Source
default:
panic(fmt.Sprintf("cannot get repository setup %q", r))
}
}
// PathInRepo returns the full path of a file in the specified repository.
func (c Setup) PathInRepo(repo RepoID, path string) string {
r := c.GetRepo(repo)
return filepath.Join(r.Path, path)
}
// RepoSetup contains relevant information
// about a single repository (either source or target).
type RepoSetup struct {
Git *git.Repository
Path string
}
// GetRepoSetup returns the repository setup for the specified path.
func GetRepoSetup(path string) (RepoSetup, error) {
repo, err := git.PlainOpen(path)
if err != nil {
return RepoSetup{}, fmt.Errorf("failed to access git repository at %q: %v", path, err)
}
return RepoSetup{
Git: repo,
Path: path,
}, nil
}

View File

@ -0,0 +1 @@
a

View File

@ -0,0 +1 @@
c

14
mattermost-plugin/go.mod Normal file
View File

@ -0,0 +1,14 @@
module github.com/mattermost/focalboard/mattermost-plugin
go 1.12
replace github.com/mattermost/focalboard/server => ../server
replace github.com/mattermost/mattermost-server/v5 => ../../mattermost-server
require (
github.com/mattermost/focalboard/server v0.0.0-20210331160003-42eaa744c065
github.com/mattermost/mattermost-server/v5 v5.34.2
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.7.0
)

1619
mattermost-plugin/go.sum Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
{
"id": "focalboard",
"name": "Focalboard",
"description": "This provides focalboard integration with mattermost.",
"homepage_url": "https://github.com/mattermost/mattermost-plugin-focalboard",
"support_url": "https://github.com/mattermost/mattermost-plugin-focalboard/issues",
"release_notes_url": "https://github.com/mattermost/mattermost-plugin-focalboard/releases/tag/v0.1.0",
"icon_path": "assets/starter-template-icon.svg",
"version": "0.2.0",
"min_server_version": "5.12.0",
"server": {
"executables": {
"linux-amd64": "server/dist/plugin-linux-amd64",
"darwin-amd64": "server/dist/plugin-darwin-amd64",
"windows-amd64": "server/dist/plugin-windows-amd64.exe"
}
},
"webapp": {
"bundle_path": "webapp/dist/main.js"
},
"settings_schema": {
"header": "",
"footer": "",
"settings": []
}
}

View File

@ -0,0 +1 @@
Hello from the static files public folder for the com.mattermost.plugin-starter-template plugin!

2
mattermost-plugin/server/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
coverage.txt
dist

View File

@ -0,0 +1,83 @@
package main
import (
"reflect"
"github.com/pkg/errors"
)
// configuration captures the plugin's external configuration as exposed in the Mattermost server
// configuration, as well as values computed from the configuration. Any public fields will be
// deserialized from the Mattermost server configuration in OnConfigurationChange.
//
// As plugins are inherently concurrent (hooks being called asynchronously), and the plugin
// configuration can change at any time, access to the configuration must be synchronized. The
// strategy used in this plugin is to guard a pointer to the configuration, and clone the entire
// struct whenever it changes. You may replace this with whatever strategy you choose.
//
// If you add non-reference types to your configuration struct, be sure to rewrite Clone as a deep
// copy appropriate for your types.
type configuration struct {
}
// Clone shallow copies the configuration. Your implementation may require a deep copy if
// your configuration has reference types.
func (c *configuration) Clone() *configuration {
var clone = *c
return &clone
}
// getConfiguration retrieves the active configuration under lock, making it safe to use
// concurrently. The active configuration may change underneath the client of this method, but
// the struct returned by this API call is considered immutable.
func (p *Plugin) getConfiguration() *configuration {
p.configurationLock.RLock()
defer p.configurationLock.RUnlock()
if p.configuration == nil {
return &configuration{}
}
return p.configuration
}
// setConfiguration replaces the active configuration under lock.
//
// Do not call setConfiguration while holding the configurationLock, as sync.Mutex is not
// reentrant. In particular, avoid using the plugin API entirely, as this may in turn trigger a
// hook back into the plugin. If that hook attempts to acquire this lock, a deadlock may occur.
//
// This method panics if setConfiguration is called with the existing configuration. This almost
// certainly means that the configuration was modified without being cloned and may result in
// an unsafe access.
func (p *Plugin) setConfiguration(configuration *configuration) {
p.configurationLock.Lock()
defer p.configurationLock.Unlock()
if configuration != nil && p.configuration == configuration {
// Ignore assignment if the configuration struct is empty. Go will optimize the
// allocation for same to point at the same memory address, breaking the check
// above.
if reflect.ValueOf(*configuration).NumField() == 0 {
return
}
panic("setConfiguration called with the existing configuration")
}
p.configuration = configuration
}
// OnConfigurationChange is invoked when configuration changes may have been made.
func (p *Plugin) OnConfigurationChange() error {
var configuration = new(configuration)
// Load the public configuration fields from the Mattermost server configuration.
if err := p.API.LoadPluginConfiguration(configuration); err != nil {
return errors.Wrap(err, "failed to load plugin configuration")
}
p.setConfiguration(configuration)
return nil
}

View File

@ -0,0 +1,9 @@
package main
import (
"github.com/mattermost/mattermost-server/v5/plugin"
)
func main() {
plugin.ClientMain(&Plugin{})
}

45
mattermost-plugin/server/manifest.go generated Normal file
View File

@ -0,0 +1,45 @@
// This file is automatically generated. Do not modify it manually.
package main
import (
"strings"
"github.com/mattermost/mattermost-server/v5/model"
)
var manifest *model.Manifest
const manifestStr = `
{
"id": "focalboard",
"name": "Focalboard",
"description": "This provides focalboard integration with mattermost.",
"homepage_url": "https://github.com/mattermost/mattermost-plugin-focalboard",
"support_url": "https://github.com/mattermost/mattermost-plugin-focalboard/issues",
"release_notes_url": "https://github.com/mattermost/mattermost-plugin-focalboard/releases/tag/v0.1.0",
"icon_path": "assets/starter-template-icon.svg",
"version": "0.2.0",
"min_server_version": "5.12.0",
"server": {
"executables": {
"linux-amd64": "server/dist/plugin-linux-amd64",
"darwin-amd64": "server/dist/plugin-darwin-amd64",
"windows-amd64": "server/dist/plugin-windows-amd64.exe"
},
"executable": ""
},
"webapp": {
"bundle_path": "webapp/dist/main.js"
},
"settings_schema": {
"header": "",
"footer": "",
"settings": []
}
}
`
func init() {
manifest = model.ManifestFromJson(strings.NewReader(manifestStr))
}

View File

@ -0,0 +1,129 @@
package main
import (
"fmt"
"net/http"
"sync"
"github.com/mattermost/focalboard/server/server"
"github.com/mattermost/focalboard/server/services/config"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/plugin"
)
// Plugin implements the interface expected by the Mattermost server to communicate between the server and plugin processes.
type Plugin struct {
plugin.MattermostPlugin
// configurationLock synchronizes access to the configuration.
configurationLock sync.RWMutex
// configuration is the active plugin configuration. Consult getConfiguration and
// setConfiguration for usage.
configuration *configuration
server *server.Server
wsHub *WSHub
}
type WSHub struct {
API plugin.API
handleWSMessage func(data []byte)
}
func (h *WSHub) SendWSMessage(data []byte) {
h.API.PublishPluginClusterEvent(model.PluginClusterEvent{
Id: "websocket_event",
Data: data,
}, model.PluginClusterEventSendOptions{
SendType: model.PluginClusterEventSendTypeReliable,
})
}
func (h *WSHub) SetReceiveWSMessage(handler func(data []byte)) {
h.handleWSMessage = handler
}
func (p *Plugin) OnActivate() error {
mmconfig := p.API.GetUnsanitizedConfig()
filesS3Config := config.AmazonS3Config{}
if mmconfig.FileSettings.AmazonS3AccessKeyId != nil {
filesS3Config.AccessKeyId = *mmconfig.FileSettings.AmazonS3AccessKeyId
}
if mmconfig.FileSettings.AmazonS3SecretAccessKey != nil {
filesS3Config.SecretAccessKey = *mmconfig.FileSettings.AmazonS3SecretAccessKey
}
if mmconfig.FileSettings.AmazonS3Bucket != nil {
filesS3Config.Bucket = *mmconfig.FileSettings.AmazonS3Bucket
}
if mmconfig.FileSettings.AmazonS3PathPrefix != nil {
filesS3Config.PathPrefix = *mmconfig.FileSettings.AmazonS3PathPrefix
}
if mmconfig.FileSettings.AmazonS3Region != nil {
filesS3Config.Region = *mmconfig.FileSettings.AmazonS3Region
}
if mmconfig.FileSettings.AmazonS3Endpoint != nil {
filesS3Config.Endpoint = *mmconfig.FileSettings.AmazonS3Endpoint
}
if mmconfig.FileSettings.AmazonS3SSL != nil {
filesS3Config.SSL = *mmconfig.FileSettings.AmazonS3SSL
}
if mmconfig.FileSettings.AmazonS3SignV2 != nil {
filesS3Config.SignV2 = *mmconfig.FileSettings.AmazonS3SignV2
}
if mmconfig.FileSettings.AmazonS3SSE != nil {
filesS3Config.SSE = *mmconfig.FileSettings.AmazonS3SSE
}
if mmconfig.FileSettings.AmazonS3Trace != nil {
filesS3Config.Trace = *mmconfig.FileSettings.AmazonS3Trace
}
server, err := server.New(&config.Configuration{
ServerRoot: *mmconfig.ServiceSettings.SiteURL + "/plugins/focalboard",
Port: -1,
DBType: *mmconfig.SqlSettings.DriverName,
DBConfigString: *mmconfig.SqlSettings.DataSource,
DBTablePrefix: "focalboard_",
UseSSL: false,
SecureCookie: true,
WebPath: "./plugins/focalboard/pack",
FilesDriver: *mmconfig.FileSettings.DriverName,
FilesPath: *mmconfig.FileSettings.Directory,
FilesS3Config: filesS3Config,
Telemetry: true,
WebhookUpdate: []string{},
SessionExpireTime: 2592000,
SessionRefreshTime: 18000,
LocalOnly: false,
EnableLocalMode: false,
LocalModeSocketLocation: "",
AuthMode: "mattermost",
}, "")
if err != nil {
fmt.Println("ERROR INITIALIZING THE SERVER", err)
return err
}
p.wsHub = &WSHub{API: p.API}
server.SetWSHub(p.wsHub)
p.server = server
return server.Start()
}
func (p *Plugin) OnPluginClusterEvent(c *plugin.Context, ev model.PluginClusterEvent) {
if ev.Id == "websocket_event" {
p.wsHub.handleWSMessage(ev.Data)
}
}
func (p *Plugin) OnDeactivate() error {
return p.server.Shutdown()
}
// ServeHTTP demonstrates a plugin that handles HTTP requests by greeting the world.
func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
router := p.server.GetRootRouter()
router.ServeHTTP(w, r)
}
// See https://developers.mattermost.com/extend/plugins/server/reference/

View File

@ -0,0 +1,28 @@
package main
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestServeHTTP(t *testing.T) {
assert := assert.New(t)
plugin := Plugin{}
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/", nil)
plugin.ServeHTTP(nil, w, r)
result := w.Result()
assert.NotNil(result)
defer result.Body.Close()
bodyBytes, err := ioutil.ReadAll(result.Body)
assert.Nil(err)
bodyString := string(bodyBytes)
assert.Equal("Hello, world!", bodyString)
}

View File

@ -0,0 +1,669 @@
{
"extends": [
"eslint:recommended",
"plugin:react-hooks/recommended"
],
"parserOptions": {
"ecmaVersion": 8,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"impliedStrict": true,
"modules": true,
"experimentalObjectRestSpread": true
}
},
"parser": "babel-eslint",
"plugins": [
"react",
"import"
],
"env": {
"browser": true,
"node": true,
"jquery": true,
"es6": true,
"jest": true
},
"globals": {
"jest": true,
"describe": true,
"it": true,
"expect": true,
"before": true,
"after": true,
"beforeEach": true
},
"settings": {
"import/resolver": "webpack"
},
"rules": {
"array-bracket-spacing": [
2,
"never"
],
"array-callback-return": 2,
"arrow-body-style": 0,
"arrow-parens": [
2,
"always"
],
"arrow-spacing": [
2,
{
"before": true,
"after": true
}
],
"block-scoped-var": 2,
"brace-style": [
2,
"1tbs",
{
"allowSingleLine": false
}
],
"capitalized-comments": 0,
"class-methods-use-this": 0,
"comma-dangle": [
2,
"always-multiline"
],
"comma-spacing": [
2,
{
"before": false,
"after": true
}
],
"comma-style": [
2,
"last"
],
"complexity": [
0,
10
],
"computed-property-spacing": [
2,
"never"
],
"consistent-return": 2,
"consistent-this": [
2,
"self"
],
"constructor-super": 2,
"curly": [
2,
"all"
],
"dot-location": [
2,
"object"
],
"dot-notation": 2,
"eqeqeq": [
2,
"smart"
],
"func-call-spacing": [
2,
"never"
],
"func-name-matching": 0,
"func-names": 2,
"func-style": [
2,
"declaration",
{
"allowArrowFunctions": true
}
],
"generator-star-spacing": [
2,
{
"before": false,
"after": true
}
],
"global-require": 2,
"guard-for-in": 2,
"id-blacklist": 0,
"import/no-unresolved": 2,
"import/order": [
"error",
{
"newlines-between": "always-and-inside-groups",
"groups": [
"builtin",
"external",
[
"internal",
"parent"
],
"sibling",
"index"
]
}
],
"indent": [
2,
4,
{
"SwitchCase": 0
}
],
"jsx-quotes": [
2,
"prefer-single"
],
"key-spacing": [
2,
{
"beforeColon": false,
"afterColon": true,
"mode": "strict"
}
],
"keyword-spacing": [
2,
{
"before": true,
"after": true,
"overrides": {}
}
],
"line-comment-position": 0,
"linebreak-style": 2,
"lines-around-comment": [
2,
{
"beforeBlockComment": true,
"beforeLineComment": true,
"allowBlockStart": true,
"allowBlockEnd": true
}
],
"max-lines": [
1,
{
"max": 450,
"skipBlankLines": true,
"skipComments": false
}
],
"max-nested-callbacks": [
2,
{
"max": 2
}
],
"max-statements-per-line": [
2,
{
"max": 1
}
],
"multiline-ternary": [
1,
"never"
],
"new-cap": 2,
"new-parens": 2,
"newline-before-return": 0,
"newline-per-chained-call": 0,
"no-alert": 2,
"no-array-constructor": 2,
"no-await-in-loop": 2,
"no-caller": 2,
"no-case-declarations": 2,
"no-class-assign": 2,
"no-compare-neg-zero": 2,
"no-cond-assign": [
2,
"except-parens"
],
"no-confusing-arrow": 2,
"no-console": 2,
"no-const-assign": 2,
"no-constant-condition": 2,
"no-debugger": 2,
"no-div-regex": 2,
"no-dupe-args": 2,
"no-dupe-class-members": 2,
"no-dupe-keys": 2,
"no-duplicate-case": 2,
"no-duplicate-imports": [
2,
{
"includeExports": true
}
],
"no-else-return": 2,
"no-empty": 2,
"no-empty-function": 2,
"no-empty-pattern": 2,
"no-eval": 2,
"no-ex-assign": 2,
"no-extend-native": 2,
"no-extra-bind": 2,
"no-extra-label": 2,
"no-extra-parens": 0,
"no-extra-semi": 2,
"no-fallthrough": 2,
"no-floating-decimal": 2,
"no-func-assign": 2,
"no-global-assign": 2,
"no-implicit-coercion": 2,
"no-implicit-globals": 0,
"no-implied-eval": 2,
"no-inner-declarations": 0,
"no-invalid-regexp": 2,
"no-irregular-whitespace": 2,
"no-iterator": 2,
"no-labels": 2,
"no-lone-blocks": 2,
"no-lonely-if": 2,
"no-loop-func": 2,
"no-magic-numbers": [
0,
{
"ignore": [
-1,
0,
1,
2
],
"enforceConst": true,
"detectObjects": true
}
],
"no-mixed-operators": [
2,
{
"allowSamePrecedence": false
}
],
"no-mixed-spaces-and-tabs": 2,
"no-multi-assign": 2,
"no-multi-spaces": [
2,
{
"exceptions": {
"Property": false
}
}
],
"no-multi-str": 0,
"no-multiple-empty-lines": [
2,
{
"max": 1
}
],
"no-native-reassign": 2,
"no-negated-condition": 2,
"no-nested-ternary": 2,
"no-new": 2,
"no-new-func": 2,
"no-new-object": 2,
"no-new-symbol": 2,
"no-new-wrappers": 2,
"no-octal-escape": 2,
"no-param-reassign": 2,
"no-process-env": 2,
"no-process-exit": 2,
"no-proto": 2,
"no-redeclare": 2,
"no-return-assign": [
2,
"always"
],
"no-return-await": 2,
"no-script-url": 2,
"no-self-assign": [
2,
{
"props": true
}
],
"no-self-compare": 2,
"no-sequences": 2,
"no-shadow": [
2,
{
"hoist": "functions"
}
],
"no-shadow-restricted-names": 2,
"no-spaced-func": 2,
"no-tabs": 0,
"no-template-curly-in-string": 2,
"no-ternary": 0,
"no-this-before-super": 2,
"no-throw-literal": 2,
"no-trailing-spaces": [
2,
{
"skipBlankLines": false
}
],
"no-undef-init": 2,
"no-undefined": 2,
"no-underscore-dangle": 2,
"no-unexpected-multiline": 2,
"no-unmodified-loop-condition": 2,
"no-unneeded-ternary": [
2,
{
"defaultAssignment": false
}
],
"no-unreachable": 2,
"no-unsafe-finally": 2,
"no-unsafe-negation": 2,
"no-unused-expressions": 2,
"no-unused-vars": [
2,
{
"vars": "all",
"args": "after-used"
}
],
"no-use-before-define": [
2,
{
"classes": false,
"functions": false,
"variables": false
}
],
"no-useless-computed-key": 2,
"no-useless-concat": 2,
"no-useless-constructor": 2,
"no-useless-escape": 2,
"no-useless-rename": 2,
"no-useless-return": 2,
"no-var": 0,
"no-void": 2,
"no-warning-comments": 1,
"no-whitespace-before-property": 2,
"no-with": 2,
"object-curly-newline": 0,
"object-curly-spacing": [
2,
"never"
],
"object-property-newline": [
2,
{
"allowMultiplePropertiesPerLine": true
}
],
"object-shorthand": [
2,
"always"
],
"one-var": [
2,
"never"
],
"one-var-declaration-per-line": 0,
"operator-assignment": [
2,
"always"
],
"operator-linebreak": [
2,
"after"
],
"padded-blocks": [
2,
"never"
],
"prefer-arrow-callback": 2,
"prefer-const": 2,
"prefer-destructuring": 0,
"prefer-numeric-literals": 2,
"prefer-promise-reject-errors": 2,
"prefer-rest-params": 2,
"prefer-spread": 2,
"prefer-template": 0,
"quote-props": [
2,
"as-needed"
],
"quotes": [
2,
"single",
"avoid-escape"
],
"radix": 2,
"react/display-name": [
0,
{
"ignoreTranspilerName": false
}
],
"react/forbid-component-props": 0,
"react/forbid-elements": [
2,
{
"forbid": [
"embed"
]
}
],
"react/jsx-boolean-value": [
2,
"always"
],
"react/jsx-closing-bracket-location": [
2,
{
"location": "tag-aligned"
}
],
"react/jsx-curly-spacing": [
2,
"never"
],
"react/jsx-equals-spacing": [
2,
"never"
],
"react/jsx-filename-extension": 2,
"react/jsx-first-prop-new-line": [
2,
"multiline"
],
"react/jsx-handler-names": 0,
"react/jsx-indent": [
2,
4
],
"react/jsx-indent-props": [
2,
4
],
"react/jsx-key": 2,
"react/jsx-max-props-per-line": [
2,
{
"maximum": 1
}
],
"react/jsx-no-bind": 0,
"react/jsx-no-comment-textnodes": 2,
"react/jsx-no-duplicate-props": [
2,
{
"ignoreCase": false
}
],
"react/jsx-no-literals": 2,
"react/jsx-no-target-blank": 2,
"react/jsx-no-undef": 2,
"react/jsx-pascal-case": 2,
"react/jsx-tag-spacing": [
2,
{
"closingSlash": "never",
"beforeSelfClosing": "never",
"afterOpening": "never"
}
],
"react/jsx-uses-react": 2,
"react/jsx-uses-vars": 2,
"react/jsx-wrap-multilines": 2,
"react/no-array-index-key": 1,
"react/no-children-prop": 2,
"react/no-danger": 0,
"react/no-danger-with-children": 2,
"react/no-deprecated": 1,
"react/no-did-mount-set-state": 2,
"react/no-did-update-set-state": 2,
"react/no-direct-mutation-state": 2,
"react/no-find-dom-node": 1,
"react/no-is-mounted": 2,
"react/no-multi-comp": [
2,
{
"ignoreStateless": true
}
],
"react/no-render-return-value": 2,
"react/no-set-state": 0,
"react/no-string-refs": 0,
"react/no-unescaped-entities": 2,
"react/no-unknown-property": 2,
"react/no-unused-prop-types": [
1,
{
"skipShapeProps": true
}
],
"react/prefer-es6-class": 2,
"react/prefer-stateless-function": 2,
"react/prop-types": [
2,
{
"ignore": [
"location",
"history",
"component"
]
}
],
"react/require-default-props": 0,
"react/require-optimization": 1,
"react/require-render-return": 2,
"react/self-closing-comp": 2,
"react/sort-comp": 0,
"react/style-prop-object": 2,
"require-yield": 2,
"rest-spread-spacing": [
2,
"never"
],
"semi": [
2,
"always"
],
"semi-spacing": [
2,
{
"before": false,
"after": true
}
],
"sort-imports": 0,
"sort-keys": 0,
"space-before-blocks": [
2,
"always"
],
"space-before-function-paren": [
2,
{
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
}
],
"space-in-parens": [
2,
"never"
],
"space-infix-ops": 2,
"space-unary-ops": [
2,
{
"words": true,
"nonwords": false
}
],
"symbol-description": 2,
"template-curly-spacing": [
2,
"never"
],
"valid-typeof": [
2,
{
"requireStringLiterals": false
}
],
"vars-on-top": 0,
"wrap-iife": [
2,
"outside"
],
"wrap-regex": 2,
"yoda": [
2,
"never",
{
"exceptRange": false,
"onlyEquality": false
}
]
},
"overrides": [
{
"files": ["**/*.tsx", "**/*.ts"],
"extends": "plugin:@typescript-eslint/recommended",
"rules": {
"@typescript-eslint/ban-ts-ignore": 0,
"@typescript-eslint/ban-types": 1,
"@typescript-eslint/ban-ts-comment": 0,
"@typescript-eslint/no-var-requires": 0,
"@typescript-eslint/prefer-interface": 0,
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/indent": [
2,
4,
{
"SwitchCase": 0
}
],
"@typescript-eslint/no-use-before-define": [
2,
{
"classes": false,
"functions": false,
"variables": false
}
],
"react/jsx-filename-extension": [
1,
{
"extensions": [".jsx", ".tsx"]
}
]
}
}
]
}

3
mattermost-plugin/webapp/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.eslintcache
junit.xml
node_modules

View File

@ -0,0 +1 @@
save-exact=true

View File

@ -0,0 +1,46 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
const config = {
presets: [
['@babel/preset-env', {
targets: {
chrome: 66,
firefox: 60,
edge: 42,
safari: 12,
},
modules: false,
corejs: 3,
debug: false,
useBuiltIns: 'usage',
shippedProposals: true,
}],
['@babel/preset-react', {
useBuiltIns: true,
}],
['@babel/typescript', {
allExtensions: true,
isTSX: true,
}],
['@emotion/babel-preset-css-prop'],
],
plugins: [
'@babel/plugin-proposal-class-properties',
'@babel/plugin-syntax-dynamic-import',
'@babel/proposal-object-rest-spread',
'@babel/plugin-proposal-optional-chaining',
'babel-plugin-typescript-to-proptypes',
],
};
// Jest needs module transformation
config.env = {
test: {
presets: config.presets,
plugins: config.plugins,
},
};
config.env.test.presets[0][1].modules = 'auto';
module.exports = config;

View File

@ -0,0 +1 @@
{}

41389
mattermost-plugin/webapp/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,115 @@
{
"private": true,
"scripts": {
"build": "webpack --mode=production",
"build:watch": "webpack --mode=production --watch",
"debug": "webpack --mode=none",
"debug:watch": "webpack --mode=development --watch",
"lint": "eslint --ignore-pattern node_modules --ignore-pattern dist --ext .js --ext .jsx --ext tsx --ext ts . --quiet --cache",
"fix": "eslint --ignore-pattern node_modules --ignore-pattern dist --ext .js --ext .jsx --ext tsx --ext ts . --quiet --fix --cache",
"test": "jest --forceExit --detectOpenHandles --verbose",
"test:watch": "jest --watch",
"test-ci": "jest --forceExit --detectOpenHandles --maxWorkers=2",
"check-types": "tsc"
},
"devDependencies": {
"@babel/cli": "7.10.4",
"@babel/core": "7.10.4",
"@babel/plugin-proposal-class-properties": "7.10.4",
"@babel/plugin-proposal-object-rest-spread": "7.10.4",
"@babel/plugin-proposal-optional-chaining": "7.10.4",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/polyfill": "7.10.4",
"@babel/preset-env": "7.10.4",
"@babel/preset-react": "7.10.4",
"@babel/preset-typescript": "7.10.4",
"@babel/runtime": "7.10.4",
"@emotion/babel-preset-css-prop": "10.0.27",
"@emotion/core": "10.0.28",
"@types/enzyme": "3.10.5",
"@types/jest": "26.0.4",
"@types/node": "14.0.20",
"@types/react": "16.9.41",
"@types/react-dom": "16.9.8",
"@types/react-intl": "3.0.0",
"@types/react-redux": "7.1.9",
"@types/react-router-dom": "5.1.5",
"@types/react-transition-group": "4.4.0",
"@typescript-eslint/eslint-plugin": "3.6.0",
"@typescript-eslint/parser": "3.6.0",
"babel-eslint": "10.1.0",
"babel-jest": "26.1.0",
"babel-loader": "8.1.0",
"babel-plugin-typescript-to-proptypes": "1.3.2",
"css-loader": "3.6.0",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.2",
"enzyme-to-json": "3.5.0",
"eslint": "7.4.0",
"eslint-import-resolver-webpack": "0.12.2",
"eslint-plugin-import": "2.22.0",
"eslint-plugin-react": "7.20.3",
"eslint-plugin-react-hooks": "4.0.6",
"file-loader": "6.0.0",
"identity-obj-proxy": "3.0.0",
"jest": "26.1.0",
"jest-canvas-mock": "2.2.0",
"jest-junit": "11.0.1",
"mattermost-webapp": "github:mattermost/mattermost-webapp#23f5f93d9f12a7e2b5623e5cee6814366abd9a0f",
"node-sass": "4.14.1",
"sass-loader": "9.0.2",
"style-loader": "1.2.1",
"webpack": "4.43.0",
"webpack-cli": "3.3.12"
},
"dependencies": {
"core-js": "3.6.5",
"mattermost-redux": "5.27.0",
"react": "16.13.1",
"react-redux": "7.2.0",
"redux": "4.0.5",
"typescript": "3.9.6"
},
"jest": {
"snapshotSerializers": [
"<rootDir>/node_modules/enzyme-to-json/serializer"
],
"testPathIgnorePatterns": [
"/node_modules/",
"/non_npm_dependencies/"
],
"clearMocks": true,
"collectCoverageFrom": [
"src/**/*.{js,jsx}"
],
"coverageReporters": [
"lcov",
"text-summary"
],
"moduleNameMapper": {
"^.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "identity-obj-proxy",
"^.+\\.(css|less|scss)$": "identity-obj-proxy",
"^.*i18n.*\\.(json)$": "<rootDir>/tests/i18n_mock.json",
"^bundle-loader\\?lazy\\!(.*)$": "$1"
},
"moduleDirectories": [
"",
"node_modules",
"non_npm_dependencies"
],
"reporters": [
"default",
"jest-junit"
],
"transformIgnorePatterns": [
"node_modules/(?!react-native|react-router|mattermost-webapp)"
],
"setupFiles": [
"jest-canvas-mock"
],
"setupFilesAfterEnv": [
"<rootDir>/tests/setup.tsx"
],
"testURL": "http://localhost:8065"
}
}

View File

@ -0,0 +1,56 @@
import {Store, Action} from 'redux';
import {GlobalState} from 'mattermost-redux/types/store';
import manifest from './manifest';
// eslint-disable-next-line import/no-unresolved
import {PluginRegistry} from './types/mattermost-webapp';
const focalboardIcon = (
<svg
className='LogoWithNameIcon Icon'
viewBox='0 0 64 64'
width='24px'
height='24px'
>
<g opacity='0.56'>
<path
fill='rgba(var(--center-channel-color-rgb), 1)'
d='M33.071,12.289C20.408,8.822,6.018,15.578,1.395,29.232 c-4.655,13.75,2.719,28.67,16.469,33.325c13.75,4.655,28.67-2.719,33.326-16.469c3.804-11.235-0.462-22.701-8.976-29.249 l-0.46,4.871l-0.001,0c4.631,4.896,6.709,11.941,4.325,18.985c-3.362,9.931-14.447,15.151-24.76,11.66 C11.005,48.865,5.37,37.985,8.731,28.054c2.975-8.788,11.998-13.715,20.743-12.625v-0.001L33.071,12.289L33.071,12.289z M26.896,28.777c3.456-0.665,6.986,2.754,5.762,6.37c-0.854,2.522-3.67,3.85-6.291,2.962c-2.62-0.887-4.052-3.651-3.197-6.174 C23.743,30.238,25.204,29.083,26.896,28.777L26.896,28.777z M25.611,23.833c-1.786,0.323-3.45,1.104-4.812,2.258 c-1.299,1.101-2.319,2.545-2.898,4.258c-0.879,2.597-0.579,5.323,0.617,7.632c1.206,2.329,3.325,4.234,6.07,5.164 c2.744,0.929,5.584,0.701,7.959-0.417c2.352-1.107,4.246-3.091,5.125-5.688c0.555-1.639,0.633-3.254,0.344-4.761 c-0.21-1.093-0.615-2.134-1.174-3.091l1.019-5.107c0.189,0.187,0.374,0.378,0.552,0.574c1.75,1.919,3.008,4.283,3.508,6.877 c0.415,2.154,0.304,4.457-0.484,6.784c-1.239,3.661-3.898,6.453-7.193,8.005c-3.273,1.541-7.175,1.858-10.93,0.588 c-3.754-1.271-6.661-3.895-8.326-7.108c-1.674-3.233-2.09-7.065-0.851-10.728c0.819-2.419,2.26-4.46,4.097-6.016 c1.88-1.593,4.181-2.673,6.656-3.125l-0.001-0.004c1.759-0.339,3.522-0.313,5.213,0.016l-3.583,3.761 c-0.294,0.028-0.588,0.071-0.883,0.127H25.611z'
/>
<polygon
fill='rgba(var(--center-channel-color-rgb), 1)'
points='37.495,11.658 36.79,8.44 41.066,0.207 43.683,4.611 48.803,4.434 44.185,12.48 40.902,13.697 29.542,34.491 26.057,32.594'
/>
</g>
</svg>
);
export default class Plugin {
channelHeaderButtonId: string;
registry: PluginRegistry
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
public async initialize(registry: PluginRegistry, store: Store<GlobalState, Action<Record<string, unknown>>>) {
this.registry = registry;
this.channelHeaderButtonId = registry.registerChannelHeaderButtonAction(focalboardIcon, () => {
const currentChannel = store.getState().entities.channels.currentChannelId;
window.open(`${window.location.origin}/plugins/focalboard/workspace/${currentChannel}`);
}, '', 'Focalboard Workspace');
}
public uninitialize() {
if (this.channelHeaderButtonId) {
this.registry.unregisterComponent(this.channelHeaderButtonId);
}
}
}
declare global {
interface Window {
registerPlugin(id: string, plugin: Plugin): void
}
}
window.registerPlugin(manifest.id, new Plugin());

View File

@ -0,0 +1,13 @@
import manifest, {id, version} from './manifest';
test('Plugin manifest, id and version are defined', () => {
expect(manifest).toBeDefined();
expect(manifest.id).toBeDefined();
expect(manifest.version).toBeDefined();
});
// To ease migration, verify separate export of id and version.
test('Plugin id and version are defined', () => {
expect(id).toBeDefined();
expect(version).toBeDefined();
});

View File

@ -0,0 +1,5 @@
import manifest from '../../plugin.json';
export default manifest;
export const id = manifest.id;
export const version = manifest.version;

View File

@ -0,0 +1,5 @@
export interface PluginRegistry {
registerPostTypeComponent(typeName: string, component: React.ElementType)
// Add more if needed from https://developers.mattermost.com/extend/plugins/webapp/reference
}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,4 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import 'mattermost-webapp/tests/setup';

View File

@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "esnext",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"experimentalDecorators": true,
"jsx": "react",
"baseUrl": "./src",
"typeRoots": [ "./src/types", "./node_modules/@types"],
},
"include": [
"src"
],
"exclude": [
"dist",
"node_modules",
"!node_modules/@types"
]
}

View File

@ -0,0 +1,99 @@
const exec = require('child_process').exec;
const path = require('path');
const PLUGIN_ID = require('../plugin.json').id;
const NPM_TARGET = process.env.npm_lifecycle_event; //eslint-disable-line no-process-env
let mode = 'production';
let devtool = '';
if (NPM_TARGET === 'debug' || NPM_TARGET === 'debug:watch') {
mode = 'development';
devtool = 'source-map';
}
const plugins = [];
if (NPM_TARGET === 'build:watch' || NPM_TARGET === 'debug:watch') {
plugins.push({
apply: (compiler) => {
compiler.hooks.watchRun.tap('WatchStartPlugin', () => {
// eslint-disable-next-line no-console
console.log('Change detected. Rebuilding webapp.');
});
compiler.hooks.afterEmit.tap('AfterEmitPlugin', () => {
exec('cd .. && make deploy-from-watch', (err, stdout, stderr) => {
if (stdout) {
process.stdout.write(stdout);
}
if (stderr) {
process.stderr.write(stderr);
}
});
});
},
});
}
module.exports = {
entry: [
'./src/index.tsx',
],
resolve: {
modules: [
'src',
'node_modules',
path.resolve(__dirname),
],
extensions: ['*', '.js', '.jsx', '.ts', '.tsx'],
},
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
// Babel configuration is in babel.config.js because jest requires it to be there.
},
},
},
{
test: /\.(scss|css)$/,
use: [
'style-loader',
{
loader: 'css-loader',
},
{
loader: 'sass-loader',
options: {
sassOptions: {
includePaths: ['node_modules/compass-mixins/lib', 'sass'],
},
},
},
],
},
],
},
externals: {
react: 'React',
redux: 'Redux',
'react-redux': 'ReactRedux',
'prop-types': 'PropTypes',
'react-bootstrap': 'ReactBootstrap',
'react-router-dom': 'ReactRouterDom',
},
output: {
devtoolNamespace: PLUGIN_ID,
path: path.join(__dirname, '/dist'),
publicPath: '/',
filename: 'main.js',
},
devtool,
mode,
plugins,
};

View File

@ -33,16 +33,11 @@ const (
// ----------------------------------------------------------------------------------------------------
// REST APIs
type WorkspaceAuthenticator interface {
DoesUserHaveWorkspaceAccess(session *model.Session, workspaceID string) bool
GetWorkspace(session *model.Session, workspaceID string) *model.Workspace
}
type API struct {
appBuilder func() *app.App
authService string
singleUserToken string
WorkspaceAuthenticator WorkspaceAuthenticator
appBuilder func() *app.App
authService string
singleUserToken string
MattermostAuth bool
}
func NewAPI(appBuilder func() *app.App, singleUserToken string, authService string) *API {
@ -138,14 +133,21 @@ func (a *API) getContainerAllowingReadTokenForBlock(r *http.Request, blockID str
ctx := r.Context()
session, _ := ctx.Value("session").(*model.Session)
if a.WorkspaceAuthenticator == nil {
// Native auth: always use root workspace
if a.MattermostAuth {
// Workspace auth
vars := mux.Vars(r)
workspaceID := vars["workspaceID"]
container := store.Container{
WorkspaceID: "0",
WorkspaceID: workspaceID,
}
// Has session
if session != nil {
if workspaceID == "0" {
return &container, nil
}
// Has session and access to workspace
if session != nil && a.app().DoesUserHaveWorkspaceAccess(session.UserID, container.WorkspaceID) {
return &container, nil
}
@ -157,16 +159,13 @@ func (a *API) getContainerAllowingReadTokenForBlock(r *http.Request, blockID str
return nil, errors.New("Access denied to workspace")
}
// Workspace auth
vars := mux.Vars(r)
workspaceID := vars["workspaceID"]
// Native auth: always use root workspace
container := store.Container{
WorkspaceID: workspaceID,
WorkspaceID: "0",
}
// Has session and access to workspace
if session != nil && a.WorkspaceAuthenticator.DoesUserHaveWorkspaceAccess(session, container.WorkspaceID) {
// Has session
if session != nil {
return &container, nil
}
@ -896,18 +895,21 @@ func (a *API) handleGetWorkspace(w http.ResponseWriter, r *http.Request) {
var workspace *model.Workspace
var err error
if a.WorkspaceAuthenticator != nil {
if a.MattermostAuth {
vars := mux.Vars(r)
workspaceID := vars["workspaceID"]
ctx := r.Context()
session := ctx.Value("session").(*model.Session)
if !a.WorkspaceAuthenticator.DoesUserHaveWorkspaceAccess(session, workspaceID) {
if !a.app().DoesUserHaveWorkspaceAccess(session.UserID, workspaceID) {
errorResponse(w, http.StatusUnauthorized, "", nil)
return
}
workspace = a.WorkspaceAuthenticator.GetWorkspace(session, workspaceID)
workspace, err = a.app().GetWorkspace(workspaceID)
if err != nil {
errorResponse(w, http.StatusInternalServerError, "", err)
}
if workspace == nil {
errorResponse(w, http.StatusUnauthorized, "", nil)
return
@ -1029,8 +1031,13 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", contentType)
filePath := a.app().GetFilePath(workspaceID, rootID, filename)
http.ServeFile(w, r, filePath)
fileReader, err := a.app().GetFileReader(workspaceID, rootID, filename)
if err != nil {
errorResponse(w, http.StatusInternalServerError, "", err)
return
}
defer fileReader.Close()
http.ServeContent(w, r, filename, time.Now(), fileReader)
}
// FileUploadResponse is the response to a file upload

View File

@ -371,6 +371,23 @@ func (a *API) attachSession(handler func(w http.ResponseWriter, r *http.Request)
return
}
if a.MattermostAuth && r.Header.Get("Mattermost-User-Id") != "" {
userID := r.Header.Get("Mattermost-User-Id")
now := time.Now().Unix()
session := &model.Session{
ID: userID,
Token: userID,
UserID: userID,
AuthService: a.authService,
Props: map[string]interface{}{},
CreateAt: now,
UpdateAt: now,
}
ctx := context.WithValue(r.Context(), "session", session)
handler(w, r.WithContext(ctx))
return
}
session, err := a.app().GetSession(token)
if err != nil {
if required {

View File

@ -6,7 +6,7 @@ import (
"github.com/mattermost/focalboard/server/services/store"
"github.com/mattermost/focalboard/server/services/webhook"
"github.com/mattermost/focalboard/server/ws"
"github.com/mattermost/mattermost-server/v5/services/filesstore"
"github.com/mattermost/mattermost-server/v5/shared/filestore"
)
type App struct {
@ -14,7 +14,7 @@ type App struct {
store store.Store
auth *auth.Auth
wsServer *ws.Server
filesBackend filesstore.FileBackend
filesBackend filestore.FileBackend
webhook *webhook.Client
}
@ -23,7 +23,7 @@ func New(
store store.Store,
auth *auth.Auth,
wsServer *ws.Server,
filesBackend filesstore.FileBackend,
filesBackend filestore.FileBackend,
webhook *webhook.Client,
) *App {
return &App{

View File

@ -11,7 +11,7 @@ import (
"github.com/mattermost/focalboard/server/services/store/mockstore"
"github.com/mattermost/focalboard/server/services/webhook"
"github.com/mattermost/focalboard/server/ws"
"github.com/mattermost/mattermost-server/v5/services/filesstore/mocks"
"github.com/mattermost/mattermost-server/v5/shared/filestore/mocks"
"github.com/stretchr/testify/require"
)
@ -22,7 +22,7 @@ func TestGetParentID(t *testing.T) {
store := mockstore.NewMockStore(ctrl)
auth := auth.New(&cfg, store)
sessionToken := "TESTTOKEN"
wsserver := ws.NewServer(auth, sessionToken)
wsserver := ws.NewServer(auth, sessionToken, false)
webhook := webhook.NewClient(&cfg)
app := New(&cfg, store, auth, wsserver, &mocks.FileBackend{}, webhook)

View File

@ -10,6 +10,7 @@ import (
"strings"
"github.com/mattermost/focalboard/server/utils"
"github.com/mattermost/mattermost-server/v5/shared/filestore"
)
func (a *App) SaveFile(reader io.Reader, workspaceID, rootID, filename string) (string, error) {
@ -30,26 +31,34 @@ func (a *App) SaveFile(reader io.Reader, workspaceID, rootID, filename string) (
return createdFilename, nil
}
func (a *App) GetFilePath(workspaceID, rootID, filename string) string {
folderPath := a.config.FilesPath
rootPath := filepath.Join(folderPath, workspaceID, rootID)
filePath := filepath.Join(rootPath, filename)
func (a *App) GetFileReader(workspaceID, rootID, filename string) (filestore.ReadCloseSeeker, error) {
filePath := filepath.Join(workspaceID, rootID, filename)
exists, err := a.filesBackend.FileExists(filePath)
if err != nil {
return nil, err
}
// FIXUP: Check the deprecated old location
if workspaceID == "0" && !fileExists(filePath) {
oldFilePath := filepath.Join(folderPath, filename)
if fileExists(oldFilePath) {
err := os.Rename(oldFilePath, filePath)
if workspaceID == "0" && !exists {
oldExists, err := a.filesBackend.FileExists(filename)
if err != nil {
return nil, err
}
if oldExists {
err := a.filesBackend.MoveFile(filename, filePath)
if err != nil {
log.Printf("ERROR moving old file from '%s' to '%s'", oldFilePath, filePath)
log.Printf("ERROR moving old file from '%s' to '%s'", filename, filePath)
} else {
log.Printf("Moved old file from '%s' to '%s'", oldFilePath, filePath)
log.Printf("Moved old file from '%s' to '%s'", filename, filePath)
}
}
}
return filePath
reader, err := a.filesBackend.Reader(filePath)
if err != nil {
return nil, err
}
return reader, nil
}
func fileExists(path string) bool {

View File

@ -33,7 +33,7 @@ func (a *App) GetRootWorkspace() (*model.Workspace, error) {
return workspace, nil
}
func (a *App) getWorkspace(ID string) (*model.Workspace, error) {
func (a *App) GetWorkspace(ID string) (*model.Workspace, error) {
workspace, err := a.store.GetWorkspace(ID)
if err == sql.ErrNoRows {
return nil, nil
@ -44,6 +44,10 @@ func (a *App) getWorkspace(ID string) (*model.Workspace, error) {
return workspace, nil
}
func (a *App) DoesUserHaveWorkspaceAccess(userID string, workspaceID string) bool {
return a.auth.DoesUserHaveWorkspaceAccess(userID, workspaceID)
}
func (a *App) UpsertWorkspaceSettings(workspace model.Workspace) error {
return a.store.UpsertWorkspaceSettings(workspace)
}

View File

@ -58,3 +58,11 @@ func (a *Auth) IsValidReadToken(c store.Container, blockID string, readToken str
return false, nil
}
func (a *Auth) DoesUserHaveWorkspaceAccess(userID string, workspaceID string) bool {
hasAccess, err := a.store.HasWorkspaceAccess(userID, workspaceID)
if err != nil {
return false
}
return hasAccess
}

View File

@ -4,6 +4,7 @@ go 1.15
require (
github.com/Masterminds/squirrel v1.5.0
github.com/go-sql-driver/mysql v1.5.0
github.com/golang-migrate/migrate/v4 v4.14.1
github.com/golang/mock v1.5.0
github.com/google/uuid v1.2.0
@ -12,7 +13,7 @@ require (
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/lib/pq v1.10.0
github.com/magiconair/properties v1.8.5 // indirect
github.com/mattermost/mattermost-server/v5 v5.33.2
github.com/mattermost/mattermost-server/v5 v5.34.2
github.com/mattn/go-sqlite3 v2.0.3+incompatible
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/oklog/run v1.1.0

View File

@ -101,7 +101,7 @@ github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/araddon/dateparse v0.0.0-20180729174819-cfd92a431d0e/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI=
github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI=
github.com/araddon/dateparse v0.0.0-20201001162425-8aadafed4dc4/go.mod h1:hMAUZFIkk4B1FouGxqlogyMyU6BwY/UiVmmbbzz9Up8=
github.com/araddon/dateparse v0.0.0-20210207001429-0eec95c9db7e/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
@ -113,7 +113,7 @@ github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQ
github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.19.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.36.29/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.37.18/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
@ -152,7 +152,6 @@ github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -246,7 +245,6 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqL
github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
@ -264,14 +262,14 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw=
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
github.com/getsentry/sentry-go v0.9.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws=
github.com/getsentry/sentry-go v0.10.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573/go.mod h1:eBvb3i++NHDH4Ugo9qCvMw8t0mTSctaEa5blJbWcNxs=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
github.com/glycerine/go-unsnap-stream v0.0.0-20210130063903-47dfef350d96/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
github.com/go-asn1-ber/asn1-ber v1.3.2-0.20191121212151-29be175fc3a3/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-asn1-ber/asn1-ber v1.5.3 h1:u7utq56RUFiynqUzgVMFDymapcOtQ/MZkh3H4QYkxag=
@ -289,10 +287,10 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
github.com/go-redis/redis/v8 v8.0.0/go.mod h1:isLoQT/NFSP7V67lyvM9GmdvLdyZ7pEhsXvvyQtnQTo=
github.com/go-redis/redis/v8 v8.4.9/go.mod h1:d5yY/TlkQyYBSBHnXUmnf1OrHbyQere5JV4dLKwvXmo=
github.com/go-redis/redis/v8 v8.6.0/go.mod h1:DQ9q4Rk2HtwkrwVrdgmphoOQDMfpvcd/nHEwRsicg8s=
github.com/go-resty/resty/v2 v2.0.0/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8=
github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU=
github.com/go-resty/resty/v2 v2.4.0/go.mod h1:B88+xCTEwvfD94NOuE6GS1wMlnoKNY8eEiNizfNwOwA=
github.com/go-resty/resty/v2 v2.5.0/go.mod h1:B88+xCTEwvfD94NOuE6GS1wMlnoKNY8eEiNizfNwOwA=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
@ -378,7 +376,6 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
@ -387,8 +384,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20210202160940-bed99a852dfe h1:rcf1P0fm+1l0EjG16p06mYLj9gW9X36KgdHJ/88hS4g=
github.com/gopherjs/gopherjs v0.0.0-20210202160940-bed99a852dfe/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
@ -508,6 +505,7 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
@ -546,6 +544,9 @@ github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgo
github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s=
github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.4 h1:g0I61F2K2DjRHz1cnxlkNSBIaePVoJIjjnHui8QHbiw=
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kljensen/snowball v0.6.0/go.mod h1:27N7E8fVU5H68RlUmnWwZCfxgt4POBJfENGMvNRhldw=
@ -585,6 +586,9 @@ github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-b
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/magefile/mage v1.11.0 h1:C/55Ywp9BpgVVclD3lRnSYCwXTYxmSppIgLeDYlNuls=
github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
@ -601,8 +605,8 @@ github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d h1:/RJ/UV7M5c7L2TQ
github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d/go.mod h1:HLbgMEI5K131jpxGazJ97AxfPDt31osq36YS1oxFQPQ=
github.com/mattermost/logr v1.0.13 h1:6F/fM3csvH6Oy5sUpJuW7YyZSzZZAhJm5VcgKMxA2P8=
github.com/mattermost/logr v1.0.13/go.mod h1:Mt4DPu1NXMe6JxPdwCC0XBoxXmN9eXOIRPoZarU2PXs=
github.com/mattermost/mattermost-server/v5 v5.33.2 h1:67pG6GAQ+jGU2HZkMu5uexLjeExt6rqIhovv6ksKgpk=
github.com/mattermost/mattermost-server/v5 v5.33.2/go.mod h1:VjeLX3HlgzR1FKYR6Ju0tR+9dAhhVMlM/s8a7rfbIMI=
github.com/mattermost/mattermost-server/v5 v5.34.2 h1:5EjIo/UjwksCxbL6nDS7XNRghmqjdf2vH5wUFgg7EAo=
github.com/mattermost/mattermost-server/v5 v5.34.2/go.mod h1:UGbMK9bMzmqGyQQ3N/3I6FDpTwJsL/wMYaO+4zctzBA=
github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
@ -617,17 +621,16 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
@ -639,15 +642,15 @@ github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00v
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.39/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw=
github.com/minio/md5-simd v1.1.1 h1:9ojcLbuZ4gXbB2sX53MKn8JUZ0sB/2wfwsEcRw+I08U=
github.com/minio/md5-simd v1.1.1/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw=
github.com/minio/minio-go/v7 v7.0.7 h1:Qld/xb8C1Pwbu0jU46xAceyn9xXKCMW+3XfNbpmTB70=
github.com/minio/minio-go/v7 v7.0.7/go.mod h1:pEZBUa+L2m9oECoIA6IcSK8bv/qggtQVLovjeKK5jYc=
github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.10 h1:1oUKe4EOPUEhw2qnPQaPsJ0lmVTYLFu03SiItauXs94=
github.com/minio/minio-go/v7 v7.0.10/go.mod h1:td4gW1ldOsj1PbSNS+WYK43j+P1XVhX/8W8awaYlBFo=
github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
github.com/minio/sio v0.2.1/go.mod h1:8b0yPp2avGThviy/+OCJBI6OMpvxoUuiLvE6F1lebhw=
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@ -703,6 +706,7 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/olivere/elastic v6.2.35+incompatible/go.mod h1:J+q1zQJTgAz9woqsbVRqGeB5G1iqDKVBWLNSYW8yfJ8=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@ -711,13 +715,13 @@ github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ=
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
github.com/oov/psd v0.0.0-20201203182240-dad9002861d9/go.mod h1:GHI1bnmAcbp96z6LNfBJvtrjxhaXGkbsk967utPlvL8=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@ -796,8 +800,9 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.15.0 h1:4fgOnadei3EZvgRwxJ7RMpG1k1pOZth5Pc13tyspaKM=
github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
github.com/prometheus/common v0.17.0 h1:kDIZLI74SS+3tedSvEkykgBkD7txMxaJAPj8DtJUKYA=
github.com/prometheus/common v0.17.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
@ -806,8 +811,8 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.3.0 h1:Uehi/mxLK0eiUc0H0++5tpMGTexB8wZ598MIgU8VpDM=
github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
@ -871,12 +876,12 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.8.0 h1:nfhvjKcUMhBMVqbKHJlk5RPrrfYr/NMo3692g0dwfWU=
github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8=
github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/snowflakedb/glog v0.0.0-20180824191149-f5055e6f21ce/go.mod h1:EB/w24pR5VKI60ecFnKqXzxX3dOorz1rnVicQTQrGM0=
@ -894,7 +899,7 @@ github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
@ -906,9 +911,9 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/splitio/go-client/v6 v6.0.1/go.mod h1:WHjkBtEEJFHOpgqnHSb/X+e4CgzTi1gPNkg+c7w1Izg=
github.com/splitio/go-split-commons/v2 v2.0.1/go.mod h1:YsmZa0AE9DCTosPXekKBxYHzNXJHpZwID9FSfo5Tnis=
github.com/splitio/go-toolkit/v3 v3.0.1/go.mod h1:HGgawLnM2RlM84zVRbATpPMjF7H6u9CUYG6RlpwOlOk=
github.com/splitio/go-client/v6 v6.0.2/go.mod h1:JbxrPJiIJHdPi5alQx5bszNzNrdJR+er1ktd9RzGtgk=
github.com/splitio/go-split-commons/v3 v3.0.0/go.mod h1:6YEmYZnfrqV1Fp0vo4JL26zhzsDPD7p/OwFWy1LW2n4=
github.com/splitio/go-toolkit/v4 v4.0.0/go.mod h1:EdIHN0yzB1GTXDYQc0KdKvnjkO/jfUM2YqHVYfhD3Wo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/steveyen/gtreap v0.1.0/go.mod h1:kl/5J7XbrOmlIbYIXdRHDDE5QxHqpk0cmkT7Z4dM9/Y=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
@ -957,7 +962,7 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
@ -967,8 +972,8 @@ github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPU
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/vmihailenco/msgpack/v5 v5.1.4/go.mod h1:C5gboKD0TJPqWDTVTtrQNfRbiBwHZGo8UTqP/9/XvLI=
github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/vmihailenco/msgpack/v5 v5.2.0/go.mod h1:fEM7KuHcnm0GvDCztRpw9hV0PuoO2ciTismP6vjggcM=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/wiggin77/cfg v1.0.2 h1:NBUX+iJRr+RTncTqTNvajHwzduqbhCQjEqxLHr6Fk7A=
github.com/wiggin77/cfg v1.0.2/go.mod h1:b3gotba2e5bXTqTW48DwIFoLc+4lWKP7WPi/CdvZ4aE=
github.com/wiggin77/merror v1.0.2/go.mod h1:uQTcIU0Z6jRK4OwqganPYerzQxSFJ4GSHM3aurxxQpg=
@ -1016,7 +1021,10 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0=
go.opentelemetry.io/otel v0.16.0/go.mod h1:e4GKElweB8W2gWUqbghw0B8t5MCTccc9212eNHnOHwA=
go.opentelemetry.io/otel v0.17.0/go.mod h1:Oqtdxmf7UtEvL037ohlgnaYa1h7GtMh0NcSd9eqkC9s=
go.opentelemetry.io/otel/metric v0.17.0/go.mod h1:hUz9lH1rNXyEwWAhIWCMFWKhYtpASgSnObJFnU26dJ0=
go.opentelemetry.io/otel/oteltest v0.17.0/go.mod h1:JT/LGFxPwpN+nlsTiinSYjdIx3hZIGqHCpChcIZmdoE=
go.opentelemetry.io/otel/trace v0.17.0/go.mod h1:bIujpqg6ZL6xUTubIUgziI1jSaUPthmabA/ygf/6Cfg=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
@ -1046,7 +1054,6 @@ golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -1056,7 +1063,7 @@ golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -1075,8 +1082,8 @@ golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMx
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI=
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk=
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -1154,7 +1161,7 @@ golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -1177,6 +1184,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -1240,7 +1249,10 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201029080932-201ba4db2418/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225014209-683adc9d29d7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210324051608-47abb6519492 h1:Paq34FxTluEPvVyayQqMPgHm+vTOrIifmcYxFBx9TLg=
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
@ -1320,6 +1332,7 @@ golang.org/x/tools v0.0.0-20200817023811-d00afeaade8f/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/tools v0.0.0-20200818005847-188abfa75333/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -1400,8 +1413,8 @@ google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20200815001618-f69a88009b70/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200911024640-645f7a48b24f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201030142918-24207fddd1c3/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210119180700-e258113e47cc h1:tsmkSntchraIEmbGgYaZGZ8LVIidKCWEmPwEnSQSalA=
google.golang.org/genproto v0.0.0-20210119180700-e258113e47cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210224155714-063164c882e6 h1:bXUwz2WkXXrXgiLxww3vWmoSHLOGv4ipdPdTvKymcKw=
google.golang.org/genproto v0.0.0-20210224155714-063164c882e6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
@ -1425,8 +1438,8 @@ google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0 h1:o1bcQ6imQMIOpdrO3SWf2z5RV72WbDwdXuK0MDlc8As=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=

View File

@ -34,6 +34,7 @@ func getTestConfig() *config.Configuration {
DBConfigString: connectionString,
DBTablePrefix: "test_",
WebPath: "./pack",
FilesDriver: "local",
FilesPath: "./files",
}
}

View File

@ -1,27 +1,6 @@
package server
import (
"log"
"github.com/mattermost/focalboard/server/einterfaces"
)
func (s *Server) initHandlers() {
cfg := s.config
if cfg.AuthMode == "mattermost" && mattermostAuth != nil {
log.Println("Using Mattermost Auth")
params := einterfaces.MattermostAuthParameters{
ServerRoot: cfg.ServerRoot,
MattermostURL: cfg.MattermostURL,
ClientID: cfg.MattermostClientID,
ClientSecret: cfg.MattermostClientSecret,
UseSecureCookie: cfg.SecureCookie,
}
mmauthHandler := mattermostAuth(params, s.store)
log.Println("CREATING AUTH")
s.webServer.AddRoutes(mmauthHandler)
log.Println("ADDING ROUTES")
s.api.WorkspaceAuthenticator = mmauthHandler
log.Println("SETTING THE AUTHENTICATOR")
}
s.api.MattermostAuth = cfg.AuthMode == "mattermost"
}

View File

@ -24,12 +24,13 @@ import (
"github.com/mattermost/focalboard/server/services/prometheus"
"github.com/mattermost/focalboard/server/services/scheduler"
"github.com/mattermost/focalboard/server/services/store"
"github.com/mattermost/focalboard/server/services/store/mattermostauthlayer"
"github.com/mattermost/focalboard/server/services/store/sqlstore"
"github.com/mattermost/focalboard/server/services/telemetry"
"github.com/mattermost/focalboard/server/services/webhook"
"github.com/mattermost/focalboard/server/web"
"github.com/mattermost/focalboard/server/ws"
"github.com/mattermost/mattermost-server/v5/services/filesstore"
"github.com/mattermost/mattermost-server/v5/shared/filestore"
"github.com/mattermost/mattermost-server/v5/utils"
"github.com/oklog/run"
)
@ -46,7 +47,7 @@ type Server struct {
wsServer *ws.Server
webServer *web.Server
store store.Store
filesBackend filesstore.FileBackend
filesBackend filestore.FileBackend
telemetry *telemetry.Service
logger *zap.Logger
cleanUpSessionsTask *scheduler.ScheduledTask
@ -65,21 +66,40 @@ func New(cfg *config.Configuration, singleUserToken string) (*Server, error) {
return nil, err
}
db, err := sqlstore.New(cfg.DBType, cfg.DBConfigString, cfg.DBTablePrefix)
var db store.Store
db, err = sqlstore.New(cfg.DBType, cfg.DBConfigString, cfg.DBTablePrefix)
if err != nil {
log.Print("Unable to start the database", err)
return nil, err
}
if cfg.AuthMode == "mattermost" {
layeredStore, err := mattermostauthlayer.New(cfg.DBType, cfg.DBConfigString, db)
if err != nil {
log.Print("Unable to start the database", err)
return nil, err
}
db = layeredStore
}
authenticator := auth.New(cfg, db)
wsServer := ws.NewServer(authenticator, singleUserToken)
wsServer := ws.NewServer(authenticator, singleUserToken, cfg.AuthMode == "mattermost")
filesBackendSettings := filesstore.FileBackendSettings{}
filesBackendSettings.DriverName = "local"
filesBackendSettings := filestore.FileBackendSettings{}
filesBackendSettings.DriverName = cfg.FilesDriver
filesBackendSettings.Directory = cfg.FilesPath
filesBackend, appErr := filesstore.NewFileBackend(filesBackendSettings)
filesBackendSettings.AmazonS3AccessKeyId = cfg.FilesS3Config.AccessKeyId
filesBackendSettings.AmazonS3SecretAccessKey = cfg.FilesS3Config.SecretAccessKey
filesBackendSettings.AmazonS3Bucket = cfg.FilesS3Config.Bucket
filesBackendSettings.AmazonS3PathPrefix = cfg.FilesS3Config.PathPrefix
filesBackendSettings.AmazonS3Region = cfg.FilesS3Config.Region
filesBackendSettings.AmazonS3Endpoint = cfg.FilesS3Config.Endpoint
filesBackendSettings.AmazonS3SSL = cfg.FilesS3Config.SSL
filesBackendSettings.AmazonS3SignV2 = cfg.FilesS3Config.SignV2
filesBackendSettings.AmazonS3SSE = cfg.FilesS3Config.SSE
filesBackendSettings.AmazonS3Trace = cfg.FilesS3Config.Trace
filesBackend, appErr := filestore.NewFileBackend(filesBackendSettings)
if appErr != nil {
log.Print("Unable to initialize the files storage")
@ -302,3 +322,7 @@ func (s *Server) stopLocalModeServer() {
func (s *Server) GetRootRouter() *mux.Router {
return s.webServer.Router()
}
func (s *Server) SetWSHub(hub ws.Hub) {
s.wsServer.SetHub(hub)
}

View File

@ -35,7 +35,7 @@ func TestParseAuthTokenFromRequest(t *testing.T) {
}
if tc.cookie != "" {
req.AddCookie(&http.Cookie{
Name: SESSION_COOKIE_TOKEN,
Name: "FOCALBOARDAUTHTOKEN",
Value: tc.cookie,
})
}

View File

@ -11,31 +11,43 @@ const (
DefaultPort = 8000
)
type AmazonS3Config struct {
AccessKeyId string
SecretAccessKey string
Bucket string
PathPrefix string
Region string
Endpoint string
SSL bool
SignV2 bool
SSE bool
Trace bool
}
// Configuration is the app configuration stored in a json file.
type Configuration struct {
ServerRoot string `json:"serverRoot" mapstructure:"serverRoot"`
Port int `json:"port" mapstructure:"port"`
DBType string `json:"dbtype" mapstructure:"dbtype"`
DBConfigString string `json:"dbconfig" mapstructure:"dbconfig"`
DBTablePrefix string `json:"dbtableprefix" mapstructure:"dbtableprefix"`
UseSSL bool `json:"useSSL" mapstructure:"useSSL"`
SecureCookie bool `json:"secureCookie" mapstructure:"secureCookie"`
WebPath string `json:"webpath" mapstructure:"webpath"`
FilesPath string `json:"filespath" mapstructure:"filespath"`
Telemetry bool `json:"telemetry" mapstructure:"telemetry"`
PrometheusAddress string `json:"prometheus_address" mapstructure:"prometheus_address"`
WebhookUpdate []string `json:"webhook_update" mapstructure:"webhook_update"`
Secret string `json:"secret" mapstructure:"secret"`
SessionExpireTime int64 `json:"session_expire_time" mapstructure:"session_expire_time"`
SessionRefreshTime int64 `json:"session_refresh_time" mapstructure:"session_refresh_time"`
LocalOnly bool `json:"localonly" mapstructure:"localonly"`
EnableLocalMode bool `json:"enableLocalMode" mapstructure:"enableLocalMode"`
LocalModeSocketLocation string `json:"localModeSocketLocation" mapstructure:"localModeSocketLocation"`
ServerRoot string `json:"serverRoot" mapstructure:"serverRoot"`
Port int `json:"port" mapstructure:"port"`
DBType string `json:"dbtype" mapstructure:"dbtype"`
DBConfigString string `json:"dbconfig" mapstructure:"dbconfig"`
DBTablePrefix string `json:"dbtableprefix" mapstructure:"dbtableprefix"`
UseSSL bool `json:"useSSL" mapstructure:"useSSL"`
SecureCookie bool `json:"secureCookie" mapstructure:"secureCookie"`
WebPath string `json:"webpath" mapstructure:"webpath"`
FilesDriver string `json:"filesdriver" mapstructure:"filesdriver"`
FilesS3Config AmazonS3Config `json:"filess3config" mapstructure:"filess3config"`
FilesPath string `json:"filespath" mapstructure:"filespath"`
Telemetry bool `json:"telemetry" mapstructure:"telemetry"`
PrometheusAddress string `json:"prometheus_address" mapstructure:"prometheus_address"`
WebhookUpdate []string `json:"webhook_update" mapstructure:"webhook_update"`
Secret string `json:"secret" mapstructure:"secret"`
SessionExpireTime int64 `json:"session_expire_time" mapstructure:"session_expire_time"`
SessionRefreshTime int64 `json:"session_refresh_time" mapstructure:"session_refresh_time"`
LocalOnly bool `json:"localonly" mapstructure:"localonly"`
EnableLocalMode bool `json:"enableLocalMode" mapstructure:"enableLocalMode"`
LocalModeSocketLocation string `json:"localModeSocketLocation" mapstructure:"localModeSocketLocation"`
AuthMode string `json:"authMode" mapstructure:"authMode"`
MattermostURL string `json:"mattermostURL" mapstructure:"mattermostURL"`
MattermostClientID string `json:"mattermostClientID" mapstructure:"mattermostClientID"`
MattermostClientSecret string `json:"mattermostClientSecret" mapstructure:"mattermostClientSecret"`
AuthMode string `json:"authMode" mapstructure:"authMode"`
}
// ReadConfigFile read the configuration from the filesystem.
@ -53,6 +65,7 @@ func ReadConfigFile() (*Configuration, error) {
viper.SetDefault("SecureCookie", false)
viper.SetDefault("WebPath", "./pack")
viper.SetDefault("FilesPath", "./files")
viper.SetDefault("FilesDriver", "local")
viper.SetDefault("Telemetry", true)
viper.SetDefault("WebhookUpdate", nil)
viper.SetDefault("SessionExpireTime", 60*60*24*30) // 30 days session lifetime
@ -83,9 +96,5 @@ func ReadConfigFile() (*Configuration, error) {
func removeSecurityData(config Configuration) Configuration {
clean := config
clean.Secret = "hidden"
clean.MattermostClientID = "hidden"
clean.MattermostClientSecret = "hidden"
return clean
}

View File

@ -0,0 +1,254 @@
package mattermostauthlayer
import (
"database/sql"
"encoding/json"
"errors"
"log"
"strings"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/store"
)
const (
mysqlDBType = "mysql"
sqliteDBType = "sqlite3"
postgresDBType = "postgres"
)
// Store represents the abstraction of the data storage.
type MattermostAuthLayer struct {
store.Store
dbType string
mmDB *sql.DB
}
// New creates a new SQL implementation of the store.
func New(dbType, connectionString string, store store.Store) (*MattermostAuthLayer, error) {
log.Println("connectDatabase", dbType, connectionString)
var err error
db, err := sql.Open(dbType, connectionString)
if err != nil {
log.Print("connectDatabase: ", err)
return nil, err
}
err = db.Ping()
if err != nil {
log.Printf(`Database Ping failed: %v`, err)
return nil, err
}
layer := &MattermostAuthLayer{
Store: store,
dbType: dbType,
mmDB: db,
}
return layer, nil
}
// Shutdown close the connection with the store.
func (l *MattermostAuthLayer) Shutdown() error {
err := l.Store.Shutdown()
if err != nil {
return err
}
return l.mmDB.Close()
}
func (s *MattermostAuthLayer) GetRegisteredUserCount() (int, error) {
query := s.getQueryBuilder().
Select("count(*)").
From("Users").
Where(sq.Eq{"deleteAt": 0})
row := query.QueryRow()
var count int
err := row.Scan(&count)
if err != nil {
return 0, err
}
return count, nil
}
func (s *MattermostAuthLayer) getUserByCondition(condition sq.Eq) (*model.User, error) {
query := s.getQueryBuilder().
Select("id", "username", "email", "password", "MFASecret as mfa_secret", "AuthService as auth_service", "COALESCE(AuthData, '') as auth_data", "props", "CreateAt as create_at", "UpdateAt as update_at", "DeleteAt as delete_at").
From("Users").
Where(sq.Eq{"deleteAt": 0}).
Where(condition)
row := query.QueryRow()
user := model.User{}
var propsBytes []byte
err := row.Scan(&user.ID, &user.Username, &user.Email, &user.Password, &user.MfaSecret, &user.AuthService, &user.AuthData, &propsBytes, &user.CreateAt, &user.UpdateAt, &user.DeleteAt)
if err != nil {
return nil, err
}
err = json.Unmarshal(propsBytes, &user.Props)
if err != nil {
return nil, err
}
return &user, nil
}
func (s *MattermostAuthLayer) GetUserById(userID string) (*model.User, error) {
return s.getUserByCondition(sq.Eq{"id": userID})
}
func (s *MattermostAuthLayer) GetUserByEmail(email string) (*model.User, error) {
return s.getUserByCondition(sq.Eq{"email": email})
}
func (s *MattermostAuthLayer) GetUserByUsername(username string) (*model.User, error) {
return s.getUserByCondition(sq.Eq{"username": username})
}
func (s *MattermostAuthLayer) CreateUser(user *model.User) error {
return errors.New("no user creation allowed from focalboard, create it using mattermost")
}
func (s *MattermostAuthLayer) UpdateUser(user *model.User) error {
return errors.New("no update allowed from focalboard, update it using mattermost")
}
func (s *MattermostAuthLayer) UpdateUserPassword(username, password string) error {
return errors.New("no update allowed from focalboard, update it using mattermost")
}
func (s *MattermostAuthLayer) UpdateUserPasswordByID(userID, password string) error {
return errors.New("no update allowed from focalboard, update it using mattermost")
}
// GetActiveUserCount returns the number of users with active sessions within N seconds ago
func (s *MattermostAuthLayer) GetActiveUserCount(updatedSecondsAgo int64) (int, error) {
query := s.getQueryBuilder().
Select("count(distinct userId)").
From("Sessions").
Where(sq.Gt{"LastActivityAt": time.Now().Unix() - updatedSecondsAgo})
row := query.QueryRow()
var count int
err := row.Scan(&count)
if err != nil {
return 0, err
}
return count, nil
}
func (s *MattermostAuthLayer) GetSession(token string, expireTime int64) (*model.Session, error) {
return nil, errors.New("sessions not used when using mattermost")
}
func (s *MattermostAuthLayer) CreateSession(session *model.Session) error {
return errors.New("no update allowed from focalboard, update it using mattermost")
}
func (s *MattermostAuthLayer) RefreshSession(session *model.Session) error {
return errors.New("no update allowed from focalboard, update it using mattermost")
}
func (s *MattermostAuthLayer) UpdateSession(session *model.Session) error {
return errors.New("no update allowed from focalboard, update it using mattermost")
}
func (s *MattermostAuthLayer) DeleteSession(sessionId string) error {
return errors.New("no update allowed from focalboard, update it using mattermost")
}
func (s *MattermostAuthLayer) CleanUpSessions(expireTime int64) error {
return errors.New("no update allowed from focalboard, update it using mattermost")
}
func (s *MattermostAuthLayer) GetWorkspace(ID string) (*model.Workspace, error) {
if ID == "0" {
workspace := model.Workspace{
ID: ID,
Title: "",
}
return &workspace, nil
}
query := s.getQueryBuilder().
Select("DisplayName, Type").
From("Channels").
Where(sq.Eq{"ID": ID})
row := query.QueryRow()
var displayName string
var channelType string
err := row.Scan(&displayName, &channelType)
if err != nil {
return nil, err
}
if channelType != "D" && channelType != "G" {
return &model.Workspace{ID: ID, Title: displayName}, nil
}
query = s.getQueryBuilder().
Select("Username").
From("ChannelMembers").
Join("Users ON Users.ID=ChannelMembers.UserID").
Where(sq.Eq{"ChannelID": ID})
var sb strings.Builder
rows, err := query.Query()
if err != nil {
return nil, err
}
first := true
for rows.Next() {
if first {
sb.WriteString(", ")
first = false
}
var name string
if err := rows.Scan(&name); err != nil {
log.Fatal(err)
}
sb.WriteString(name)
}
return &model.Workspace{ID: ID, Title: sb.String()}, nil
}
func (s *MattermostAuthLayer) HasWorkspaceAccess(userID string, workspaceID string) (bool, error) {
query := s.getQueryBuilder().
Select("count(*)").
From("ChannelMembers").
Where(sq.Eq{"ChannelID": workspaceID}).
Where(sq.Eq{"UserID": userID})
row := query.QueryRow()
var count int
err := row.Scan(&count)
if err != nil {
return false, err
}
return count > 0, nil
}
func (s *MattermostAuthLayer) getQueryBuilder() sq.StatementBuilderType {
builder := sq.StatementBuilder
if s.dbType == postgresDBType || s.dbType == sqliteDBType {
builder = builder.PlaceholderFormat(sq.Dollar)
}
return builder.RunWith(s.mmDB)
}

View File

@ -375,6 +375,21 @@ func (mr *MockStoreMockRecorder) GetWorkspace(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspace", reflect.TypeOf((*MockStore)(nil).GetWorkspace), arg0)
}
// HasWorkspaceAccess mocks base method.
func (m *MockStore) HasWorkspaceAccess(arg0, arg1 string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HasWorkspaceAccess", arg0, arg1)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HasWorkspaceAccess indicates an expected call of HasWorkspaceAccess.
func (mr *MockStoreMockRecorder) HasWorkspaceAccess(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasWorkspaceAccess", reflect.TypeOf((*MockStore)(nil).HasWorkspaceAccess), arg0, arg1)
}
// InsertBlock mocks base method.
func (m *MockStore) InsertBlock(arg0 store.Container, arg1 model.Block) error {
m.ctrl.T.Helper()

View File

@ -2,6 +2,7 @@ package sqlstore
import (
"bytes"
"database/sql"
"errors"
"fmt"
"io"
@ -9,6 +10,7 @@ import (
"os"
"text/template"
mysqldriver "github.com/go-sql-driver/mysql"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database"
"github.com/golang-migrate/migrate/v4/database/mysql"
@ -66,6 +68,29 @@ func (pm *PrefixedMigration) ReadDown(version uint) (io.ReadCloser, string, erro
return pm.executeTemplate(r, identifier)
}
// migrations in MySQL need to run with the multiStatements flag
// enabled, so this method creates a new connection ensuring that it's
// enabled
func (s *SQLStore) getMySQLMigrationConnection() (*sql.DB, error) {
config, err := mysqldriver.ParseDSN(s.connectionString)
if err != nil {
return nil, err
}
config.Params["multiStatements"] = "true"
connectionString := config.FormatDSN()
db, err := sql.Open(s.dbType, connectionString)
if err != nil {
return nil, err
}
if err = db.Ping(); err != nil {
return nil, err
}
return db, nil
}
func (s *SQLStore) Migrate() error {
var driver database.Driver
var err error
@ -86,7 +111,13 @@ func (s *SQLStore) Migrate() error {
}
if s.dbType == mysqlDBType {
driver, err = mysql.WithInstance(s.db, &mysql.Config{MigrationsTable: migrationsTable})
db, err := s.getMySQLMigrationConnection()
if err != nil {
return err
}
defer db.Close()
driver, err = mysql.WithInstance(db, &mysql.Config{MigrationsTable: migrationsTable})
if err != nil {
return err
}

View File

@ -18,6 +18,7 @@ type SQLStore struct {
db *sql.DB
dbType string
tablePrefix string
connectionString string
}
// New creates a new SQL implementation of the store.
@ -40,9 +41,10 @@ func New(dbType, connectionString string, tablePrefix string) (*SQLStore, error)
}
store := &SQLStore{
db: db,
dbType: dbType,
tablePrefix: tablePrefix,
db: db,
dbType: dbType,
tablePrefix: tablePrefix,
connectionString: connectionString,
}
err = store.Migrate()

View File

@ -104,3 +104,7 @@ func (s *SQLStore) GetWorkspace(ID string) (*model.Workspace, error) {
return &workspace, nil
}
func (s *SQLStore) HasWorkspaceAccess(userID string, workspaceID string) (bool, error) {
return true, nil
}

View File

@ -51,4 +51,5 @@ type Store interface {
UpsertWorkspaceSignupToken(workspace model.Workspace) error
UpsertWorkspaceSettings(workspace model.Workspace) error
GetWorkspace(ID string) (*model.Workspace, error)
HasWorkspaceAccess(userID string, workspaceID string) (bool, error)
}

View File

@ -94,6 +94,10 @@ func (ws *Server) registerRoutes() {
// Start runs the web server and start listening for charsetnnections.
func (ws *Server) Start() {
ws.registerRoutes()
if ws.port == -1 {
log.Print("server not bind to any port\n")
return
}
isSSL := ws.ssl && fileExists("./cert/cert.pem") && fileExists("./cert/key.pem")
if isSSL {

View File

@ -15,21 +15,23 @@ import (
"github.com/mattermost/focalboard/server/services/store"
)
type WorkspaceAuthenticator interface {
DoesUserHaveWorkspaceAccess(session *model.Session, workspaceID string) bool
}
// IsValidSessionToken authenticates session tokens
type IsValidSessionToken func(token string) bool
type Hub interface {
SendWSMessage(data []byte)
SetReceiveWSMessage(func(data []byte))
}
// Server is a WebSocket server.
type Server struct {
upgrader websocket.Upgrader
listeners map[string][]*websocket.Conn
mu sync.RWMutex
auth *auth.Auth
singleUserToken string
WorkspaceAuthenticator WorkspaceAuthenticator
upgrader websocket.Upgrader
listeners map[string][]*websocket.Conn
mu sync.RWMutex
auth *auth.Auth
hub Hub
singleUserToken string
isMattermostAuth bool
}
// UpdateMsg is sent on block updates
@ -38,6 +40,13 @@ type UpdateMsg struct {
Block model.Block `json:"block"`
}
// clusterUpdateMsg is sent on block updates
type clusterUpdateMsg struct {
UpdateMsg
BlockID string `json:"block_id"`
WorkspaceID string `json:"workspace_id"`
}
// ErrorMsg is sent on errors
type ErrorMsg struct {
Error string `json:"error"`
@ -59,7 +68,7 @@ type websocketSession struct {
}
// NewServer creates a new Server.
func NewServer(auth *auth.Auth, singleUserToken string) *Server {
func NewServer(auth *auth.Auth, singleUserToken string, isMattermostAuth bool) *Server {
return &Server{
listeners: make(map[string][]*websocket.Conn),
upgrader: websocket.Upgrader{
@ -67,8 +76,9 @@ func NewServer(auth *auth.Auth, singleUserToken string) *Server {
return true
},
},
auth: auth,
singleUserToken: singleUserToken,
auth: auth,
singleUserToken: singleUserToken,
isMattermostAuth: isMattermostAuth,
}
}
@ -85,10 +95,6 @@ func (ws *Server) handleWebSocketOnChange(w http.ResponseWriter, r *http.Request
return
}
// TODO: Auth
log.Printf("CONNECT WebSocket onChange, client: %s", client.RemoteAddr())
// Make sure we close the connection when the function returns
defer func() {
log.Printf("DISCONNECT WebSocket onChange, client: %s", client.RemoteAddr())
@ -99,9 +105,14 @@ func (ws *Server) handleWebSocketOnChange(w http.ResponseWriter, r *http.Request
client.Close()
}()
userID := ""
if ws.isMattermostAuth {
userID = r.Header.Get("Mattermost-User-Id")
}
wsSession := websocketSession{
client: client,
isAuthenticated: false,
isAuthenticated: userID != "",
}
// Simple message handling loop
@ -124,19 +135,25 @@ func (ws *Server) handleWebSocketOnChange(w http.ResponseWriter, r *http.Request
continue
}
if userID != "" {
if ws.auth.DoesUserHaveWorkspaceAccess(userID, command.WorkspaceID) {
wsSession.workspaceID = command.WorkspaceID
} else {
log.Printf(`ERROR User doesn't have permissions to read the workspace: %s`, command.WorkspaceID)
continue
}
}
switch command.Action {
case "AUTH":
log.Printf(`Command: AUTH, client: %s`, client.RemoteAddr())
ws.authenticateListener(&wsSession, command.WorkspaceID, command.Token)
case "ADD":
log.Printf(`Command: Add workspaceID: %s, blockIDs: %v, client: %s`, wsSession.workspaceID, command.BlockIDs, client.RemoteAddr())
ws.addListener(&wsSession, &command)
case "REMOVE":
log.Printf(`Command: Remove workspaceID: %s, blockID: %v, client: %s`, wsSession.workspaceID, command.BlockIDs, client.RemoteAddr())
ws.removeListenerFromBlocks(&wsSession, &command)
default:
log.Printf(`ERROR webSocket command, invalid action: %v`, command.Action)
}
@ -154,13 +171,7 @@ func (ws *Server) isValidSessionToken(token, workspaceID string) bool {
}
// Check workspace permission
if ws.WorkspaceAuthenticator != nil {
if !ws.WorkspaceAuthenticator.DoesUserHaveWorkspaceAccess(session, workspaceID) {
return false
}
}
return true
return ws.auth.DoesUserHaveWorkspaceAccess(session.UserID, workspaceID)
}
func (ws *Server) authenticateListener(wsSession *websocketSession, workspaceID, token string) {
@ -301,6 +312,38 @@ func sendError(conn *websocket.Conn, message string) {
}
}
func (ws *Server) SetHub(hub Hub) {
ws.hub = hub
ws.hub.SetReceiveWSMessage(func(data []byte) {
var msg clusterUpdateMsg
err := json.Unmarshal(data, &msg)
if err != nil {
log.Printf("unable to unmarshal cluster message")
return
}
listeners := ws.getListeners(msg.WorkspaceID, msg.BlockID)
log.Printf("%d listener(s) for blockID: %s", len(listeners), msg.BlockID)
message := UpdateMsg{
Action: msg.Action,
Block: msg.Block,
}
if listeners != nil {
for _, listener := range listeners {
log.Printf("Broadcast change, workspaceID: %s, blockID: %s, remoteAddr: %s", msg.WorkspaceID, msg.BlockID, listener.RemoteAddr())
err := listener.WriteJSON(message)
if err != nil {
log.Printf("broadcast error: %v", err)
listener.Close()
}
}
}
})
}
// getListeners returns the listeners to a blockID's changes.
func (ws *Server) getListeners(workspaceID string, blockID string) []*websocket.Conn {
ws.mu.Lock()
@ -331,12 +374,19 @@ func (ws *Server) BroadcastBlockChange(workspaceID string, block model.Block) {
listeners := ws.getListeners(workspaceID, blockID)
log.Printf("%d listener(s) for blockID: %s", len(listeners), blockID)
if listeners != nil {
message := UpdateMsg{
Action: "UPDATE_BLOCK",
Block: block,
message := UpdateMsg{
Action: "UPDATE_BLOCK",
Block: block,
}
if ws.hub != nil {
data, err := json.Marshal(clusterUpdateMsg{UpdateMsg: message, WorkspaceID: workspaceID, BlockID: blockID})
if err != nil {
log.Printf("unable to serialize websocket message %v with the error: %v", message, err)
}
ws.hub.SendWSMessage(data)
}
if listeners != nil {
for _, listener := range listeners {
log.Printf("Broadcast change, workspaceID: %s, blockID: %s, remoteAddr: %s", workspaceID, blockID, listener.RemoteAddr())

View File

@ -66,7 +66,7 @@ class OctoListener {
const url = new URL(this.serverUrl)
const protocol = (url.protocol === 'https:') ? 'wss:' : 'ws:'
const wsServerUrl = `${protocol}//${url.host}${url.pathname}ws/onchange`
const wsServerUrl = `${protocol}//${url.host}${url.pathname.replace(/\/$/, '')}/ws/onchange`
Utils.log(`OctoListener open: ${wsServerUrl}`)
const ws = new WebSocket(wsServerUrl)
this.ws = ws