You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by kl...@apache.org on 2022/07/15 03:29:31 UTC

[incubator-devlake] branch main updated: feat: org plugin (#2461)

This is an automated email from the ASF dual-hosted git repository.

klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new 0322cfc0 feat: org plugin (#2461)
0322cfc0 is described below

commit 0322cfc05eea6fce6d5efe82783dd7535f1f7aa1
Author: mindlesscloud <li...@merico.dev>
AuthorDate: Fri Jul 15 11:29:27 2022 +0800

    feat: org plugin (#2461)
    
    * feat: org plugin
    
    * fix: fix user account association
    
    * fix: fix a typo
    
    * fix: fix e2e for org plugin
    
    * feat: add account_users.csv endpoint
    
    * refactor: rename accountUser to userAccount
    
    * fix: make slice with zero lenght
    
    * feat: createAccount
    
    * fix: toDomainLayer return slice of pointers
    
    * refactor: remove user_account.csv endpoint & user  as delimiter for array
    
    * feat: add swagger support for plugin org
    
    * refactor: rename accounts.csv to user_account_mapping.csv
    
    * refactor: change http handler from gin.HandlerFunc to core.ApiResourceHandler
    
    * fix: response to PR commtents
    
    * fix: subtask ConnectUserAccountsExact only insert not modify
    
    * fix: enable sub task by default && revise swagger doc
---
 api/router.go                                      |  18 +-
 go.mod                                             |  22 +-
 go.sum                                             |  25 +++
 models/domainlayer/crossdomain/team_user.go        |   3 +
 models/domainlayer/crossdomain/user_account.go     |   5 +-
 models/migrationscripts/archived/team_user.go      |   1 +
 models/migrationscripts/archived/user_account.go   |   3 +-
 plugins/core/plugin_api.go                         |  19 +-
 plugins/org/api/handlers.go                        |  60 ++++++
 plugins/org/api/store.go                           | 113 +++++++++++
 plugins/org/api/team.go                            |  96 +++++++++
 plugins/org/api/types.go                           | 225 +++++++++++++++++++++
 plugins/org/api/user.go                            | 103 ++++++++++
 plugins/org/api/user_account_mapping.go            |  88 ++++++++
 plugins/org/e2e/raw_tables/accounts.csv            |  11 +
 plugins/org/e2e/raw_tables/user_accounts.csv       |   5 +
 plugins/org/e2e/raw_tables/users.csv               |  11 +
 plugins/org/e2e/snapshot_tables/user_accounts.csv  |  11 +
 plugins/org/e2e/user_account_test.go               |  56 +++++
 plugins/org/impl/impl.go                           |  87 ++++++++
 .../archived/team_user.go => plugins/org/org.go    |  11 +-
 .../team_user.go => plugins/org/tasks/task_data.go |  14 +-
 plugins/org/tasks/user_account.go                  | 116 +++++++++++
 23 files changed, 1069 insertions(+), 34 deletions(-)

diff --git a/api/router.go b/api/router.go
index 3666f251..d99fba7c 100644
--- a/api/router.go
+++ b/api/router.go
@@ -20,10 +20,10 @@ package api
 import (
 	"fmt"
 	"net/http"
+	"strings"
 
 	"github.com/apache/incubator-devlake/api/blueprints"
 	"github.com/apache/incubator-devlake/api/domainlayer"
-
 	"github.com/apache/incubator-devlake/api/ping"
 	"github.com/apache/incubator-devlake/api/pipelines"
 	"github.com/apache/incubator-devlake/api/push"
@@ -77,10 +77,14 @@ func RegisterRouter(r *gin.Engine) {
 						}
 						input.Query = c.Request.URL.Query()
 						if c.Request.Body != nil {
-							err := c.ShouldBindJSON(&input.Body)
-							if err != nil && err.Error() != "EOF" {
-								shared.ApiOutputError(c, err, http.StatusBadRequest)
-								return
+							if strings.HasPrefix(c.Request.Header.Get("Content-Type"), "multipart/form-data;") {
+								input.Request = c.Request
+							} else {
+								err = c.ShouldBindJSON(&input.Body)
+								if err != nil && err.Error() != "EOF" {
+									shared.ApiOutputError(c, err, http.StatusBadRequest)
+									return
+								}
 							}
 						}
 						output, err := handler(input)
@@ -91,6 +95,10 @@ func RegisterRouter(r *gin.Engine) {
 							if status < http.StatusContinue {
 								status = http.StatusOK
 							}
+							if output.File != nil {
+								c.Data(status, output.File.ContentType, output.File.Data)
+								return
+							}
 							shared.ApiOutputSuccess(c, output.Body, status)
 						} else {
 							shared.ApiOutputSuccess(c, nil, http.StatusOK)
diff --git a/go.mod b/go.mod
index 710d9139..e82cbfde 100644
--- a/go.mod
+++ b/go.mod
@@ -20,7 +20,7 @@ require (
 	github.com/stoewer/go-strcase v1.2.0
 	github.com/stretchr/testify v1.7.0
 	github.com/swaggo/gin-swagger v1.4.3
-	github.com/swaggo/swag v1.8.1
+	github.com/swaggo/swag v1.8.3
 	github.com/x-cray/logrus-prefixed-formatter v0.5.2
 	go.temporal.io/api v1.7.1-0.20220223032354-6e6fe738916a
 	go.temporal.io/sdk v1.14.0
@@ -46,16 +46,18 @@ require (
 	github.com/emirpasic/gods v1.12.0 // indirect
 	github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect
 	github.com/fsnotify/fsnotify v1.5.1 // indirect
+	github.com/ghodss/yaml v1.0.0 // indirect
 	github.com/gin-contrib/sse v0.1.0 // indirect
 	github.com/go-git/gcfg v1.5.0 // indirect
 	github.com/go-git/go-billy/v5 v5.3.1 // indirect
 	github.com/go-openapi/jsonpointer v0.19.5 // indirect
-	github.com/go-openapi/jsonreference v0.19.6 // indirect
-	github.com/go-openapi/spec v0.20.4 // indirect
-	github.com/go-openapi/swag v0.19.15 // indirect
+	github.com/go-openapi/jsonreference v0.20.0 // indirect
+	github.com/go-openapi/spec v0.20.6 // indirect
+	github.com/go-openapi/swag v0.21.1 // indirect
 	github.com/go-playground/locales v0.14.0 // indirect
 	github.com/go-playground/universal-translator v0.18.0 // indirect
 	github.com/go-sql-driver/mysql v1.6.0 // indirect
+	github.com/gocarina/gocsv v0.0.0-20220707092902-b9da1f06c77e // indirect
 	github.com/gogo/googleapis v1.4.1 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/gogo/status v1.1.0 // indirect
@@ -82,7 +84,7 @@ require (
 	github.com/json-iterator/go v1.1.11 // indirect
 	github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
 	github.com/leodido/go-urn v1.2.1 // indirect
-	github.com/mailru/easyjson v0.7.6 // indirect
+	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mattn/go-colorable v0.1.6 // indirect
 	github.com/mattn/go-isatty v0.0.13 // indirect
 	github.com/mattn/go-sqlite3 v1.14.6 // indirect
@@ -104,19 +106,21 @@ require (
 	github.com/stretchr/objx v0.3.0 // indirect
 	github.com/subosito/gotenv v1.2.0 // indirect
 	github.com/ugorji/go/codec v1.2.6 // indirect
+	github.com/urfave/cli/v2 v2.11.0 // indirect
 	github.com/xanzy/ssh-agent v0.3.0 // indirect
+	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
 	go.uber.org/atomic v1.9.0 // indirect
-	golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect
-	golang.org/x/sys v0.0.0-20220222200937-f2425489ef4c // indirect
+	golang.org/x/net v0.0.0-20220708220712-1185a9018129 // indirect
+	golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e // indirect
 	golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
 	golang.org/x/text v0.3.7 // indirect
 	golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
-	golang.org/x/tools v0.1.7 // indirect
+	golang.org/x/tools v0.1.11 // indirect
 	google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf // indirect
 	google.golang.org/grpc v1.44.0 // indirect
 	google.golang.org/protobuf v1.27.1 // indirect
 	gopkg.in/ini.v1 v1.62.0 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
-	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 7fc8d5cb..0ab186a4 100644
--- a/go.sum
+++ b/go.sum
@@ -119,6 +119,7 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
 github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA=
 github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk=
@@ -153,11 +154,17 @@ github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUe
 github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
 github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
 github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
+github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
+github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
 github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
 github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
+github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
+github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
 github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
 github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
 github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
+github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU=
+github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
 github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
 github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
@@ -175,6 +182,8 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
 github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
 github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/gocarina/gocsv v0.0.0-20220707092902-b9da1f06c77e h1:GMIV+S6grz+vlIaUsP+fedQ6L+FovyMPMY26WO8dwQE=
+github.com/gocarina/gocsv v0.0.0-20220707092902-b9da1f06c77e/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
@@ -412,6 +421,8 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN
 github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
 github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
 github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
 github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
 github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
@@ -556,6 +567,8 @@ github.com/swaggo/gin-swagger v1.4.3 h1:mHJz+yzJne0udgYnC5qlDf4e7KuxUbVNX2dhD/cw
 github.com/swaggo/gin-swagger v1.4.3/go.mod h1:hBg6tGeKJsUu/P79BH+WGUR8nq2LuGE0O160+s4iefo=
 github.com/swaggo/swag v1.8.1 h1:JuARzFX1Z1njbCGz+ZytBR15TFJwF2Q7fu8puJHhQYI=
 github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
+github.com/swaggo/swag v1.8.3 h1:3pZSSCQ//gAH88lfmxM3Cd1+JCsxV8Md6f36b9hrZ5s=
+github.com/swaggo/swag v1.8.3/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg=
 github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
 github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E=
 github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0=
@@ -563,10 +576,14 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY
 github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ=
 github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw=
 github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
+github.com/urfave/cli/v2 v2.11.0 h1:c6bD90aLd2iEsokxhxkY5Er0zA2V9fId2aJfwmrF+do=
+github.com/urfave/cli/v2 v2.11.0/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo=
 github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
 github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
 github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
 github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
+github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
+github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -706,6 +723,8 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
 golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220708220712-1185a9018129 h1:vucSRfWwTsoXro7P+3Cjlr6flUMtzCwzlvkxEQtHHB0=
+golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -797,6 +816,8 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220222200937-f2425489ef4c h1:sSIdNI2Dd6vGv47bKc/xArpfxVmEz2+3j0E6I484xC4=
 golang.org/x/sys v0.0.0-20220222200937-f2425489ef4c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e h1:NHvCuwuS43lGnYhten69ZWqi2QOj/CiDNcKbVqwVoew=
+golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
@@ -875,6 +896,8 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
 golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
+golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY=
+golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
 golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1022,6 +1045,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gorm.io/datatypes v1.0.1 h1:6npnXbBtjpSb7FFVA2dG/llyTN8tvZfbUqs+WyLrYgQ=
 gorm.io/datatypes v1.0.1/go.mod h1:HEHoUU3/PO5ZXfAJcVWl11+zWlE16+O0X2DgJEb4Ixs=
 gorm.io/driver/mysql v1.0.5/go.mod h1:N1OIhHAIhx5SunkMGqWbGFVeh4yTNWKmMo1GOAsohLI=
diff --git a/models/domainlayer/crossdomain/team_user.go b/models/domainlayer/crossdomain/team_user.go
index 41417a66..c3d98f26 100644
--- a/models/domainlayer/crossdomain/team_user.go
+++ b/models/domainlayer/crossdomain/team_user.go
@@ -17,9 +17,12 @@ limitations under the License.
 
 package crossdomain
 
+import "github.com/apache/incubator-devlake/models/common"
+
 type TeamUser struct {
 	TeamId string `gorm:"primaryKey;type:varchar(255)"`
 	UserId string `gorm:"primaryKey;type:varchar(255)"`
+	common.NoPKModel
 }
 
 func (TeamUser) TableName() string {
diff --git a/models/domainlayer/crossdomain/user_account.go b/models/domainlayer/crossdomain/user_account.go
index 405d6f58..e6402447 100644
--- a/models/domainlayer/crossdomain/user_account.go
+++ b/models/domainlayer/crossdomain/user_account.go
@@ -17,9 +17,12 @@ limitations under the License.
 
 package crossdomain
 
+import "github.com/apache/incubator-devlake/models/common"
+
 type UserAccount struct {
-	UserId    string `gorm:"primaryKey;type:varchar(255)"`
+	UserId    string `gorm:"type:varchar(255)"`
 	AccountId string `gorm:"primaryKey;type:varchar(255)"`
+	common.NoPKModel
 }
 
 func (UserAccount) TableName() string {
diff --git a/models/migrationscripts/archived/team_user.go b/models/migrationscripts/archived/team_user.go
index 9cd3bf27..ff143ca1 100644
--- a/models/migrationscripts/archived/team_user.go
+++ b/models/migrationscripts/archived/team_user.go
@@ -20,6 +20,7 @@ package archived
 type TeamUser struct {
 	TeamId string `gorm:"primaryKey;type:varchar(255)"`
 	UserId string `gorm:"primaryKey;type:varchar(255)"`
+	NoPKModel
 }
 
 func (TeamUser) TableName() string {
diff --git a/models/migrationscripts/archived/user_account.go b/models/migrationscripts/archived/user_account.go
index 5a9acd7f..f4a9adf1 100644
--- a/models/migrationscripts/archived/user_account.go
+++ b/models/migrationscripts/archived/user_account.go
@@ -18,8 +18,9 @@ limitations under the License.
 package archived
 
 type UserAccount struct {
-	UserId    string `gorm:"primaryKey;type:varchar(255)"`
+	UserId    string `gorm:"type:varchar(255)"`
 	AccountId string `gorm:"primaryKey;type:varchar(255)"`
+	NoPKModel
 }
 
 func (UserAccount) TableName() string {
diff --git a/plugins/core/plugin_api.go b/plugins/core/plugin_api.go
index a57a33d1..d7dfdd9b 100644
--- a/plugins/core/plugin_api.go
+++ b/plugins/core/plugin_api.go
@@ -17,19 +17,30 @@ limitations under the License.
 
 package core
 
-import "net/url"
+import (
+	"net/http"
+	"net/url"
+)
 
 // Contains api request information
 type ApiResourceInput struct {
-	Params map[string]string      // path variables
-	Query  url.Values             // query string
-	Body   map[string]interface{} // json body
+	Params  map[string]string      // path variables
+	Query   url.Values             // query string
+	Body    map[string]interface{} // json body
+	Request *http.Request
+}
+
+// OutputFile is the file returned
+type OutputFile struct {
+	ContentType string
+	Data        []byte
 }
 
 // Describe response data of a api
 type ApiResourceOutput struct {
 	Body   interface{} // response body
 	Status int
+	File   *OutputFile
 }
 
 type ApiResourceHandler func(input *ApiResourceInput) (*ApiResourceOutput, error)
diff --git a/plugins/org/api/handlers.go b/plugins/org/api/handlers.go
new file mode 100644
index 00000000..ee5947e6
--- /dev/null
+++ b/plugins/org/api/handlers.go
@@ -0,0 +1,60 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+*/
+
+package api
+
+import (
+	"encoding/csv"
+	"errors"
+	"net/http"
+
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/core/dal"
+	"github.com/gocarina/gocsv"
+)
+
+const maxMemory = 32 << 20 // 32 MB
+
+type Handlers struct {
+	store store
+}
+
+func NewHandlers(db dal.Dal, basicRes core.BasicRes) *Handlers {
+	return &Handlers{store: NewDbStore(db, basicRes)}
+}
+
+func (h *Handlers) unmarshal(r *http.Request, items interface{}) error {
+	if r == nil {
+		return errors.New("request is nil")
+	}
+	if r.MultipartForm == nil {
+		if err := r.ParseMultipartForm(maxMemory); err != nil {
+			return err
+		}
+	}
+	f, fh, err := r.FormFile("file")
+	if err != nil {
+		return err
+	}
+	f.Close()
+	file, err := fh.Open()
+	if err != nil {
+		return err
+	}
+	defer file.Close()
+	return gocsv.UnmarshalCSV(csv.NewReader(file), items)
+}
diff --git a/plugins/org/api/store.go b/plugins/org/api/store.go
new file mode 100644
index 00000000..903b45b9
--- /dev/null
+++ b/plugins/org/api/store.go
@@ -0,0 +1,113 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+*/
+
+package api
+
+import (
+	"reflect"
+
+	"github.com/apache/incubator-devlake/models/domainlayer/crossdomain"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/core/dal"
+	"github.com/apache/incubator-devlake/plugins/helper"
+)
+
+type store interface {
+	findAllUsers() ([]user, error)
+	findAllTeams() ([]team, error)
+	findAllAccounts() ([]account, error)
+	findAllUserAccounts() ([]userAccount, error)
+	deleteAll(i interface{}) error
+	save(items []interface{}) error
+}
+
+type dbStore struct {
+	db     dal.Dal
+	driver *helper.BatchSaveDivider
+}
+
+func NewDbStore(db dal.Dal, basicRes core.BasicRes) *dbStore {
+	driver := helper.NewBatchSaveDivider(basicRes, 1000, "", "")
+	return &dbStore{db: db, driver: driver}
+}
+
+func (d *dbStore) findAllUsers() ([]user, error) {
+	var u *user
+	var uu []crossdomain.User
+	err := d.db.All(&uu)
+	if err != nil {
+		return nil, err
+	}
+	var tus []crossdomain.TeamUser
+	err = d.db.All(&tus)
+	if err != nil {
+		return nil, err
+	}
+	return u.fromDomainLayer(uu, tus), nil
+}
+func (d *dbStore) findAllTeams() ([]team, error) {
+	var tt []crossdomain.Team
+	err := d.db.All(&tt)
+	if err != nil {
+		return nil, err
+	}
+	var t *team
+	return t.fromDomainLayer(tt), nil
+}
+func (d *dbStore) findAllAccounts() ([]account, error) {
+	var aa []crossdomain.Account
+	err := d.db.All(&aa)
+	if err != nil {
+		return nil, err
+	}
+	var ua []crossdomain.UserAccount
+	err = d.db.All(&ua)
+	if err != nil {
+		return nil, err
+	}
+	var a *account
+	return a.fromDomainLayer(aa, ua), nil
+}
+
+func (d *dbStore) findAllUserAccounts() ([]userAccount, error) {
+	var uas []crossdomain.UserAccount
+	err := d.db.All(&uas)
+	if err != nil {
+		return nil, err
+	}
+
+	var au *userAccount
+	return au.fromDomainLayer(uas), nil
+}
+func (d *dbStore) deleteAll(i interface{}) error {
+	return d.db.Delete(i, dal.Where("1=1"))
+}
+
+func (d *dbStore) save(items []interface{}) error {
+	for _, item := range items {
+		batch, err := d.driver.ForType(reflect.TypeOf(item))
+		if err != nil {
+			return err
+		}
+		err = batch.Add(item)
+		if err != nil {
+			return err
+		}
+	}
+	d.driver.Close()
+	return nil
+}
diff --git a/plugins/org/api/team.go b/plugins/org/api/team.go
new file mode 100644
index 00000000..17a055d4
--- /dev/null
+++ b/plugins/org/api/team.go
@@ -0,0 +1,96 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+*/
+
+package api
+
+import (
+	"github.com/apache/incubator-devlake/plugins/core"
+	"net/http"
+
+	"github.com/apache/incubator-devlake/models/domainlayer/crossdomain"
+	"github.com/gocarina/gocsv"
+)
+
+// GetTeam godoc
+// @Summary      Get teams.csv file
+// @Description  get teams.csv file
+// @Tags 		 plugins/org
+// @Produce      text/csv
+// @Param        fake_data    query     bool  false  "return fake data or not"
+// @Success      200
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router       /plugins/org/teams.csv [get]
+func (h *Handlers) GetTeam(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
+	input.Query.Get("fake_data")
+	var teams []team
+	var t *team
+	var err error
+	if input.Query.Get("fake_data") == "true" {
+		teams = t.fakeData()
+	} else {
+		teams, err = h.store.findAllTeams()
+		if err != nil {
+			return nil, err
+		}
+	}
+	blob, err := gocsv.MarshalBytes(teams)
+	if err != nil {
+		return nil, err
+	}
+	return &core.ApiResourceOutput{
+		Body:   nil,
+		Status: http.StatusOK,
+		File: &core.OutputFile{
+			ContentType: "text/csv",
+			Data:        blob,
+		},
+	}, nil
+}
+
+// CreateTeam godoc
+// @Summary      Upload teams.csv file
+// @Description  upload teams.csv file
+// @Tags 		 plugins/org
+// @Accept       multipart/form-data
+// @Param        file formData file true "select file to upload"
+// @Produce      json
+// @Success      200
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router       /plugins/org/teams.csv [put]
+func (h *Handlers) CreateTeam(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
+	var tt []team
+	err := h.unmarshal(input.Request, &tt)
+	if err != nil {
+		return nil, err
+	}
+	var t *team
+	var items []interface{}
+	for _, tm := range t.toDomainLayer(tt) {
+		items = append(items, tm)
+	}
+	err = h.store.deleteAll(&crossdomain.Team{})
+	if err != nil {
+		return nil, err
+	}
+	err = h.store.save(items)
+	if err != nil {
+		return nil, err
+	}
+	return &core.ApiResourceOutput{Status: http.StatusOK}, nil
+}
diff --git a/plugins/org/api/types.go b/plugins/org/api/types.go
new file mode 100644
index 00000000..98d7be9a
--- /dev/null
+++ b/plugins/org/api/types.go
@@ -0,0 +1,225 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+*/
+
+package api
+
+import (
+	"github.com/apache/incubator-devlake/models/domainlayer"
+	"github.com/apache/incubator-devlake/models/domainlayer/crossdomain"
+	"strings"
+)
+
+const TimeFormat = "2006-01-02"
+
+var fakeUsers = []user{{
+	Id:      "1",
+	Name:    "Tyrone K. Cummings",
+	Email:   "TyroneKCummings@teleworm.us",
+	TeamIds: "1;2",
+}, {
+	Id:      "2",
+	Name:    "Dorothy R. Updegraff",
+	Email:   "DorothyRUpdegraff@dayrep.com",
+	TeamIds: "3",
+}}
+
+var fakeTeams = []team{{
+	Id:           "1",
+	Name:         "Maple Leafs",
+	Alias:        "ML",
+	ParentId:     "2",
+	SortingIndex: 0,
+}, {
+	Id:           "2",
+	Name:         "Friendly Confines",
+	Alias:        "FC",
+	ParentId:     "",
+	SortingIndex: 1,
+}, {
+	Id:           "3",
+	Name:         "Blue Jays",
+	Alias:        "BJ",
+	ParentId:     "",
+	SortingIndex: 2,
+}}
+
+type user struct {
+	Id      string
+	Name    string
+	Email   string
+	TeamIds string
+}
+
+func (*user) fromDomainLayer(users []crossdomain.User, teamUsers []crossdomain.TeamUser) []user {
+	var result []user
+	teamUserMap := make(map[string][]string)
+	for _, tu := range teamUsers {
+		teamUserMap[tu.UserId] = append(teamUserMap[tu.UserId], tu.TeamId)
+	}
+	for _, u := range users {
+		result = append(result, user{
+			Id:      u.Id,
+			Name:    u.Name,
+			Email:   u.Email,
+			TeamIds: strings.Join(teamUserMap[u.Id], ";"),
+		})
+	}
+	return result
+}
+
+func (*user) toDomainLayer(uu []user) (users []*crossdomain.User, teamUsers []*crossdomain.TeamUser) {
+	for _, u := range uu {
+		users = append(users, &crossdomain.User{
+			DomainEntity: domainlayer.DomainEntity{Id: u.Id},
+			Email:        u.Email,
+			Name:         u.Name,
+		})
+		for _, teamId := range strings.Split(u.TeamIds, ";") {
+			if u.Id == "" || teamId == "" {
+				continue
+			}
+			teamUsers = append(teamUsers, &crossdomain.TeamUser{
+				TeamId: teamId,
+				UserId: u.Id,
+			})
+		}
+	}
+	return
+}
+
+func (*user) fakeData() []user {
+	return fakeUsers
+}
+
+type account struct {
+	Id           string
+	Email        string
+	FullName     string
+	UserName     string
+	AvatarUrl    string
+	Organization string
+	CreatedDate  string
+	Status       int
+	UserId       string
+}
+
+func (*account) fromDomainLayer(accounts []crossdomain.Account, userAccounts []crossdomain.UserAccount) []account {
+	var result []account
+	userAccountMap := make(map[string]string)
+	for _, ua := range userAccounts {
+		userAccountMap[ua.AccountId] = ua.UserId
+	}
+	for _, a := range accounts {
+		var createdDate string
+		if a.CreatedDate != nil {
+			createdDate = a.CreatedDate.Format(TimeFormat)
+		}
+		result = append(result, account{
+			Id:           a.Id,
+			Email:        a.Email,
+			FullName:     a.FullName,
+			UserName:     a.UserName,
+			AvatarUrl:    a.AvatarUrl,
+			Organization: a.Organization,
+			CreatedDate:  createdDate,
+			Status:       a.Status,
+			UserId:       userAccountMap[a.Id],
+		})
+	}
+	return result
+}
+
+func (*account) toDomainLayer(aa []account) []*crossdomain.UserAccount {
+	var userAccounts []*crossdomain.UserAccount
+	for _, a := range aa {
+		if a.UserId == "" || a.Id == "" {
+			continue
+		}
+		userAccounts = append(userAccounts, &crossdomain.UserAccount{
+			UserId:    a.UserId,
+			AccountId: a.Id,
+		})
+	}
+	return userAccounts
+}
+
+type userAccount struct {
+	AccountId string
+	UserId    string
+}
+
+func (au *userAccount) toDomainLayer(accountUsers []userAccount) []*crossdomain.UserAccount {
+	result := make([]*crossdomain.UserAccount, 0, len(accountUsers))
+	for _, ac := range accountUsers {
+		result = append(result, &crossdomain.UserAccount{
+			UserId:    ac.UserId,
+			AccountId: ac.AccountId,
+		})
+	}
+	return result
+}
+
+func (au *userAccount) fromDomainLayer(accountUsers []crossdomain.UserAccount) []userAccount {
+	result := make([]userAccount, 0, len(accountUsers))
+	for _, ac := range accountUsers {
+		result = append(result, userAccount{
+			UserId:    ac.UserId,
+			AccountId: ac.AccountId,
+		})
+	}
+	return result
+}
+
+type team struct {
+	Id           string
+	Name         string
+	Alias        string
+	ParentId     string
+	SortingIndex int
+}
+
+func (*team) fromDomainLayer(tt []crossdomain.Team) []team {
+	var result []team
+	for _, t := range tt {
+		result = append(result, team{
+			Id:           t.Id,
+			Name:         t.Name,
+			Alias:        t.Alias,
+			ParentId:     t.ParentId,
+			SortingIndex: t.SortingIndex,
+		})
+	}
+	return result
+}
+
+func (*team) toDomainLayer(tt []team) []*crossdomain.Team {
+	var result []*crossdomain.Team
+	for _, t := range tt {
+		result = append(result, &crossdomain.Team{
+			DomainEntity: domainlayer.DomainEntity{Id: t.Id},
+			Name:         t.Name,
+			Alias:        t.Alias,
+			ParentId:     t.ParentId,
+			SortingIndex: t.SortingIndex,
+		})
+	}
+	return result
+}
+
+func (*team) fakeData() []team {
+	return fakeTeams
+}
diff --git a/plugins/org/api/user.go b/plugins/org/api/user.go
new file mode 100644
index 00000000..bc14e1ae
--- /dev/null
+++ b/plugins/org/api/user.go
@@ -0,0 +1,103 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+*/
+
+package api
+
+import (
+	"net/http"
+
+	"github.com/apache/incubator-devlake/models/domainlayer/crossdomain"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/gocarina/gocsv"
+)
+
+// GetUser godoc
+// @Summary      Get users.csv file
+// @Description  get users.csv file
+// @Tags 		 plugins/org
+// @Produce      text/csv
+// @Param        fake_data    query     bool  false  "return fake data or not"
+// @Success      200
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router       /plugins/org/users.csv [get]
+func (h *Handlers) GetUser(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
+	var users []user
+	var u *user
+	var err error
+	if input.Query.Get("fake_data") == "true" {
+		users = u.fakeData()
+	} else {
+		users, err = h.store.findAllUsers()
+		if err != nil {
+			return nil, err
+		}
+	}
+	blob, err := gocsv.MarshalBytes(users)
+	if err != nil {
+		return nil, err
+	}
+	return &core.ApiResourceOutput{
+		Body:   nil,
+		Status: http.StatusOK,
+		File: &core.OutputFile{
+			ContentType: "text/csv",
+			Data:        blob,
+		},
+	}, nil
+}
+
+// CreateUser godoc
+// @Summary      Upload users.csv file
+// @Description  upload users.csv file
+// @Tags 		 plugins/org
+// @Accept       multipart/form-data
+// @Param        file formData file true "select file to upload"
+// @Produce      json
+// @Success      200
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router       /plugins/org/users.csv [put]
+func (h *Handlers) CreateUser(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
+	var uu []user
+	err := h.unmarshal(input.Request, &uu)
+	if err != nil {
+		return nil, err
+	}
+	var u *user
+	var items []interface{}
+	users, teamUsers := u.toDomainLayer(uu)
+	for _, user := range users {
+		items = append(items, user)
+	}
+	for _, teamUser := range teamUsers {
+		items = append(items, teamUser)
+	}
+	err = h.store.deleteAll(&crossdomain.User{})
+	if err != nil {
+		return nil, err
+	}
+	err = h.store.deleteAll(&crossdomain.TeamUser{})
+	if err != nil {
+		return nil, err
+	}
+	err = h.store.save(items)
+	if err != nil {
+		return nil, err
+	}
+	return &core.ApiResourceOutput{Status: http.StatusOK}, nil
+}
diff --git a/plugins/org/api/user_account_mapping.go b/plugins/org/api/user_account_mapping.go
new file mode 100644
index 00000000..077e3144
--- /dev/null
+++ b/plugins/org/api/user_account_mapping.go
@@ -0,0 +1,88 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+*/
+
+package api
+
+import (
+	"github.com/apache/incubator-devlake/models/domainlayer/crossdomain"
+	"net/http"
+
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/gocarina/gocsv"
+)
+
+// GetUserAccountMapping godoc
+// @Summary      Get user_account_mapping.csv.csv file
+// @Description  get user_account_mapping.csv.csv file
+// @Tags 		 plugins/org
+// @Produce      text/csv
+// @Success      200
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router       /plugins/org/user_account_mapping.csv [get]
+func (h *Handlers) GetUserAccountMapping(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
+	accounts, err := h.store.findAllAccounts()
+	if err != nil {
+		return nil, err
+	}
+	blob, err := gocsv.MarshalBytes(accounts)
+	if err != nil {
+		return nil, err
+	}
+	return &core.ApiResourceOutput{
+		Body:   nil,
+		Status: http.StatusOK,
+		File: &core.OutputFile{
+			ContentType: "text/csv",
+			Data:        blob,
+		},
+	}, nil
+}
+
+// CreateUserAccountMapping godoc
+// @Summary      Upload user_account_mapping.csv.csv file
+// @Description  upload user_account_mapping.csv.csv file
+// @Tags 		 plugins/org
+// @Accept       multipart/form-data
+// @Param        file formData file true "select file to upload"
+// @Produce      json
+// @Success      200
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router       /plugins/org/user_account_mapping.csv [put]
+func (h *Handlers) CreateUserAccountMapping(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
+	var aa []account
+	err := h.unmarshal(input.Request, &aa)
+	if err != nil {
+		return nil, err
+	}
+	var a *account
+	var items []interface{}
+	userAccounts := a.toDomainLayer(aa)
+	for _, userAccount := range userAccounts {
+		items = append(items, userAccount)
+	}
+	err = h.store.deleteAll(&crossdomain.UserAccount{})
+	if err != nil {
+		return nil, err
+	}
+	err = h.store.save(items)
+	if err != nil {
+		return nil, err
+	}
+	return &core.ApiResourceOutput{Status: http.StatusOK}, nil
+}
diff --git a/plugins/org/e2e/raw_tables/accounts.csv b/plugins/org/e2e/raw_tables/accounts.csv
new file mode 100644
index 00000000..6bbdb2a8
--- /dev/null
+++ b/plugins/org/e2e/raw_tables/accounts.csv
@@ -0,0 +1,11 @@
+"id","created_at","updated_at","_raw_data_params","_raw_data_table","_raw_data_id","_raw_data_remark","email","avatar_url","full_name","user_name","organization","created_date","status"
+"a1","2022-07-10 14:27:43.813","2022-07-10 14:27:43.813","","",0,"","e1","","n1","n1","","2022-07-10 14:27:43.813",0
+"a10","2022-07-10 14:27:43.813","2022-07-10 14:27:43.813","","",0,"","e15","","pq","n10","","2022-07-10 14:27:43.813",0
+"a2","2022-07-10 14:27:43.813","2022-07-10 14:27:43.813","","",0,"","e1","","n2","n2","","2022-07-10 14:27:43.813",0
+"a3","2022-07-10 14:27:43.813","2022-07-10 14:27:43.813","","",0,"","e2","","xyz","n3","","2022-07-10 14:27:43.813",0
+"a4","2022-07-10 14:27:43.813","2022-07-10 14:27:43.813","","",0,"","e4","","n4","n4","","2022-07-10 14:27:43.813",0
+"a5","2022-07-10 14:27:43.813","2022-07-10 14:27:43.813","","",0,"","e5","","abc","n5","","2022-07-10 14:27:43.813",0
+"a6","2022-07-10 14:27:43.813","2022-07-10 14:27:43.813","","",0,"","e5","","n6","n6","","2022-07-10 14:27:43.813",0
+"a7","2022-07-10 14:27:43.813","2022-07-10 14:27:43.813","","",0,"","","","n5","n7","","2022-07-10 14:27:43.813",0
+"a8","2022-07-10 14:27:43.813","2022-07-10 14:27:43.813","","",0,"","xy","","n8","n8","","2022-07-10 14:27:43.813",0
+"a9","2022-07-10 14:27:43.813","2022-07-10 14:27:43.813","","",0,"","e11","","def","n9","","2022-07-10 14:27:43.813",0
diff --git a/plugins/org/e2e/raw_tables/user_accounts.csv b/plugins/org/e2e/raw_tables/user_accounts.csv
new file mode 100644
index 00000000..2340c876
--- /dev/null
+++ b/plugins/org/e2e/raw_tables/user_accounts.csv
@@ -0,0 +1,5 @@
+account_id,user_id
+a1,U111
+a2,U112
+a3,U113
+
diff --git a/plugins/org/e2e/raw_tables/users.csv b/plugins/org/e2e/raw_tables/users.csv
new file mode 100644
index 00000000..afab7c04
--- /dev/null
+++ b/plugins/org/e2e/raw_tables/users.csv
@@ -0,0 +1,11 @@
+"id","created_at","updated_at","_raw_data_params","_raw_data_table","_raw_data_id","_raw_data_remark","email","name"
+"U001","2022-07-10 15:29:51.239","2022-07-10 15:29:51.239","","",0,"","e1","n1"
+"U002","2022-07-10 15:29:51.239","2022-07-10 15:29:51.239","","",0,"","e2","n2"
+"U003","2022-07-10 15:29:51.239","2022-07-10 15:29:51.239","","",0,"","e3","n3"
+"U004","2022-07-10 15:29:51.239","2022-07-10 15:29:51.239","","",0,"","e4","n4"
+"U005","2022-07-10 15:29:51.239","2022-07-10 15:29:51.239","","",0,"","e5","n5"
+"U006","2022-07-10 15:29:51.239","2022-07-10 15:29:51.239","","",0,"","e6","n6"
+"U007","2022-07-10 15:29:51.239","2022-07-10 15:29:51.239","","",0,"","e7","n7"
+"U008","2022-07-10 15:29:51.239","2022-07-10 15:29:51.239","","",0,"","e8","n8"
+"U009","2022-07-10 15:29:51.239","2022-07-10 15:29:51.239","","",0,"","e9","n9"
+"U010","2022-07-10 15:29:51.239","2022-07-10 15:29:51.239","","",0,"","e10","n10"
diff --git a/plugins/org/e2e/snapshot_tables/user_accounts.csv b/plugins/org/e2e/snapshot_tables/user_accounts.csv
new file mode 100644
index 00000000..2acffef1
--- /dev/null
+++ b/plugins/org/e2e/snapshot_tables/user_accounts.csv
@@ -0,0 +1,11 @@
+account_id,user_id
+a1,U111
+a10,U010
+a2,U112
+a3,U113
+a4,U004
+a5,U005
+a6,U005
+a7,U005
+a8,U008
+a9,U009
diff --git a/plugins/org/e2e/user_account_test.go b/plugins/org/e2e/user_account_test.go
new file mode 100644
index 00000000..24f20169
--- /dev/null
+++ b/plugins/org/e2e/user_account_test.go
@@ -0,0 +1,56 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+*/
+
+package e2e
+
+import (
+	"github.com/apache/incubator-devlake/models/domainlayer/crossdomain"
+	"testing"
+
+	"github.com/apache/incubator-devlake/helpers/e2ehelper"
+	"github.com/apache/incubator-devlake/plugins/org/impl"
+	"github.com/apache/incubator-devlake/plugins/org/tasks"
+)
+
+func TestUserAccountDataFlow(t *testing.T) {
+	var plugin impl.Org
+	dataflowTester := e2ehelper.NewDataFlowTester(t, "org", plugin)
+
+	taskData := &tasks.TaskData{
+		Options: &tasks.Options{
+			ConnectionId: 2,
+		},
+	}
+
+	// import raw data table
+	dataflowTester.FlushTabler(&crossdomain.User{})
+	dataflowTester.FlushTabler(&crossdomain.Account{})
+	dataflowTester.FlushTabler(&crossdomain.UserAccount{})
+	dataflowTester.ImportCsvIntoTabler("./raw_tables/users.csv", &crossdomain.User{})
+	dataflowTester.ImportCsvIntoTabler("./raw_tables/accounts.csv", &crossdomain.Account{})
+	dataflowTester.ImportCsvIntoTabler("./raw_tables/user_accounts.csv", &crossdomain.UserAccount{})
+
+	dataflowTester.Subtask(tasks.ConnectUserAccountsExactMeta, taskData)
+	dataflowTester.VerifyTable(
+		crossdomain.UserAccount{},
+		"./snapshot_tables/user_accounts.csv",
+		[]string{
+			"user_id",
+			"account_id",
+		},
+	)
+}
diff --git a/plugins/org/impl/impl.go b/plugins/org/impl/impl.go
new file mode 100644
index 00000000..4faec005
--- /dev/null
+++ b/plugins/org/impl/impl.go
@@ -0,0 +1,87 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+*/
+
+package impl
+
+import (
+	"github.com/apache/incubator-devlake/impl/dalgorm"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/apache/incubator-devlake/plugins/org/api"
+	"github.com/apache/incubator-devlake/plugins/org/tasks"
+	"github.com/mitchellh/mapstructure"
+	"github.com/spf13/viper"
+	"gorm.io/gorm"
+)
+
+var _ core.PluginMeta = (*Org)(nil)
+var _ core.PluginInit = (*Org)(nil)
+var _ core.PluginTask = (*Org)(nil)
+
+type Org struct {
+	handlers *api.Handlers
+}
+
+func (plugin *Org) Init(config *viper.Viper, logger core.Logger, db *gorm.DB) error {
+	basicRes := helper.NewDefaultBasicRes(config, logger, db)
+	plugin.handlers = api.NewHandlers(dalgorm.NewDalgorm(db), basicRes)
+	return nil
+}
+
+func (plugin Org) Description() string {
+	return "collect data related to team and organization"
+}
+
+func (plugin Org) SubTaskMetas() []core.SubTaskMeta {
+	return []core.SubTaskMeta{
+		tasks.ConnectUserAccountsExactMeta,
+	}
+}
+
+func (plugin Org) PrepareTaskData(taskCtx core.TaskContext, options map[string]interface{}) (interface{}, error) {
+	var op tasks.Options
+	err := mapstructure.Decode(options, &op)
+	if err != nil {
+		return nil, err
+	}
+	taskData := &tasks.TaskData{
+		Options: &op,
+	}
+	return taskData, nil
+}
+
+func (plugin Org) RootPkgPath() string {
+	return "github.com/apache/incubator-devlake/plugins/org"
+}
+
+func (plugin Org) ApiResources() map[string]map[string]core.ApiResourceHandler {
+	return map[string]map[string]core.ApiResourceHandler{
+		"teams.csv": {
+			"GET": plugin.handlers.GetTeam,
+			"PUT": plugin.handlers.CreateTeam,
+		},
+		"users.csv": {
+			"GET": plugin.handlers.GetUser,
+			"PUT": plugin.handlers.CreateUser,
+		},
+
+		"user_account_mapping.csv": {
+			"GET": plugin.handlers.GetUserAccountMapping,
+			"PUT": plugin.handlers.CreateUserAccountMapping,
+		},
+	}
+}
diff --git a/models/migrationscripts/archived/team_user.go b/plugins/org/org.go
similarity index 78%
copy from models/migrationscripts/archived/team_user.go
copy to plugins/org/org.go
index 9cd3bf27..5cb7a393 100644
--- a/models/migrationscripts/archived/team_user.go
+++ b/plugins/org/org.go
@@ -15,13 +15,8 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package archived
+package main
 
-type TeamUser struct {
-	TeamId string `gorm:"primaryKey;type:varchar(255)"`
-	UserId string `gorm:"primaryKey;type:varchar(255)"`
-}
+import "github.com/apache/incubator-devlake/plugins/org/impl"
 
-func (TeamUser) TableName() string {
-	return "team_users"
-}
+var PluginEntry impl.Org //nolint
diff --git a/models/migrationscripts/archived/team_user.go b/plugins/org/tasks/task_data.go
similarity index 79%
copy from models/migrationscripts/archived/team_user.go
copy to plugins/org/tasks/task_data.go
index 9cd3bf27..f78913d2 100644
--- a/models/migrationscripts/archived/team_user.go
+++ b/plugins/org/tasks/task_data.go
@@ -15,13 +15,15 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package archived
+package tasks
 
-type TeamUser struct {
-	TeamId string `gorm:"primaryKey;type:varchar(255)"`
-	UserId string `gorm:"primaryKey;type:varchar(255)"`
+type Options struct {
+	ConnectionId uint64 `json:"connectionId"`
 }
 
-func (TeamUser) TableName() string {
-	return "team_users"
+type TaskData struct {
+	Options *Options
+}
+type Params struct {
+	ConnectionId uint64
 }
diff --git a/plugins/org/tasks/user_account.go b/plugins/org/tasks/user_account.go
new file mode 100644
index 00000000..0d69207d
--- /dev/null
+++ b/plugins/org/tasks/user_account.go
@@ -0,0 +1,116 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+*/
+
+package tasks
+
+import (
+	"github.com/apache/incubator-devlake/models/common"
+	"reflect"
+
+	"github.com/apache/incubator-devlake/models/domainlayer/crossdomain"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/core/dal"
+	"github.com/apache/incubator-devlake/plugins/helper"
+)
+
+var ConnectUserAccountsExactMeta = core.SubTaskMeta{
+	Name:             "connectUserAccountsExact",
+	EntryPoint:       ConnectUserAccountsExact,
+	EnabledByDefault: true,
+	Description:      "associate users and accounts",
+	DomainTypes:      []string{core.DOMAIN_TYPE_CROSS},
+}
+
+func ConnectUserAccountsExact(taskCtx core.SubTaskContext) error {
+	db := taskCtx.GetDal()
+	data := taskCtx.GetData().(*TaskData)
+	type input struct {
+		UserId    string
+		AccountId string
+		common.NoPKModel
+	}
+	var users []crossdomain.User
+	err := db.All(&users)
+	if err != nil {
+		return err
+	}
+	emails := make(map[string]string)
+	names := make(map[string]string)
+	for _, user := range users {
+		if user.Email != "" {
+			emails[user.Email] = user.Id
+		}
+		if user.Name != "" {
+			names[user.Name] = user.Id
+		}
+	}
+	clauses := []dal.Clause{
+		dal.Select("*"),
+		dal.From(&crossdomain.Account{}),
+		dal.Where("id NOT IN (SELECT account_id FROM user_accounts)"),
+	}
+	cursor, err := db.Cursor(clauses...)
+	if err != nil {
+		return err
+	}
+	defer cursor.Close()
+
+	converter, err := helper.NewDataConverter(helper.DataConverterArgs{
+		InputRowType: reflect.TypeOf(crossdomain.Account{}),
+		Input:        cursor,
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: Params{
+				ConnectionId: data.Options.ConnectionId,
+			},
+			Table: "users",
+		},
+
+		Convert: func(inputRow interface{}) ([]interface{}, error) {
+			account := inputRow.(*crossdomain.Account)
+			if userId, ok := emails[account.Email]; account.Email != "" && ok {
+				return []interface{}{
+					&crossdomain.UserAccount{
+						UserId:    userId,
+						AccountId: account.Id,
+					},
+				}, nil
+			}
+			if userId, ok := names[account.FullName]; account.FullName != "" && ok {
+				return []interface{}{
+					&crossdomain.UserAccount{
+						UserId:    userId,
+						AccountId: account.Id,
+					},
+				}, nil
+			}
+			if userId, ok := names[account.UserName]; account.UserName != "" && ok {
+				return []interface{}{
+					&crossdomain.UserAccount{
+						UserId:    userId,
+						AccountId: account.Id,
+					},
+				}, nil
+			}
+			return nil, nil
+		},
+	})
+	if err != nil {
+		return err
+	}
+	return converter.Execute()
+}