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

lynwee 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 df1a1b928 Complete #5643 Not returning the tokens or passwords on APIs 
(#6494)
df1a1b928 is described below

commit df1a1b9288f8fb29c6733aad220dd94cdb332f3d
Author: Lynwee <[email protected]>
AuthorDate: Thu Nov 23 19:53:00 2023 +0800

    Complete #5643 Not returning the tokens or passwords on APIs (#6494)
    
    * fix(typo): rename
    
    * feat(helper): remove sensitive token from GitHub/GitLab plugins
    
    * feat(plugin): remove sensitive token from other plugins(except 
AzureDevops)
    
    * feat(plugin): add new test connection api
    
    * fix(helper): remove sensitive info
    
    * fix(framework): rename some functions
    
    * refactor(helper): rename some methods
    
    * refactor(helper): rename some parameters
---
 backend/core/models/locking.go                     |   2 +-
 backend/helpers/pluginhelper/api/api_extractor.go  |   2 +-
 .../pluginhelper/api/ds_connection_api_helper.go   |   9 +-
 backend/helpers/pluginhelper/api/ds_helper.go      |   9 +-
 .../pluginhelper/api/ds_scope_api_helper.go        |   8 +-
 .../pluginhelper/api/ds_scope_config_api_helper.go |   9 +-
 .../helpers/pluginhelper/api/model_api_helper.go   |  35 ++++-
 backend/plugins/ae/api/connection.go               |  74 +++++++---
 backend/plugins/ae/impl/impl.go                    |   3 +
 backend/plugins/ae/models/connection.go            |   5 +
 backend/plugins/bamboo/api/connection.go           |  73 +++++++---
 backend/plugins/bamboo/impl/impl.go                |   3 +
 backend/plugins/bamboo/models/connection.go        |  10 ++
 .../plugins/bamboo/tasks/plan_build_extractor.go   |   2 +-
 backend/plugins/bitbucket/api/connection.go        |  89 +++++++++---
 backend/plugins/bitbucket/impl/impl.go             |   3 +
 backend/plugins/bitbucket/models/connection.go     |  10 ++
 backend/plugins/circleci/api/connection.go         |  83 ++++++++---
 backend/plugins/circleci/impl/impl.go              |   3 +
 backend/plugins/circleci/models/connection.go      |   5 +
 backend/plugins/feishu/api/connection.go           |  70 +++++++--
 backend/plugins/feishu/impl/impl.go                |   3 +
 backend/plugins/feishu/models/connection.go        |  10 ++
 backend/plugins/gitee/api/connection.go            |  89 +++++++++---
 backend/plugins/gitee/impl/impl.go                 |   3 +
 backend/plugins/gitee/models/connection.go         |  10 ++
 backend/plugins/github/api/connection_api.go       | 158 ++++++++++++---------
 backend/plugins/github/api/init.go                 |   8 +-
 backend/plugins/github/impl/impl.go                |   3 +
 backend/plugins/github/models/connection.go        |   9 +-
 backend/plugins/gitlab/api/connection_api.go       |  69 ++++++---
 backend/plugins/gitlab/api/init.go                 |   5 +
 backend/plugins/gitlab/impl/impl.go                |   3 +
 backend/plugins/gitlab/models/connection.go        |  10 ++
 backend/plugins/jenkins/api/connection.go          |  91 +++++++++---
 backend/plugins/jenkins/impl/impl.go               |   3 +
 backend/plugins/jenkins/models/connection.go       |  10 ++
 backend/plugins/jira/api/connection.go             |  96 +++++++++----
 backend/plugins/jira/impl/impl.go                  |   3 +
 backend/plugins/jira/models/connection.go          |  11 ++
 backend/plugins/pagerduty/api/connection.go        |  73 +++++++---
 backend/plugins/pagerduty/impl/impl.go             |   3 +
 backend/plugins/pagerduty/models/connection.go     |   5 +
 backend/plugins/slack/api/connection.go            |  69 +++++++--
 backend/plugins/slack/impl/impl.go                 |   3 +
 backend/plugins/slack/models/connection.go         |  10 ++
 backend/plugins/sonarqube/api/connection.go        |  87 ++++++++----
 backend/plugins/sonarqube/impl/impl.go             |   3 +
 backend/plugins/sonarqube/models/connection.go     |  10 ++
 backend/plugins/tapd/api/connection.go             |  86 ++++++++---
 backend/plugins/tapd/impl/impl.go                  |   3 +
 backend/plugins/tapd/models/connection.go          |  10 ++
 backend/plugins/teambition/api/connection.go       |  85 ++++++++---
 backend/plugins/teambition/impl/impl.go            |   3 +
 backend/plugins/teambition/models/connection.go    |  10 ++
 backend/plugins/trello/api/connection.go           |  87 +++++++++---
 backend/plugins/trello/impl/impl.go                |   3 +
 backend/plugins/trello/models/connection.go        |  10 ++
 backend/plugins/zentao/api/connection.go           |  92 +++++++++---
 backend/plugins/zentao/impl/impl.go                |   3 +
 backend/plugins/zentao/models/connection.go        |  33 +++++
 61 files changed, 1394 insertions(+), 395 deletions(-)

diff --git a/backend/core/models/locking.go b/backend/core/models/locking.go
index 3bf02fc20..9510e24f3 100644
--- a/backend/core/models/locking.go
+++ b/backend/core/models/locking.go
@@ -43,7 +43,7 @@ func (LockingHistory) TableName() string {
 
 // LockingStub does nothing but offer a locking target
 type LockingStub struct {
-       Stub string `gorm:"primaryKey"`
+       Stub string `gorm:"primaryKey;type:varchar(255)"`
 }
 
 func (LockingStub) TableName() string {
diff --git a/backend/helpers/pluginhelper/api/api_extractor.go 
b/backend/helpers/pluginhelper/api/api_extractor.go
index 189b5618c..a2cd8a1eb 100644
--- a/backend/helpers/pluginhelper/api/api_extractor.go
+++ b/backend/helpers/pluginhelper/api/api_extractor.go
@@ -98,7 +98,7 @@ func (extractor *ApiExtractor) Execute() errors.Error {
        // batch save divider
        divider := NewBatchSaveDivider(extractor.args.Ctx, 
extractor.args.BatchSize, extractor.table, extractor.params)
 
-       // prgress
+       // progress
        extractor.args.Ctx.SetProgress(0, -1)
        ctx := extractor.args.Ctx.GetContext()
        // iterate all rows
diff --git a/backend/helpers/pluginhelper/api/ds_connection_api_helper.go 
b/backend/helpers/pluginhelper/api/ds_connection_api_helper.go
index 8d7bf5a55..3525ae2de 100644
--- a/backend/helpers/pluginhelper/api/ds_connection_api_helper.go
+++ b/backend/helpers/pluginhelper/api/ds_connection_api_helper.go
@@ -33,12 +33,16 @@ type DsConnectionApiHelper[C plugin.ToolLayerConnection, S 
plugin.ToolLayerScope
        *srvhelper.ConnectionSrvHelper[C, S, SC]
 }
 
-func NewDsConnectionApiHelper[C plugin.ToolLayerConnection, S 
plugin.ToolLayerScope, SC plugin.ToolLayerScopeConfig](
+func NewDsConnectionApiHelper[
+       C plugin.ToolLayerConnection,
+       S plugin.ToolLayerScope,
+       SC plugin.ToolLayerScopeConfig](
        basicRes context.BasicRes,
        connSrvHelper *srvhelper.ConnectionSrvHelper[C, S, SC],
+       sterilizer func(c C) C,
 ) *DsConnectionApiHelper[C, S, SC] {
        return &DsConnectionApiHelper[C, S, SC]{
-               ModelApiHelper:      NewModelApiHelper[C](basicRes, 
connSrvHelper.ModelSrvHelper, []string{"connectionId"}),
+               ModelApiHelper:      NewModelApiHelper[C](basicRes, 
connSrvHelper.ModelSrvHelper, []string{"connectionId"}, sterilizer),
                ConnectionSrvHelper: connSrvHelper,
        }
 }
@@ -57,6 +61,7 @@ func (connApi *DsConnectionApiHelper[C, S, SC]) Delete(input 
*plugin.ApiResource
                        Data:    refs,
                }, Status: err.GetType().GetHttpCode()}, nil
        }
+       conn = connApi.Sanitize(conn)
        return &plugin.ApiResourceOutput{
                Body: conn,
        }, nil
diff --git a/backend/helpers/pluginhelper/api/ds_helper.go 
b/backend/helpers/pluginhelper/api/ds_helper.go
index 303789fdd..e4e823a86 100644
--- a/backend/helpers/pluginhelper/api/ds_helper.go
+++ b/backend/helpers/pluginhelper/api/ds_helper.go
@@ -44,13 +44,16 @@ func NewDataSourceHelper[
        basicRes context.BasicRes,
        pluginName string,
        scopeSearchColumns []string,
+       connectionSterilizer func(c C) C,
+       scopeSterilizer func(s S) S,
+       scopeConfigSterilizer func(s SC) SC,
 ) *DsHelper[C, S, SC] {
        connSrv := srvhelper.NewConnectionSrvHelper[C, S, SC](basicRes, 
pluginName)
-       connApi := NewDsConnectionApiHelper[C, S, SC](basicRes, connSrv)
+       connApi := NewDsConnectionApiHelper[C, S, SC](basicRes, connSrv, 
connectionSterilizer)
        scopeSrv := srvhelper.NewScopeSrvHelper[C, S, SC](basicRes, pluginName, 
scopeSearchColumns)
-       scopeApi := NewDsScopeApiHelper[C, S, SC](basicRes, scopeSrv)
+       scopeApi := NewDsScopeApiHelper[C, S, SC](basicRes, scopeSrv, 
scopeSterilizer)
        scSrv := srvhelper.NewScopeConfigSrvHelper[C, S, SC](basicRes, 
scopeSearchColumns)
-       scApi := NewDsScopeConfigApiHelper[C, S, SC](basicRes, scSrv)
+       scApi := NewDsScopeConfigApiHelper[C, S, SC](basicRes, scSrv, 
scopeConfigSterilizer)
        return &DsHelper[C, S, SC]{
                ConnSrv:        connSrv,
                ConnApi:        connApi,
diff --git a/backend/helpers/pluginhelper/api/ds_scope_api_helper.go 
b/backend/helpers/pluginhelper/api/ds_scope_api_helper.go
index 279d9b1ae..cdf220694 100644
--- a/backend/helpers/pluginhelper/api/ds_scope_api_helper.go
+++ b/backend/helpers/pluginhelper/api/ds_scope_api_helper.go
@@ -36,12 +36,16 @@ type DsScopeApiHelper[C plugin.ToolLayerConnection, S 
plugin.ToolLayerScope, SC
        *srvhelper.ScopeSrvHelper[C, S, SC]
 }
 
-func NewDsScopeApiHelper[C plugin.ToolLayerConnection, S 
plugin.ToolLayerScope, SC plugin.ToolLayerScopeConfig](
+func NewDsScopeApiHelper[
+       C plugin.ToolLayerConnection,
+       S plugin.ToolLayerScope,
+       SC plugin.ToolLayerScopeConfig](
        basicRes context.BasicRes,
        srvHelper *srvhelper.ScopeSrvHelper[C, S, SC],
+       sterilizer func(s S) S,
 ) *DsScopeApiHelper[C, S, SC] {
        return &DsScopeApiHelper[C, S, SC]{
-               ModelApiHelper: NewModelApiHelper[S](basicRes, 
srvHelper.ModelSrvHelper, []string{"connectionId", "scopeId"}),
+               ModelApiHelper: NewModelApiHelper[S](basicRes, 
srvHelper.ModelSrvHelper, []string{"connectionId", "scopeId"}, sterilizer),
                ScopeSrvHelper: srvHelper,
        }
 }
diff --git a/backend/helpers/pluginhelper/api/ds_scope_config_api_helper.go 
b/backend/helpers/pluginhelper/api/ds_scope_config_api_helper.go
index 6a3e7cd19..8bfc52306 100644
--- a/backend/helpers/pluginhelper/api/ds_scope_config_api_helper.go
+++ b/backend/helpers/pluginhelper/api/ds_scope_config_api_helper.go
@@ -31,12 +31,17 @@ type DsScopeConfigApiHelper[C plugin.ToolLayerConnection, S 
plugin.ToolLayerScop
        *srvhelper.ScopeConfigSrvHelper[C, S, SC]
 }
 
-func NewDsScopeConfigApiHelper[C plugin.ToolLayerConnection, S 
plugin.ToolLayerScope, SC plugin.ToolLayerScopeConfig](
+func NewDsScopeConfigApiHelper[
+       C plugin.ToolLayerConnection,
+       S plugin.ToolLayerScope,
+       SC plugin.ToolLayerScopeConfig,
+](
        basicRes context.BasicRes,
        dalHelper *srvhelper.ScopeConfigSrvHelper[C, S, SC],
+       sterilizer func(sc SC) SC,
 ) *DsScopeConfigApiHelper[C, S, SC] {
        return &DsScopeConfigApiHelper[C, S, SC]{
-               ModelApiHelper:       NewModelApiHelper[SC](basicRes, 
dalHelper.ModelSrvHelper, []string{"scopeConfigId"}),
+               ModelApiHelper:       NewModelApiHelper[SC](basicRes, 
dalHelper.ModelSrvHelper, []string{"scopeConfigId"}, sterilizer),
                ScopeConfigSrvHelper: dalHelper,
        }
 }
diff --git a/backend/helpers/pluginhelper/api/model_api_helper.go 
b/backend/helpers/pluginhelper/api/model_api_helper.go
index ce4d18be5..af4905eb6 100644
--- a/backend/helpers/pluginhelper/api/model_api_helper.go
+++ b/backend/helpers/pluginhelper/api/model_api_helper.go
@@ -39,22 +39,28 @@ type ModelApiHelper[M dal.Tabler] struct {
        log            log.Logger
        modelName      string
        pkPathVarNames []string
+       sterilizers    []func(m M) M
 }
 
 func NewModelApiHelper[M dal.Tabler](
        basicRes context.BasicRes,
        dalHelper *srvhelper.ModelSrvHelper[M],
        pkPathVarNames []string, // path variable names of primary key
+       sterilizer func(m M) M,
 ) *ModelApiHelper[M] {
        m := new(M)
        modelName := fmt.Sprintf("%T", m)
-       return &ModelApiHelper[M]{
+       modelApiHelper := &ModelApiHelper[M]{
                basicRes:       basicRes,
                dalHelper:      dalHelper,
                log:            
basicRes.GetLogger().Nested(fmt.Sprintf("%s_dal", modelName)),
                modelName:      modelName,
                pkPathVarNames: pkPathVarNames,
        }
+       if sterilizer != nil {
+               modelApiHelper.sterilizers = []func(m M) M{sterilizer}
+       }
+       return modelApiHelper
 }
 
 func (self *ModelApiHelper[M]) Post(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
@@ -98,11 +104,30 @@ func (self *ModelApiHelper[M]) GetDetail(input 
*plugin.ApiResourceInput) (*plugi
        if err != nil {
                return nil, err
        }
+       model = self.Sanitize(model)
        return &plugin.ApiResourceOutput{
                Body: model,
        }, nil
 }
 
+func (self *ModelApiHelper[M]) Sanitize(model *M) *M {
+       if self.sterilizers != nil {
+               for _, sterilizer := range self.sterilizers {
+                       sanitizedModel := sterilizer(*model)
+                       model = &sanitizedModel
+               }
+       }
+       return model
+}
+
+func (self *ModelApiHelper[M]) BatchSanitize(models []*M) []*M {
+       for idx, m := range models {
+               model := *m
+               models[idx] = self.Sanitize(&model)
+       }
+       return models
+}
+
 func (self *ModelApiHelper[M]) Patch(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
        model, err := self.FindByPk(input)
        if err != nil {
@@ -116,6 +141,7 @@ func (self *ModelApiHelper[M]) Patch(input 
*plugin.ApiResourceInput) (*plugin.Ap
        if err != nil {
                return nil, err
        }
+       model = self.Sanitize(model)
        return &plugin.ApiResourceOutput{
                Body: model,
        }, nil
@@ -130,6 +156,7 @@ func (self *ModelApiHelper[M]) Delete(input 
*plugin.ApiResourceInput) (*plugin.A
        if err != nil {
                return nil, err
        }
+       model = self.Sanitize(model)
        return &plugin.ApiResourceOutput{
                Body: model,
        }, nil
@@ -137,6 +164,7 @@ func (self *ModelApiHelper[M]) Delete(input 
*plugin.ApiResourceInput) (*plugin.A
 
 func (self *ModelApiHelper[M]) GetAll(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
        all, err := self.dalHelper.GetAll()
+       all = self.BatchSanitize(all)
        return &plugin.ApiResourceOutput{
                Body: all,
        }, err
@@ -144,18 +172,19 @@ func (self *ModelApiHelper[M]) GetAll(input 
*plugin.ApiResourceInput) (*plugin.A
 
 func (self *ModelApiHelper[M]) PutMultiple(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
        var req struct {
-               Data []M `json:"data"`
+               Data []*M `json:"data"`
        }
        err := utils.DecodeMapStruct(input.Body, &req, false)
        if err != nil {
                return nil, err
        }
        for i, item := range req.Data {
-               err := self.dalHelper.CreateOrUpdate(&item)
+               err := self.dalHelper.CreateOrUpdate(item)
                if err != nil {
                        return nil, errors.BadInput.Wrap(err, 
fmt.Sprintf("failed to save item %d", i))
                }
        }
+       req.Data = self.BatchSanitize(req.Data)
        return &plugin.ApiResourceOutput{
                Body: req.Data,
        }, nil
diff --git a/backend/plugins/ae/api/connection.go 
b/backend/plugins/ae/api/connection.go
index 20287e0ed..f2d8b8bdd 100644
--- a/backend/plugins/ae/api/connection.go
+++ b/backend/plugins/ae/api/connection.go
@@ -32,6 +32,32 @@ type ApiMeResponse struct {
        Name string `json:"name"`
 }
 
+func testConnection(ctx context.Context, connection models.AeConn) 
(*plugin.ApiResourceOutput, errors.Error) {
+       // validate
+       if vld != nil {
+               if err := vld.Struct(connection); err != nil {
+                       return nil, errors.Default.Wrap(err, "error validating 
target")
+               }
+       }
+       apiClient, err := api.NewApiClientFromConnection(ctx, basicRes, 
&connection)
+       if err != nil {
+               return nil, err
+       }
+       res, err := apiClient.Get("projects", nil, nil)
+       if err != nil {
+               return nil, err
+       }
+       switch res.StatusCode {
+       case 200: // right StatusCode
+               return &plugin.ApiResourceOutput{Body: true, Status: 200}, nil
+       case 401: // error secretKey or nonceStr
+               return &plugin.ApiResourceOutput{Body: false, Status: 
http.StatusBadRequest}, nil
+       default: // unknow what happen , back to user
+               return &plugin.ApiResourceOutput{Body: res.Body, Status: 
res.StatusCode}, nil
+       }
+}
+
+// TestConnection test ae connection
 // @Summary test ae connection
 // @Description Test AE Connection
 // @Tags plugins/ae
@@ -47,23 +73,25 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err = api.Decode(input.Body, &connection, vld); err != nil {
                return nil, errors.BadInput.Wrap(err, "could not decode request 
parameters")
        }
+       return testConnection(context.TODO(), connection)
+}
 
-       apiClient, err := api.NewApiClientFromConnection(context.TODO(), 
basicRes, &connection)
-       if err != nil {
-               return nil, err
-       }
-       res, err := apiClient.Get("projects", nil, nil)
+// TestExistingConnection test ae connection
+// @Summary test ae connection
+// @Description Test AE Connection
+// @Tags plugins/ae
+// @Success 200  {object} shared.ApiBody "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/ae/{connectionId}/test [POST]
+func TestExistingConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       // decode
+       connection := &models.AeConnection{}
+       err := connectionHelper.First(connection, input.Params)
        if err != nil {
-               return nil, err
-       }
-       switch res.StatusCode {
-       case 200: // right StatusCode
-               return &plugin.ApiResourceOutput{Body: true, Status: 200}, nil
-       case 401: // error secretKey or nonceStr
-               return &plugin.ApiResourceOutput{Body: false, Status: 
http.StatusBadRequest}, nil
-       default: // unknow what happen , back to user
-               return &plugin.ApiResourceOutput{Body: res.Body, Status: 
res.StatusCode}, nil
+               return nil, errors.BadInput.Wrap(err, "find connection from db")
        }
+       return testConnection(context.TODO(), connection.AeConn)
 }
 
 // @Summary create ae connection
@@ -80,7 +108,7 @@ func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: 
http.StatusOK}, nil
 }
 
 // @Summary get all ae connections
@@ -96,6 +124,9 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
+       for idx, c := range connections {
+               connections[idx] = c.Sanitize()
+       }
        return &plugin.ApiResourceOutput{Body: connections, Status: 
http.StatusOK}, nil
 }
 
@@ -109,7 +140,7 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
        connection := &models.AeConnection{}
        err := connectionHelper.First(connection, input.Params)
-       return &plugin.ApiResourceOutput{Body: connection}, err
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, err
 }
 
 // @Summary patch ae connection
@@ -126,7 +157,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: 
http.StatusOK}, nil
 }
 
 // @Summary delete a ae connection
@@ -138,5 +169,12 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/ae/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       return connectionHelper.Delete(&models.AeConnection{}, input)
+       conn := &models.AeConnection{}
+       output, err := connectionHelper.Delete(conn, input)
+       if err != nil {
+               return output, err
+       }
+       output.Body = conn.Sanitize()
+       return output, nil
+
 }
diff --git a/backend/plugins/ae/impl/impl.go b/backend/plugins/ae/impl/impl.go
index db56e84ed..907975e48 100644
--- a/backend/plugins/ae/impl/impl.go
+++ b/backend/plugins/ae/impl/impl.go
@@ -139,6 +139,9 @@ func (p AE) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler {
                        "PATCH":  api.PatchConnection,
                        "DELETE": api.DeleteConnection,
                },
+               "connections/:connectionId/test": {
+                       "POST": api.TestExistingConnection,
+               },
        }
 }
 
diff --git a/backend/plugins/ae/models/connection.go 
b/backend/plugins/ae/models/connection.go
index 7196d4fee..d13767c7b 100644
--- a/backend/plugins/ae/models/connection.go
+++ b/backend/plugins/ae/models/connection.go
@@ -65,6 +65,11 @@ func (AeConnection) TableName() string {
        return "_tool_ae_connections"
 }
 
+func (connection AeConnection) Sanitize() AeConnection {
+       connection.AeAppKey.SecretKey = ""
+       return connection
+}
+
 func signRequest(query url.Values, appId, secretKey, nonceStr, timestamp 
string) string {
        // clone query because we need to add items
        kvs := make([]string, 0, len(query)+3)
diff --git a/backend/plugins/bamboo/api/connection.go 
b/backend/plugins/bamboo/api/connection.go
index 91df4cefe..96a0d8923 100644
--- a/backend/plugins/bamboo/api/connection.go
+++ b/backend/plugins/bamboo/api/connection.go
@@ -33,6 +33,27 @@ type BambooTestConnResponse struct {
        Connection *models.BambooConn
 }
 
+func testConnection(ctx context.Context, connection models.BambooConn) 
(*BambooTestConnResponse, errors.Error) {
+       // validate
+       if vld != nil {
+               if err := vld.Struct(connection); err != nil {
+                       return nil, errors.Default.Wrap(err, "error validating 
target")
+               }
+       }
+       // test connection
+       _, err := api.NewApiClientFromConnection(ctx, basicRes, &connection)
+       connection = connection.Sanitize()
+       if err != nil {
+               return nil, err
+       }
+       body := BambooTestConnResponse{}
+       body.Success = true
+       body.Message = "success"
+       body.Connection = &connection
+       return &body, nil
+}
+
+// TestConnection test bamboo connection
 // @Summary test bamboo connection
 // @Description Test bamboo Connection
 // @Tags plugins/bamboo
@@ -48,23 +69,34 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err = api.Decode(input.Body, &connection, vld); err != nil {
                return nil, err
        }
-
        // test connection
-       _, err = api.NewApiClientFromConnection(
-               context.TODO(),
-               basicRes,
-               &connection,
-       )
+       result, err := testConnection(context.TODO(), connection)
        if err != nil {
                return nil, err
        }
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
+}
 
-       body := BambooTestConnResponse{}
-       body.Success = true
-       body.Message = "success"
-       body.Connection = &connection
-
-       return &plugin.ApiResourceOutput{Body: body, Status: http.StatusOK}, nil
+// TestExistingConnection test bamboo connection
+// @Summary test bamboo connection
+// @Description Test bamboo Connection
+// @Tags plugins/bamboo
+// @Success 200  {object} BambooTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/bamboo/{connectionId}/test [POST]
+func TestExistingConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connection := &models.BambooConnection{}
+       err := connectionHelper.First(connection, input.Params)
+       if err != nil {
+               return nil, errors.BadInput.Wrap(err, "find connection from db")
+       }
+       // test connection
+       result, err := testConnection(context.TODO(), connection.BambooConn)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
 }
 
 // @Summary create bamboo connection
@@ -82,7 +114,7 @@ func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: 
http.StatusOK}, nil
 }
 
 // @Summary patch bamboo connection
@@ -100,7 +132,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
 }
 
 // @Summary delete a bamboo connection
@@ -113,7 +145,13 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internel Error"
 // @Router /plugins/bamboo/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       return connectionHelper.Delete(&models.BambooConnection{}, input)
+       conn := &models.BambooConnection{}
+       output, err := connectionHelper.Delete(conn, input)
+       if err != nil {
+               return output, err
+       }
+       output.Body = conn.Sanitize()
+       return output, nil
 }
 
 // @Summary get all bamboo connections
@@ -129,6 +167,9 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
+       for idx, c := range connections {
+               connections[idx] = c.Sanitize()
+       }
        return &plugin.ApiResourceOutput{Body: connections, Status: 
http.StatusOK}, nil
 }
 
@@ -143,5 +184,5 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
        connection := &models.BambooConnection{}
        err := connectionHelper.First(connection, input.Params)
-       return &plugin.ApiResourceOutput{Body: connection}, err
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, err
 }
diff --git a/backend/plugins/bamboo/impl/impl.go 
b/backend/plugins/bamboo/impl/impl.go
index 05965c456..23543fb91 100644
--- a/backend/plugins/bamboo/impl/impl.go
+++ b/backend/plugins/bamboo/impl/impl.go
@@ -215,6 +215,9 @@ func (p Bamboo) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler {
                        "PATCH":  api.PatchConnection,
                        "DELETE": api.DeleteConnection,
                },
+               "connections/:connectionId/test": {
+                       "POST": api.TestExistingConnection,
+               },
                "connections/:connectionId/scope-configs": {
                        "POST": api.CreateScopeConfig,
                        "GET":  api.GetScopeConfigList,
diff --git a/backend/plugins/bamboo/models/connection.go 
b/backend/plugins/bamboo/models/connection.go
index fa0fceae1..1feeb5cc1 100644
--- a/backend/plugins/bamboo/models/connection.go
+++ b/backend/plugins/bamboo/models/connection.go
@@ -36,6 +36,11 @@ type BambooConnection struct {
        BambooConn         `mapstructure:",squash"`
 }
 
+func (connection BambooConnection) Sanitize() BambooConnection {
+       connection.BambooConn = connection.BambooConn.Sanitize()
+       return connection
+}
+
 // TODO Please modify the following code to fit your needs
 // This object conforms to what the frontend currently sends.
 type BambooConn struct {
@@ -44,6 +49,11 @@ type BambooConn struct {
        api.BasicAuth `mapstructure:",squash"`
 }
 
+func (conn *BambooConn) Sanitize() BambooConn {
+       conn.Password = ""
+       return *conn
+}
+
 // PrepareApiClient test api and set the IsPrivateToken,version,UserId and so 
on.
 func (conn *BambooConn) PrepareApiClient(apiClient 
apihelperabstract.ApiClientAbstract) errors.Error {
        header := http.Header{}
diff --git a/backend/plugins/bamboo/tasks/plan_build_extractor.go 
b/backend/plugins/bamboo/tasks/plan_build_extractor.go
index c03d8a7b3..273e61764 100644
--- a/backend/plugins/bamboo/tasks/plan_build_extractor.go
+++ b/backend/plugins/bamboo/tasks/plan_build_extractor.go
@@ -50,7 +50,7 @@ func ExtractPlanBuild(taskCtx plugin.SubTaskContext) 
errors.Error {
                        results := make([]interface{}, 0)
                        results = append(results, body)
                        // As job build can get more accuracy repo info,
-                       // we can collect BambooPlanBuildVcsRevision in 
job_biuld_extractor
+                       // we can collect BambooPlanBuildVcsRevision in 
job_build_extractor
                        for _, v := range res.VcsRevisions.VcsRevision {
                                results = append(results, 
&models.BambooPlanBuildVcsRevision{
                                        ConnectionId:   
data.Options.ConnectionId,
diff --git a/backend/plugins/bitbucket/api/connection.go 
b/backend/plugins/bitbucket/api/connection.go
index 4d88ba7c2..a3e637458 100644
--- a/backend/plugins/bitbucket/api/connection.go
+++ b/backend/plugins/bitbucket/api/connection.go
@@ -34,23 +34,15 @@ type BitBucketTestConnResponse struct {
        Connection *models.BitbucketConn
 }
 
-// @Summary test bitbucket connection
-// @Description Test bitbucket Connection
-// @Tags plugins/bitbucket
-// @Param body body models.BitbucketConn true "json body"
-// @Success 200  {object} BitBucketTestConnResponse "Success"
-// @Failure 400  {string} errcode.Error "Bad Request"
-// @Failure 500  {string} errcode.Error "Internal Error"
-// @Router /plugins/bitbucket/test [POST]
-func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       // decode
-       var err errors.Error
-       var connection models.BitbucketConn
-       if err := api.Decode(input.Body, &connection, vld); err != nil {
-               return nil, errors.BadInput.Wrap(err, "could not decode request 
parameters")
+func testConnection(ctx context.Context, connection models.BitbucketConn) 
(*BitBucketTestConnResponse, errors.Error) {
+       // validate
+       if vld != nil {
+               if err := vld.Struct(connection); err != nil {
+                       return nil, errors.Default.Wrap(err, "error validating 
target")
+               }
        }
        // test connection
-       apiClient, err := api.NewApiClientFromConnection(context.TODO(), 
basicRes, &connection)
+       apiClient, err := api.NewApiClientFromConnection(ctx, basicRes, 
&connection)
        if err != nil {
                return nil, err
        }
@@ -66,12 +58,59 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if res.StatusCode != http.StatusOK {
                return nil, errors.HttpStatus(res.StatusCode).New("unexpected 
status code when testing connection")
        }
+       connection = connection.Sanitize()
        body := BitBucketTestConnResponse{}
        body.Success = true
        body.Message = "success"
        body.Connection = &connection
        // output
-       return &plugin.ApiResourceOutput{Body: body, Status: 200}, nil
+       return &body, nil
+}
+
+// TestConnection test bitbucket connection
+// @Summary test bitbucket connection
+// @Description Test bitbucket Connection
+// @Tags plugins/bitbucket
+// @Param body body models.BitbucketConn true "json body"
+// @Success 200  {object} BitBucketTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/bitbucket/test [POST]
+func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       // decode
+       var err errors.Error
+       var connection models.BitbucketConn
+       if err := api.Decode(input.Body, &connection, vld); err != nil {
+               return nil, errors.BadInput.Wrap(err, "could not decode request 
parameters")
+       }
+       // test connection
+       result, err := testConnection(context.TODO(), connection)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
+}
+
+// TestExistingConnection test bitbucket connection
+// @Summary test bitbucket connection
+// @Description Test bitbucket Connection
+// @Tags plugins/bitbucket
+// @Success 200  {object} BitBucketTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/bitbucket/{connectionId}/test [POST]
+func TestExistingConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connection := &models.BitbucketConnection{}
+       err := connectionHelper.First(connection, input.Params)
+       if err != nil {
+               return nil, errors.BadInput.Wrap(err, "find connection from db")
+       }
+       // test connection
+       result, err := testConnection(context.TODO(), connection.BitbucketConn)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
 }
 
 // @Summary create bitbucket connection
@@ -89,7 +128,7 @@ func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: 
http.StatusOK}, nil
 }
 
 // @Summary patch bitbucket connection
@@ -106,7 +145,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
 }
 
 // @Summary delete a bitbucket connection
@@ -118,7 +157,14 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/bitbucket/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       return connectionHelper.Delete(&models.BitbucketConnection{}, input)
+       conn := &models.BitbucketConnection{}
+       output, err := connectionHelper.Delete(conn, input)
+       if err != nil {
+               return output, err
+       }
+       output.Body = conn.Sanitize()
+       return output, nil
+
 }
 
 // @Summary get all bitbucket connections
@@ -134,6 +180,9 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
+       for idx, c := range connections {
+               connections[idx] = c.Sanitize()
+       }
        return &plugin.ApiResourceOutput{Body: connections, Status: 
http.StatusOK}, nil
 }
 
@@ -147,5 +196,5 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
        connection := &models.BitbucketConnection{}
        err := connectionHelper.First(connection, input.Params)
-       return &plugin.ApiResourceOutput{Body: connection}, err
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, err
 }
diff --git a/backend/plugins/bitbucket/impl/impl.go 
b/backend/plugins/bitbucket/impl/impl.go
index 8611b1287..f79d82167 100644
--- a/backend/plugins/bitbucket/impl/impl.go
+++ b/backend/plugins/bitbucket/impl/impl.go
@@ -207,6 +207,9 @@ func (p Bitbucket) ApiResources() 
map[string]map[string]plugin.ApiResourceHandle
                        "DELETE": api.DeleteConnection,
                        "GET":    api.GetConnection,
                },
+               "connections/:connectionId/test": {
+                       "POST": api.TestExistingConnection,
+               },
                "connections/:connectionId/scopes/*scopeId": {
                        "GET":    api.GetScope,
                        "PATCH":  api.UpdateScope,
diff --git a/backend/plugins/bitbucket/models/connection.go 
b/backend/plugins/bitbucket/models/connection.go
index 9db3befc5..3e187a39e 100644
--- a/backend/plugins/bitbucket/models/connection.go
+++ b/backend/plugins/bitbucket/models/connection.go
@@ -30,6 +30,11 @@ type BitbucketConn struct {
        api.BasicAuth      `mapstructure:",squash"`
 }
 
+func (connection BitbucketConn) Sanitize() BitbucketConn {
+       connection.Password = ""
+       return connection
+}
+
 // BitbucketConnection holds BitbucketConn plus ID/Name for database storage
 type BitbucketConnection struct {
        api.BaseConnection `mapstructure:",squash"`
@@ -39,3 +44,8 @@ type BitbucketConnection struct {
 func (BitbucketConnection) TableName() string {
        return "_tool_bitbucket_connections"
 }
+
+func (connection BitbucketConnection) Sanitize() BitbucketConnection {
+       connection.BitbucketConn = connection.BitbucketConn.Sanitize()
+       return connection
+}
diff --git a/backend/plugins/circleci/api/connection.go 
b/backend/plugins/circleci/api/connection.go
index 1f851cccc..84f7f78d2 100644
--- a/backend/plugins/circleci/api/connection.go
+++ b/backend/plugins/circleci/api/connection.go
@@ -33,7 +33,37 @@ type CircleciTestConnResponse struct {
        shared.ApiBody
 }
 
-// TestConnection @Summary test circleci connection
+func testConnection(ctx context.Context, connection models.CircleciConn) 
(*CircleciTestConnResponse, errors.Error) {
+       // validate
+       if vld != nil {
+               if err := vld.Struct(connection); err != nil {
+                       return nil, errors.Default.Wrap(err, "error validating 
target")
+               }
+       }
+       // test connection
+       apiClient, err := api.NewApiClientFromConnection(ctx, basicRes, 
&connection)
+       if err != nil {
+               return nil, err
+       }
+
+       res, err := apiClient.Get("/v2/me", nil, nil)
+       if err != nil {
+               return nil, err
+       }
+
+       if res.StatusCode != http.StatusOK {
+               return nil, 
errors.HttpStatus(res.StatusCode).New(fmt.Sprintf("unexpected status code: %d", 
res.StatusCode))
+       }
+
+       body := CircleciTestConnResponse{}
+       body.Success = true
+       body.Message = "success"
+       // output
+       return &body, nil
+}
+
+// TestConnection test circleci connection
+// @Summary test circleci connection
 // @Description Test circleci Connection
 // @Tags plugins/circleci
 // @Param body body models.CircleciConnection true "json body"
@@ -48,27 +78,34 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-
        // test connection
-       apiClient, err := api.NewApiClientFromConnection(context.TODO(), 
basicRes, &connection)
+       result, err := testConnection(context.TODO(), connection)
        if err != nil {
                return nil, err
        }
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
+}
 
-       res, err := apiClient.Get("/v2/me", nil, nil)
+// TestExistingConnection test circleci connection
+// @Summary test circleci connection
+// @Description Test circleci Connection
+// @Tags plugins/circleci
+// @Success 200  {object} CircleciTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/circleci/{connectionId}/test [POST]
+func TestExistingConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connection := &models.CircleciConnection{}
+       err := connectionHelper.First(connection, input.Params)
        if err != nil {
-               return nil, err
+               return nil, errors.BadInput.Wrap(err, "find connection from db")
        }
-
-       if res.StatusCode != http.StatusOK {
-               return nil, 
errors.HttpStatus(res.StatusCode).New(fmt.Sprintf("unexpected status code: %d", 
res.StatusCode))
+       // test connection
+       result, err := testConnection(context.TODO(), connection.CircleciConn)
+       if err != nil {
+               return nil, err
        }
-
-       body := CircleciTestConnResponse{}
-       body.Success = true
-       body.Message = "success"
-       // output
-       return &plugin.ApiResourceOutput{Body: body, Status: 200}, nil
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
 }
 
 // PostConnections @Summary create circleci connection
@@ -86,7 +123,7 @@ func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: 
http.StatusOK}, nil
 }
 
 // PatchConnection @Summary patch circleci connection
@@ -103,7 +140,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
 }
 
 // DeleteConnection @Summary delete a circleci connection
@@ -114,7 +151,14 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/circleci/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       return connectionHelper.Delete(&models.CircleciConnection{}, input)
+       conn := &models.CircleciConnection{}
+       output, err := connectionHelper.Delete(conn, input)
+       if err != nil {
+               return output, err
+       }
+       output.Body = conn.Sanitize()
+       return output, nil
+
 }
 
 // ListConnections @Summary get all circleci connections
@@ -130,6 +174,9 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
+       for idx, c := range connections {
+               connections[idx] = c.Sanitize()
+       }
        return &plugin.ApiResourceOutput{Body: connections, Status: 
http.StatusOK}, nil
 }
 
@@ -143,5 +190,5 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
        connection := &models.CircleciConnection{}
        err := connectionHelper.First(connection, input.Params)
-       return &plugin.ApiResourceOutput{Body: connection}, err
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, err
 }
diff --git a/backend/plugins/circleci/impl/impl.go 
b/backend/plugins/circleci/impl/impl.go
index 19e73ca36..1025ea4cd 100644
--- a/backend/plugins/circleci/impl/impl.go
+++ b/backend/plugins/circleci/impl/impl.go
@@ -180,6 +180,9 @@ func (p Circleci) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler
                        "PATCH":  api.PatchConnection,
                        "DELETE": api.DeleteConnection,
                },
+               "connections/:connectionId/test": {
+                       "POST": api.TestExistingConnection,
+               },
                "connections/:connectionId/remote-scopes": {
                        "GET": api.RemoteScopes,
                },
diff --git a/backend/plugins/circleci/models/connection.go 
b/backend/plugins/circleci/models/connection.go
index b1ae0544a..60d661581 100644
--- a/backend/plugins/circleci/models/connection.go
+++ b/backend/plugins/circleci/models/connection.go
@@ -43,3 +43,8 @@ func (cc *CircleciConn) SetupAuthentication(req 
*http.Request) errors.Error {
 func (CircleciConnection) TableName() string {
        return "_tool_circleci_connections"
 }
+
+func (connection CircleciConnection) Sanitize() CircleciConnection {
+       connection.Token = ""
+       return connection
+}
diff --git a/backend/plugins/feishu/api/connection.go 
b/backend/plugins/feishu/api/connection.go
index 141b14c5b..35cd1d9e8 100644
--- a/backend/plugins/feishu/api/connection.go
+++ b/backend/plugins/feishu/api/connection.go
@@ -34,6 +34,27 @@ type FeishuTestConnResponse struct {
        Connection *models.FeishuConn
 }
 
+func testConnection(ctx context.Context, connection models.FeishuConn) 
(*FeishuTestConnResponse, errors.Error) {
+       // validate
+       if vld != nil {
+               if err := vld.Struct(connection); err != nil {
+                       return nil, errors.Default.Wrap(err, "error validating 
target")
+               }
+       }
+       _, err := api.NewApiClientFromConnection(ctx, basicRes, &connection)
+       if err != nil {
+               return nil, err
+       }
+       connection = connection.Sanitize()
+       body := FeishuTestConnResponse{}
+       body.Success = true
+       body.Message = "success"
+       body.Connection = &connection
+
+       return &body, nil
+}
+
+// TestConnection test feishu connection
 // @Summary test feishu connection
 // @Description Test feishu Connection. endpoint: 
https://open.feishu.cn/open-apis/
 // @Tags plugins/feishu
@@ -48,18 +69,34 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err := api.Decode(input.Body, &connection, vld); err != nil {
                return nil, errors.BadInput.Wrap(err, "could not decode request 
parameters")
        }
-
        // test connection
-       _, err := api.NewApiClientFromConnection(context.TODO(), basicRes, 
&connection)
+       result, err := testConnection(context.TODO(), connection)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
+}
 
-       body := FeishuTestConnResponse{}
-       body.Success = true
-       body.Message = "success"
-       body.Connection = &connection
+// TestExistingConnection test feishu connection
+// @Summary test feishu connection
+// @Description Test feishu Connection. endpoint: 
https://open.feishu.cn/open-apis/
+// @Tags plugins/feishu
+// @Success 200  {object} FeishuTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/feishu/{connectionId}/test [POST]
+func TestExistingConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connection := &models.FeishuConnection{}
+       err := connectionHelper.First(connection, input.Params)
+       if err != nil {
+               return nil, errors.BadInput.Wrap(err, "find connection from db")
+       }
+       // test connection
+       result, err := testConnection(context.TODO(), connection.FeishuConn)
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: body, Status: 200}, nil
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
 }
 
 // @Summary create feishu connection
@@ -76,7 +113,7 @@ func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: 
http.StatusOK}, nil
 }
 
 // @Summary patch feishu connection
@@ -93,7 +130,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: 
http.StatusOK}, nil
 }
 
 // @Summary delete a feishu connection
@@ -105,7 +142,14 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/feishu/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       return connectionHelper.Delete(&models.FeishuConnection{}, input)
+       conn := &models.FeishuConnection{}
+       output, err := connectionHelper.Delete(conn, input)
+       if err != nil {
+               return output, err
+       }
+       output.Body = conn.Sanitize()
+       return output, nil
+
 }
 
 // @Summary get all feishu connections
@@ -121,7 +165,9 @@ func ListConnections(_ *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, err
        if err != nil {
                return nil, err
        }
-
+       for idx, c := range connections {
+               connections[idx] = c.Sanitize()
+       }
        return &plugin.ApiResourceOutput{Body: connections}, nil
 }
 
@@ -138,5 +184,5 @@ func GetConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, e
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection}, err
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, err
 }
diff --git a/backend/plugins/feishu/impl/impl.go 
b/backend/plugins/feishu/impl/impl.go
index 8429fc883..eb321a12b 100644
--- a/backend/plugins/feishu/impl/impl.go
+++ b/backend/plugins/feishu/impl/impl.go
@@ -141,6 +141,9 @@ func (p Feishu) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler {
                        "DELETE": api.DeleteConnection,
                        "GET":    api.GetConnection,
                },
+               "connections/:connectionId/test": {
+                       "POST": api.TestExistingConnection,
+               },
        }
 }
 
diff --git a/backend/plugins/feishu/models/connection.go 
b/backend/plugins/feishu/models/connection.go
index 2ad8988bc..9e0b26644 100644
--- a/backend/plugins/feishu/models/connection.go
+++ b/backend/plugins/feishu/models/connection.go
@@ -33,6 +33,11 @@ type FeishuConn struct {
        helper.AppKey         `mapstructure:",squash"`
 }
 
+func (conn *FeishuConn) Sanitize() FeishuConn {
+       conn.SecretKey = ""
+       return *conn
+}
+
 func (conn *FeishuConn) PrepareApiClient(apiClient 
apihelperabstract.ApiClientAbstract) errors.Error {
        // request for access token
        tokenReqBody := &apimodels.ApiAccessTokenRequest{
@@ -71,3 +76,8 @@ type FeishuConnection struct {
 func (FeishuConnection) TableName() string {
        return "_tool_feishu_connections"
 }
+
+func (connection FeishuConnection) Sanitize() FeishuConnection {
+       connection.FeishuConn = connection.FeishuConn.Sanitize()
+       return connection
+}
diff --git a/backend/plugins/gitee/api/connection.go 
b/backend/plugins/gitee/api/connection.go
index db926bce8..f946df928 100644
--- a/backend/plugins/gitee/api/connection.go
+++ b/backend/plugins/gitee/api/connection.go
@@ -34,22 +34,14 @@ type GiteeTestConnResponse struct {
        Connection *models.GiteeConn
 }
 
-// @Summary test gitee connection
-// @Description Test gitee Connection. endpoint: https://gitee.com/api/v5/
-// @Tags plugins/gitee
-// @Param body body models.GiteeConn true "json body"
-// @Success 200  {object} GiteeTestConnResponse "Success"
-// @Failure 400  {string} errcode.Error "Bad Request"
-// @Failure 500  {string} errcode.Error "Internal Error"
-// @Router /plugins/gitee/test [POST]
-func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       var err errors.Error
-       var connection models.GiteeConn
-       if err = helper.Decode(input.Body, &connection, vld); err != nil {
-               return nil, errors.BadInput.Wrap(err, "could not decode request 
parameters")
+func testConnection(ctx context.Context, connection models.GiteeConn) 
(*GiteeTestConnResponse, errors.Error) {
+       // validate
+       if vld != nil {
+               if err := vld.Struct(connection); err != nil {
+                       return nil, errors.Default.Wrap(err, "error validating 
target")
+               }
        }
-
-       apiClient, err := helper.NewApiClientFromConnection(context.TODO(), 
basicRes, &connection)
+       apiClient, err := helper.NewApiClientFromConnection(ctx, basicRes, 
&connection)
        if err != nil {
                return nil, err
        }
@@ -70,12 +62,58 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if res.StatusCode != http.StatusOK {
                return nil, errors.HttpStatus(res.StatusCode).New("unexpected 
status code when testing connection")
        }
+       connection = connection.Sanitize()
        body := GiteeTestConnResponse{}
        body.Success = true
        body.Message = "success"
        body.Connection = &connection
        // output
-       return &plugin.ApiResourceOutput{Body: body, Status: 200}, nil
+       return &body, nil
+}
+
+// TestConnection test gitee connection
+// @Summary test gitee connection
+// @Description Test gitee Connection. endpoint: https://gitee.com/api/v5/
+// @Tags plugins/gitee
+// @Param body body models.GiteeConn true "json body"
+// @Success 200  {object} GiteeTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/gitee/test [POST]
+func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       var err errors.Error
+       var connection models.GiteeConn
+       if err = helper.Decode(input.Body, &connection, vld); err != nil {
+               return nil, errors.BadInput.Wrap(err, "could not decode request 
parameters")
+       }
+       // test connection
+       result, err := testConnection(context.TODO(), connection)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
+}
+
+// TestExistingConnection test gitee connection
+// @Summary test gitee connection
+// @Description Test gitee Connection. endpoint: https://gitee.com/api/v5/
+// @Tags plugins/gitee
+// @Success 200  {object} GiteeTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/gitee/{connectionId}/test [POST]
+func TestExistingConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connection := &models.GiteeConnection{}
+       err := connectionHelper.First(connection, input.Params)
+       if err != nil {
+               return nil, errors.BadInput.Wrap(err, "find connection from db")
+       }
+       // test connection
+       result, err := testConnection(context.TODO(), connection.GiteeConn)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
 }
 
 // @Summary create gitee connection
@@ -92,7 +130,7 @@ func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: 
http.StatusOK}, nil
 }
 
 // @Summary patch gitee connection
@@ -109,7 +147,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: 
http.StatusOK}, nil
 }
 
 // @Summary delete a gitee connection
@@ -121,7 +159,14 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/gitee/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       return connectionHelper.Delete(&models.GiteeConnection{}, input)
+       conn := &models.GiteeConnection{}
+       output, err := connectionHelper.Delete(conn, input)
+       if err != nil {
+               return output, err
+       }
+       output.Body = conn.Sanitize()
+       return output, nil
+
 }
 
 // @Summary get all gitee connections
@@ -137,7 +182,9 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-
+       for idx, c := range connections {
+               connections[idx] = c.Sanitize()
+       }
        return &plugin.ApiResourceOutput{Body: connections}, nil
 }
 
@@ -151,5 +198,5 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
        connection := &models.GiteeConnection{}
        err := connectionHelper.First(connection, input.Params)
-       return &plugin.ApiResourceOutput{Body: connection}, err
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, err
 }
diff --git a/backend/plugins/gitee/impl/impl.go 
b/backend/plugins/gitee/impl/impl.go
index 7ccf03a4f..b31ec5bb7 100644
--- a/backend/plugins/gitee/impl/impl.go
+++ b/backend/plugins/gitee/impl/impl.go
@@ -196,6 +196,9 @@ func (p Gitee) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler {
                        "PATCH":  api.PatchConnection,
                        "DELETE": api.DeleteConnection,
                },
+               "connections/:connectionId/test": {
+                       "POST": api.TestExistingConnection,
+               },
        }
 }
 
diff --git a/backend/plugins/gitee/models/connection.go 
b/backend/plugins/gitee/models/connection.go
index fc94a7487..996009113 100644
--- a/backend/plugins/gitee/models/connection.go
+++ b/backend/plugins/gitee/models/connection.go
@@ -40,6 +40,11 @@ type GiteeConn struct {
        GiteeAccessToken      `mapstructure:",squash"`
 }
 
+func (connection GiteeConn) Sanitize() GiteeConn {
+       connection.Token = ""
+       return connection
+}
+
 // GiteeConnection holds GiteeConn plus ID/Name for database storage
 type GiteeConnection struct {
        helper.BaseConnection `mapstructure:",squash"`
@@ -67,3 +72,8 @@ type GiteeScopeConfig struct {
 func (GiteeConnection) TableName() string {
        return "_tool_gitee_connections"
 }
+
+func (connection GiteeConnection) Sanitize() GiteeConnection {
+       connection.GiteeConn = connection.GiteeConn.Sanitize()
+       return connection
+}
diff --git a/backend/plugins/github/api/connection_api.go 
b/backend/plugins/github/api/connection_api.go
index b2531a17d..f5af1b32c 100644
--- a/backend/plugins/github/api/connection_api.go
+++ b/backend/plugins/github/api/connection_api.go
@@ -60,6 +60,7 @@ type GithubTestConnResponse struct {
        Installations []models.GithubAppInstallation `json:"installations"`
 }
 
+// TestConnection test github connection
 // @Summary test github connection
 // @Description Test github Connection
 // @Tags plugins/github
@@ -75,35 +76,96 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if e != nil {
                return nil, errors.Convert(e)
        }
-       e = vld.StructExcept(conn, "GithubAppKey", "GithubAccessToken")
-       if e != nil {
-               return nil, errors.Convert(e)
+       testConnectionResult, err := testConnection(context.TODO(), conn)
+       if err != nil {
+               return nil, errors.Convert(err)
        }
+       return &plugin.ApiResourceOutput{Body: testConnectionResult, Status: 
http.StatusOK}, nil
+}
+
+// @Summary create github connection
+// @Description Create github connection
+// @Tags plugins/github
+// @Param body body models.GithubConnection true "json body"
+// @Success 200  {object} models.GithubConnection
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/github/connections [POST]
+func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       return dsHelper.ConnApi.Post(input)
+}
 
-       apiClient, err := api.NewApiClientFromConnection(context.TODO(), 
basicRes, &conn)
+// @Summary patch github connection
+// @Description Patch github connection
+// @Tags plugins/github
+// @Param body body models.GithubConnection true "json body"
+// @Success 200  {object} models.GithubConnection
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/github/connections/{connectionId} [PATCH]
+func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       return dsHelper.ConnApi.Patch(input)
+}
+
+// @Summary delete a github connection
+// @Description Delete a github connection
+// @Tags plugins/github
+// @Success 200  {object} models.GithubConnection
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 409  {object} services.BlueprintProjectPairs "References exist to 
this connection"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/github/connections/{connectionId} [DELETE]
+func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       return dsHelper.ConnApi.Delete(input)
+}
+
+// @Summary get all github connections
+// @Description Get all github connections
+// @Tags plugins/github
+// @Success 200  {object} []models.GithubConnection
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/github/connections [GET]
+func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       return dsHelper.ConnApi.GetAll(input)
+}
+
+// @Summary get github connection detail
+// @Description Get github connection detail
+// @Tags plugins/github
+// @Success 200  {object} models.GithubConnection
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/github/connections/{connectionId} [GET]
+func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
+       return dsHelper.ConnApi.GetDetail(input)
+}
+
+func testConnection(ctx context.Context, conn models.GithubConn) 
(*GithubTestConnResponse, errors.Error) {
+       if vld != nil {
+               if err := vld.StructExcept(conn, "GithubAppKey", 
"GithubAccessToken"); err != nil {
+                       return nil, errors.Convert(err)
+               }
+       }
+       apiClient, err := api.NewApiClientFromConnection(ctx, basicRes, &conn)
        if err != nil {
                return nil, err
        }
-
        githubApiResponse := &GithubTestConnResponse{}
-
        if conn.AuthMethod == "AppKey" {
                jwt, err := conn.GithubAppKey.CreateJwt()
                if err != nil {
                        return nil, err
                }
-
                res, err := apiClient.Get("app", nil, http.Header{
                        "Authorization": []string{fmt.Sprintf("Bearer %s", 
jwt)},
                })
-
                if err != nil {
                        return nil, errors.BadInput.Wrap(err, "verify token 
failed")
                }
                if res.StatusCode != http.StatusOK {
                        return nil, 
errors.HttpStatus(res.StatusCode).New("unexpected status code while testing 
connection")
                }
-
                githubApp := &models.GithubApp{}
                err = api.UnmarshalResponse(res, githubApp)
                if err != nil {
@@ -111,24 +173,20 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
                } else if githubApp.Slug == "" {
                        return nil, errors.BadInput.Wrap(err, "invalid token")
                }
-
                res, err = apiClient.Get("app/installations", nil, http.Header{
                        "Authorization": []string{fmt.Sprintf("Bearer %s", 
jwt)},
                })
-
                if err != nil {
                        return nil, errors.BadInput.Wrap(err, "verify token 
failed")
                }
                if res.StatusCode != http.StatusOK {
                        return nil, 
errors.HttpStatus(res.StatusCode).New("unexpected status code while testing 
connection")
                }
-
                githubAppInstallations := &[]models.GithubAppInstallation{}
                err = api.UnmarshalResponse(res, githubAppInstallations)
                if err != nil {
                        return nil, errors.BadInput.Wrap(err, "verify token 
failed")
                }
-
                githubApiResponse.Success = true
                githubApiResponse.Message = "success"
                githubApiResponse.Login = githubApp.Slug
@@ -139,15 +197,12 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
                if err != nil {
                        return nil, errors.BadInput.Wrap(err, "verify token 
failed")
                }
-
                if res.StatusCode == http.StatusUnauthorized {
                        return nil, 
errors.HttpStatus(http.StatusBadRequest).New("StatusUnauthorized error when 
testing connection")
                }
-
                if res.StatusCode != http.StatusOK {
                        return nil, 
errors.HttpStatus(res.StatusCode).New("unexpected status code while testing 
connection")
                }
-
                githubUserOfToken := &models.GithubUserOfToken{}
                err = api.UnmarshalResponse(res, githubUserOfToken)
                if err != nil {
@@ -155,7 +210,6 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
                } else if githubUserOfToken.Login == "" {
                        return nil, errors.BadInput.Wrap(err, "invalid token")
                }
-
                success := false
                warning := false
                messages := []string{}
@@ -200,63 +254,25 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
                return nil, errors.BadInput.New("invalid authentication method")
        }
 
-       return &plugin.ApiResourceOutput{Body: githubApiResponse, Status: 
http.StatusOK}, nil
-}
-
-// @Summary create github connection
-// @Description Create github connection
-// @Tags plugins/github
-// @Param body body models.GithubConnection true "json body"
-// @Success 200  {object} models.GithubConnection
-// @Failure 400  {string} errcode.Error "Bad Request"
-// @Failure 500  {string} errcode.Error "Internal Error"
-// @Router /plugins/github/connections [POST]
-func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       return dsHelper.ConnApi.Post(input)
-}
-
-// @Summary patch github connection
-// @Description Patch github connection
-// @Tags plugins/github
-// @Param body body models.GithubConnection true "json body"
-// @Success 200  {object} models.GithubConnection
-// @Failure 400  {string} errcode.Error "Bad Request"
-// @Failure 500  {string} errcode.Error "Internal Error"
-// @Router /plugins/github/connections/{connectionId} [PATCH]
-func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       return dsHelper.ConnApi.Patch(input)
-}
-
-// @Summary delete a github connection
-// @Description Delete a github connection
-// @Tags plugins/github
-// @Success 200  {object} models.GithubConnection
-// @Failure 400  {string} errcode.Error "Bad Request"
-// @Failure 409  {object} services.BlueprintProjectPairs "References exist to 
this connection"
-// @Failure 500  {string} errcode.Error "Internal Error"
-// @Router /plugins/github/connections/{connectionId} [DELETE]
-func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       return dsHelper.ConnApi.Delete(input)
-}
-
-// @Summary get all github connections
-// @Description Get all github connections
-// @Tags plugins/github
-// @Success 200  {object} []models.GithubConnection
-// @Failure 400  {string} errcode.Error "Bad Request"
-// @Failure 500  {string} errcode.Error "Internal Error"
-// @Router /plugins/github/connections [GET]
-func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       return dsHelper.ConnApi.GetAll(input)
+       return githubApiResponse, nil
 }
 
-// @Summary get github connection detail
-// @Description Get github connection detail
+// TestExistingConnection test github connection options
+// @Summary test github connection
+// @Description Test github Connection
 // @Tags plugins/github
-// @Success 200  {object} models.GithubConnection
+// @Success 200  {object} GithubTestConnResponse
 // @Failure 400  {string} errcode.Error "Bad Request"
 // @Failure 500  {string} errcode.Error "Internal Error"
-// @Router /plugins/github/connections/{connectionId} [GET]
-func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
-       return dsHelper.ConnApi.GetDetail(input)
+// @Router /plugins/github/{connectionId}/test [POST]
+func TestExistingConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connection, err := dsHelper.ConnApi.FindByPk(input)
+       if err != nil {
+               return nil, err
+       }
+       testConnectionResult, testConnectionErr := 
testConnection(context.TODO(), connection.GithubConn)
+       if testConnectionErr != nil {
+               return nil, testConnectionErr
+       }
+       return &plugin.ApiResourceOutput{Body: testConnectionResult, Status: 
http.StatusOK}, nil
 }
diff --git a/backend/plugins/github/api/init.go 
b/backend/plugins/github/api/init.go
index 6f6df7dfa..74d3d5293 100644
--- a/backend/plugins/github/api/init.go
+++ b/backend/plugins/github/api/init.go
@@ -37,11 +37,17 @@ func Init(br context.BasicRes, p plugin.PluginMeta) {
        basicRes = br
        dsHelper = api.NewDataSourceHelper[
                models.GithubConnection,
-               models.GithubRepo, models.GithubScopeConfig,
+               models.GithubRepo,
+               models.GithubScopeConfig,
        ](
                br,
                p.Name(),
                []string{"full_name"},
+               func(c models.GithubConnection) models.GithubConnection {
+                       return c.Sanitize()
+               },
+               nil,
+               nil,
        )
        // TODO: refactor remoteHelper
        vld = validator.New()
diff --git a/backend/plugins/github/impl/impl.go 
b/backend/plugins/github/impl/impl.go
index 51ed55dcd..90929564e 100644
--- a/backend/plugins/github/impl/impl.go
+++ b/backend/plugins/github/impl/impl.go
@@ -187,6 +187,9 @@ func (p Github) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler {
                        "PATCH":  api.PatchConnection,
                        "DELETE": api.DeleteConnection,
                },
+               "connections/:connectionId/test": {
+                       "POST": api.TestExistingConnection,
+               },
                "connections/:connectionId/scopes/:scopeId": {
                        "GET":    api.GetScope,
                        "PATCH":  api.PatchScope,
diff --git a/backend/plugins/github/models/connection.go 
b/backend/plugins/github/models/connection.go
index b1ac0252b..4b06a8788 100644
--- a/backend/plugins/github/models/connection.go
+++ b/backend/plugins/github/models/connection.go
@@ -43,7 +43,7 @@ type GithubAppKey struct {
        InstallationID int `mapstructure:"installationId" validate:"required" 
json:"installationId"`
 }
 
-// GithubConn holds the essential information to connect to the Github API
+// GithubConn holds the essential information to connect to the GitHub API
 type GithubConn struct {
        helper.RestConnection `mapstructure:",squash"`
        helper.MultiAuth      `mapstructure:",squash"`
@@ -94,10 +94,15 @@ type GithubConnection struct {
        EnableGraphql         bool `mapstructure:"enableGraphql" 
json:"enableGraphql"`
 }
 
-func (GithubConnection) TableName() string {
+func (connection GithubConnection) TableName() string {
        return "_tool_github_connections"
 }
 
+func (connection GithubConnection) Sanitize() GithubConnection {
+       connection.Token = ""
+       return connection
+}
+
 // Using GithubUserOfToken because it requires authentication, and it is 
public information anyway.
 type GithubUserOfToken struct {
        Login string `json:"login"`
diff --git a/backend/plugins/gitlab/api/connection_api.go 
b/backend/plugins/gitlab/api/connection_api.go
index ef00ac349..c03cf1727 100644
--- a/backend/plugins/gitlab/api/connection_api.go
+++ b/backend/plugins/gitlab/api/connection_api.go
@@ -35,23 +35,14 @@ type GitlabTestConnResponse struct {
        Connection *models.GitlabConn
 }
 
-// @Summary test gitlab connection
-// @Description Test gitlab Connection
-// @Tags plugins/gitlab
-// @Param body body models.GitlabConn true "json body"
-// @Success 200  {object} GitlabTestConnResponse "Success"
-// @Failure 400  {string} errcode.Error "Bad Request"
-// @Failure 500  {string} errcode.Error "Internal Error"
-// @Router /plugins/gitlab/test [POST]
-func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       // decode
-       var err errors.Error
-       var connection models.GitlabConn
-       if err = api.Decode(input.Body, &connection, vld); err != nil {
-               return nil, err
+func testConnection(ctx context.Context, connection models.GitlabConn) 
(*GitlabTestConnResponse, errors.Error) {
+       // validate
+       if vld != nil {
+               if err := vld.Struct(connection); err != nil {
+                       return nil, errors.Default.Wrap(err, "error validating 
target")
+               }
        }
-
-       apiClient, err := api.NewApiClientFromConnection(context.TODO(), 
basicRes, &connection)
+       apiClient, err := api.NewApiClientFromConnection(ctx, basicRes, 
&connection)
        if err != nil {
                return nil, err
        }
@@ -73,12 +64,56 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
                return nil, errors.BadInput.New("token need api or read_api 
permissions scope")
        }
 
+       connection = connection.Sanitize()
        body := GitlabTestConnResponse{}
        body.Success = true
        body.Message = "success"
        body.Connection = &connection
 
-       return &plugin.ApiResourceOutput{Body: body, Status: http.StatusOK}, nil
+       return &body, nil
+}
+
+// TestConnection test gitlab connection
+// @Summary test gitlab connection
+// @Description Test gitlab Connection
+// @Tags plugins/gitlab
+// @Param body body models.GitlabConn true "json body"
+// @Success 200  {object} GitlabTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/gitlab/test [POST]
+func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       // decode
+       var err errors.Error
+       var connection models.GitlabConn
+       if err = api.Decode(input.Body, &connection, vld); err != nil {
+               return nil, err
+       }
+       result, err := testConnection(context.TODO(), connection)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
+}
+
+// TestExistingConnection test gitlab connection
+// @Summary test gitlab connection
+// @Description Test gitlab Connection
+// @Tags plugins/gitlab
+// @Success 200  {object} GitlabTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/gitlab/{connectionId}/test [POST]
+func TestExistingConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connection, err := dsHelper.ConnApi.FindByPk(input)
+       if err != nil {
+               return nil, errors.BadInput.Wrap(err, "find connection from db")
+       }
+       result, err := testConnection(context.TODO(), connection.GitlabConn)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
 }
 
 // @Summary create gitlab connection
diff --git a/backend/plugins/gitlab/api/init.go 
b/backend/plugins/gitlab/api/init.go
index 4a88534c4..02bf69694 100644
--- a/backend/plugins/gitlab/api/init.go
+++ b/backend/plugins/gitlab/api/init.go
@@ -42,6 +42,11 @@ func Init(br context.BasicRes, p plugin.PluginMeta) {
                br,
                p.Name(),
                []string{"name"},
+               func(c models.GitlabConnection) models.GitlabConnection {
+                       return c.Sanitize()
+               },
+               nil,
+               nil,
        )
        // TODO: remove connectionHelper and refactor remoteHelper
        vld = validator.New()
diff --git a/backend/plugins/gitlab/impl/impl.go 
b/backend/plugins/gitlab/impl/impl.go
index 2de57fa02..9095db6cf 100644
--- a/backend/plugins/gitlab/impl/impl.go
+++ b/backend/plugins/gitlab/impl/impl.go
@@ -239,6 +239,9 @@ func (p Gitlab) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler {
                        "DELETE": api.DeleteConnection,
                        "GET":    api.GetConnection,
                },
+               "connections/:connectionId/test": {
+                       "POST": api.TestExistingConnection,
+               },
                "connections/:connectionId/scopes/:scopeId": {
                        "GET":    api.GetScope,
                        "PATCH":  api.PatchScope,
diff --git a/backend/plugins/gitlab/models/connection.go 
b/backend/plugins/gitlab/models/connection.go
index 48d33d069..411b7a82f 100644
--- a/backend/plugins/gitlab/models/connection.go
+++ b/backend/plugins/gitlab/models/connection.go
@@ -42,6 +42,11 @@ func (conn *GitlabConn) SetupAuthentication(request 
*http.Request) errors.Error
        return nil
 }
 
+func (conn *GitlabConn) Sanitize() GitlabConn {
+       conn.Token = ""
+       return *conn
+}
+
 // PrepareApiClient test api and set the IsPrivateToken,version,UserId and so 
on.
 func (conn *GitlabConn) PrepareApiClient(apiClient 
apihelperabstract.ApiClientAbstract) errors.Error {
        header1 := http.Header{}
@@ -139,3 +144,8 @@ type ApiUserResponse struct {
 func (GitlabConnection) TableName() string {
        return "_tool_gitlab_connections"
 }
+
+func (connection GitlabConnection) Sanitize() GitlabConnection {
+       connection.GitlabConn = connection.GitlabConn.Sanitize()
+       return connection
+}
diff --git a/backend/plugins/jenkins/api/connection.go 
b/backend/plugins/jenkins/api/connection.go
index ac4f90693..5ea1058da 100644
--- a/backend/plugins/jenkins/api/connection.go
+++ b/backend/plugins/jenkins/api/connection.go
@@ -35,28 +35,19 @@ type JenkinsTestConnResponse struct {
        Connection *models.JenkinsConn
 }
 
-// @Summary test jenkins connection
-// @Description Test Jenkins Connection
-// @Tags plugins/jenkins
-// @Param body body models.JenkinsConn true "json body"
-// @Success 200  {object} JenkinsTestConnResponse "Success"
-// @Failure 400  {string} errcode.Error "Bad Request"
-// @Failure 500  {string} errcode.Error "Internal Error"
-// @Router /plugins/jenkins/test [POST]
-func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       // decode
-       var err errors.Error
-       var connection models.JenkinsConn
-       err = api.Decode(input.Body, &connection, vld)
-       if err != nil {
-               return nil, err
+func testConnection(ctx context.Context, connection models.JenkinsConn) 
(*JenkinsTestConnResponse, errors.Error) {
+       // validate
+       if vld != nil {
+               if err := vld.Struct(connection); err != nil {
+                       return nil, errors.Default.Wrap(err, "error validating 
target")
+               }
        }
        // Check if the URL contains "/api"
        if strings.Contains(connection.Endpoint, "/api") {
                return nil, 
errors.HttpStatus(http.StatusBadRequest).New("Invalid URL. Please use the base 
URL without /api")
        }
        // test connection
-       apiClient, err := api.NewApiClientFromConnection(context.TODO(), 
basicRes, &connection)
+       apiClient, err := api.NewApiClientFromConnection(ctx, basicRes, 
&connection)
        if err != nil {
                return nil, err
        }
@@ -72,12 +63,60 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if res.StatusCode != http.StatusOK {
                return nil, errors.HttpStatus(res.StatusCode).New("unexpected 
status code when testing connection")
        }
+       connection = connection.Sanitize()
        body := JenkinsTestConnResponse{}
        body.Success = true
        body.Message = "success"
        body.Connection = &connection
        // output
-       return &plugin.ApiResourceOutput{Body: body, Status: 200}, nil
+       return &body, nil
+}
+
+// TestConnection test jenkins connection
+// @Summary test jenkins connection
+// @Description Test Jenkins Connection
+// @Tags plugins/jenkins
+// @Param body body models.JenkinsConn true "json body"
+// @Success 200  {object} JenkinsTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/jenkins/test [POST]
+func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       // decode
+       var err errors.Error
+       var connection models.JenkinsConn
+       err = api.Decode(input.Body, &connection, vld)
+       if err != nil {
+               return nil, err
+       }
+       // test connection
+       result, err := testConnection(context.TODO(), connection)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
+}
+
+// TestExistingConnection test jenkins connection
+// @Summary test jenkins connection
+// @Description Test Jenkins Connection
+// @Tags plugins/jenkins
+// @Success 200  {object} JenkinsTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/jenkins/{connectionId}/test [POST]
+func TestExistingConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connection := &models.JenkinsConnection{}
+       err := connectionHelper.First(connection, input.Params)
+       if err != nil {
+               return nil, errors.BadInput.Wrap(err, "find connection from db")
+       }
+       // test connection
+       result, err := testConnection(context.TODO(), connection.JenkinsConn)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
 }
 
 // @Summary create jenkins connection
@@ -97,7 +136,7 @@ func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: 
http.StatusOK}, nil
 }
 
 // @Summary patch jenkins connection
@@ -115,7 +154,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
                return nil, err
        }
 
-       return &plugin.ApiResourceOutput{Body: connection}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
 }
 
 // @Summary delete a jenkins connection
@@ -127,7 +166,14 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/jenkins/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       return connectionHelper.Delete(&models.JenkinsConnection{}, input)
+       conn := &models.JenkinsConnection{}
+       output, err := connectionHelper.Delete(conn, input)
+       if err != nil {
+               return output, err
+       }
+       output.Body = conn.Sanitize()
+       return output, nil
+
 }
 
 // @Summary get all jenkins connections
@@ -144,6 +190,9 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
                return nil, err
        }
 
+       for idx, c := range connections {
+               connections[idx] = c.Sanitize()
+       }
        return &plugin.ApiResourceOutput{Body: connections, Status: 
http.StatusOK}, nil
 }
 
@@ -160,5 +209,5 @@ func GetConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, e
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection}, err
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, err
 }
diff --git a/backend/plugins/jenkins/impl/impl.go 
b/backend/plugins/jenkins/impl/impl.go
index d0807fd64..8dee70297 100644
--- a/backend/plugins/jenkins/impl/impl.go
+++ b/backend/plugins/jenkins/impl/impl.go
@@ -183,6 +183,9 @@ func (p Jenkins) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler
                "connections/:connectionId/remote-scopes": {
                        "GET": api.RemoteScopes,
                },
+               "connections/:connectionId/test": {
+                       "POST": api.TestExistingConnection,
+               },
                "connections/:connectionId/search-remote-scopes": {
                        "GET": api.SearchRemoteScopes,
                },
diff --git a/backend/plugins/jenkins/models/connection.go 
b/backend/plugins/jenkins/models/connection.go
index 0967ebb79..665ab4d66 100644
--- a/backend/plugins/jenkins/models/connection.go
+++ b/backend/plugins/jenkins/models/connection.go
@@ -27,6 +27,11 @@ type JenkinsConn struct {
        helper.BasicAuth      `mapstructure:",squash"`
 }
 
+func (connection JenkinsConn) Sanitize() JenkinsConn {
+       connection.Password = ""
+       return connection
+}
+
 // JenkinsConnection holds JenkinsConn plus ID/Name for database storage
 type JenkinsConnection struct {
        helper.BaseConnection `mapstructure:",squash"`
@@ -36,3 +41,8 @@ type JenkinsConnection struct {
 func (JenkinsConnection) TableName() string {
        return "_tool_jenkins_connections"
 }
+
+func (connection JenkinsConnection) Sanitize() JenkinsConnection {
+       connection.JenkinsConn = connection.JenkinsConn.Sanitize()
+       return connection
+}
diff --git a/backend/plugins/jira/api/connection.go 
b/backend/plugins/jira/api/connection.go
index 6720afd13..f72a182d2 100644
--- a/backend/plugins/jira/api/connection.go
+++ b/backend/plugins/jira/api/connection.go
@@ -38,28 +38,16 @@ type JiraTestConnResponse struct {
        Connection *models.JiraConn
 }
 
-// @Summary test jira connection
-// @Description Test Jira Connection
-// @Tags plugins/jira
-// @Param body body models.JiraConn true "json body"
-// @Success 200  {object} JiraTestConnResponse "Success"
-// @Failure 400  {string} errcode.Error "Bad Request"
-// @Failure 500  {string} errcode.Error "Internal Error"
-// @Router /plugins/jira/test [POST]
-func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       // decode
-       var err errors.Error
-       var connection models.JiraConn
-       e := mapstructure.Decode(input.Body, &connection)
-       if e != nil {
-               return nil, errors.Convert(e)
-       }
-       e = vld.StructExcept(connection, "BasicAuth", "AccessToken")
-       if e != nil {
-               return nil, errors.Convert(e)
+func testConnection(ctx context.Context, connection models.JiraConn) 
(*JiraTestConnResponse, errors.Error) {
+       // validate
+       if vld != nil {
+               e := vld.StructExcept(connection, "BasicAuth", "AccessToken")
+               if e != nil {
+                       return nil, errors.Convert(e)
+               }
        }
        // test connection
-       apiClient, err := api.NewApiClientFromConnection(context.TODO(), 
basicRes, &connection)
+       apiClient, err := api.NewApiClientFromConnection(ctx, basicRes, 
&connection)
        if err != nil {
                return nil, err
        }
@@ -118,6 +106,7 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if res.StatusCode != http.StatusOK {
                return nil, 
errors.HttpStatus(res.StatusCode).New(fmt.Sprintf("%s Unexpected [%s] status 
code: %d %s", getStatusFail, res.Request.URL, res.StatusCode, errMsg))
        }
+       connection = connection.Sanitize()
        body := JiraTestConnResponse{}
        body.Success = true
        body.Message = "success"
@@ -125,7 +114,54 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: body, Status: 200}, nil
+       return &body, nil
+}
+
+// TestConnection test jira connection
+// @Summary test jira connection
+// @Description Test Jira Connection
+// @Tags plugins/jira
+// @Param body body models.JiraConn true "json body"
+// @Success 200  {object} JiraTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/jira/test [POST]
+func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       // decode
+       var err errors.Error
+       var connection models.JiraConn
+       e := mapstructure.Decode(input.Body, &connection)
+       if e != nil {
+               return nil, errors.Convert(e)
+       }
+       // test connection
+       result, err := testConnection(context.TODO(), connection)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
+}
+
+// TestExistingConnection test jira connection
+// @Summary test jira connection
+// @Description Test Jira Connection
+// @Tags plugins/jira
+// @Success 200  {object} JiraTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/jira/{connectionId}/test [POST]
+func TestExistingConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connection := &models.JiraConnection{}
+       err := connectionHelper.First(connection, input.Params)
+       if err != nil {
+               return nil, errors.BadInput.Wrap(err, "find connection from db")
+       }
+       // test connection
+       result, err := testConnection(context.TODO(), connection.JiraConn)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
 }
 
 // @Summary create jira connection
@@ -143,7 +179,7 @@ func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: 
http.StatusOK}, nil
 }
 
 // @Summary patch jira connection
@@ -160,7 +196,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
 }
 
 // @Summary delete a jira connection
@@ -172,7 +208,14 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/jira/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       return connectionHelper.Delete(&models.JiraConnection{}, input)
+       conn := &models.JiraConnection{}
+       output, err := connectionHelper.Delete(conn, input)
+       if err != nil {
+               return output, err
+       }
+       output.Body = conn.Sanitize()
+       return output, nil
+
 }
 
 // @Summary get all jira connections
@@ -188,6 +231,9 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
+       for idx, c := range connections {
+               connections[idx] = c.Sanitize()
+       }
        return &plugin.ApiResourceOutput{Body: connections, Status: 
http.StatusOK}, nil
 }
 
@@ -204,5 +250,5 @@ func GetConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, e
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection}, err
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, err
 }
diff --git a/backend/plugins/jira/impl/impl.go 
b/backend/plugins/jira/impl/impl.go
index d95587c3f..0b428bbce 100644
--- a/backend/plugins/jira/impl/impl.go
+++ b/backend/plugins/jira/impl/impl.go
@@ -290,6 +290,9 @@ func (p Jira) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler {
                "connections/:connectionId/proxy/rest/*path": {
                        "GET": api.Proxy,
                },
+               "connections/:connectionId/test": {
+                       "POST": api.TestExistingConnection,
+               },
                "connections/:connectionId/remote-scopes": {
                        "GET": api.RemoteScopes,
                },
diff --git a/backend/plugins/jira/models/connection.go 
b/backend/plugins/jira/models/connection.go
index 8e79a0aee..0229988f8 100644
--- a/backend/plugins/jira/models/connection.go
+++ b/backend/plugins/jira/models/connection.go
@@ -44,6 +44,12 @@ type JiraConn struct {
        helper.AccessToken    `mapstructure:",squash"`
 }
 
+func (jc *JiraConn) Sanitize() JiraConn {
+       jc.Password = ""
+       jc.AccessToken.Token = ""
+       return *jc
+}
+
 // SetupAuthentication implements the `IAuthentication` interface by delegating
 // the actual logic to the `MultiAuth` struct to help us write less code
 func (jc *JiraConn) SetupAuthentication(req *http.Request) errors.Error {
@@ -59,3 +65,8 @@ type JiraConnection struct {
 func (JiraConnection) TableName() string {
        return "_tool_jira_connections"
 }
+
+func (connection JiraConnection) Sanitize() JiraConnection {
+       connection.JiraConn = connection.JiraConn.Sanitize()
+       return connection
+}
diff --git a/backend/plugins/pagerduty/api/connection.go 
b/backend/plugins/pagerduty/api/connection.go
index 54e9ca900..95a9b97be 100644
--- a/backend/plugins/pagerduty/api/connection.go
+++ b/backend/plugins/pagerduty/api/connection.go
@@ -27,6 +27,30 @@ import (
        "github.com/apache/incubator-devlake/plugins/pagerduty/models"
 )
 
+func testConnection(ctx context.Context, connection models.PagerDutyConn) 
(*plugin.ApiResourceOutput, errors.Error) {
+       if vld != nil {
+               if err := vld.Struct(connection); err != nil {
+                       return nil, errors.Default.Wrap(err, "error validating 
target")
+               }
+       }
+       apiClient, err := api.NewApiClientFromConnection(ctx, basicRes, 
&connection)
+       if err != nil {
+               return nil, err
+       }
+       response, err := apiClient.Get("licenses", nil, nil)
+       if err != nil {
+               return nil, err
+       }
+       if response.StatusCode == http.StatusUnauthorized {
+               return nil, 
errors.HttpStatus(http.StatusBadRequest).New("StatusUnauthorized error while 
testing connection")
+       }
+       if response.StatusCode == http.StatusOK {
+               return &plugin.ApiResourceOutput{Body: nil, Status: 
http.StatusOK}, nil
+       }
+       return &plugin.ApiResourceOutput{Body: nil, Status: 
response.StatusCode}, errors.HttpStatus(response.StatusCode).Wrap(err, "could 
not validate connection")
+}
+
+// TestConnection test pagerduty connection
 // @Summary test pagerduty connection
 // @Description Test Pagerduty Connection
 // @Tags plugins/pagerduty
@@ -41,23 +65,24 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       apiClient, err := api.NewApiClientFromConnection(context.TODO(), 
basicRes, &connection)
-       if err != nil {
-               return nil, err
-       }
-       response, err := apiClient.Get("licenses", nil, nil)
-       if err != nil {
-               return nil, err
-       }
-
-       if response.StatusCode == http.StatusUnauthorized {
-               return nil, 
errors.HttpStatus(http.StatusBadRequest).New("StatusUnauthorized error while 
testing connection")
-       }
+       return testConnection(context.TODO(), connection)
+}
 
-       if response.StatusCode == http.StatusOK {
-               return &plugin.ApiResourceOutput{Body: nil, Status: 
http.StatusOK}, nil
+// TestExistingConnection test pagerduty connection
+// @Summary test pagerduty connection
+// @Description Test Pagerduty Connection
+// @Tags plugins/pagerduty
+// @Success 200  {object} shared.ApiBody "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/pagerduty/{connectionId}/test [POST]
+func TestExistingConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connection := &models.PagerDutyConnection{}
+       err := connectionHelper.First(connection, input.Params)
+       if err != nil {
+               return nil, errors.BadInput.Wrap(err, "find connection from db")
        }
-       return &plugin.ApiResourceOutput{Body: nil, Status: 
response.StatusCode}, errors.HttpStatus(response.StatusCode).Wrap(err, "could 
not validate connection")
+       return testConnection(context.TODO(), connection.PagerDutyConn)
 }
 
 // @Summary create pagerduty connection
@@ -74,7 +99,7 @@ func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: 
http.StatusOK}, nil
 }
 
 // @Summary patch pagerduty connection
@@ -91,7 +116,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: 
http.StatusOK}, nil
 }
 
 // @Summary delete pagerduty connection
@@ -103,7 +128,13 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/pagerduty/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       return connectionHelper.Delete(&models.PagerDutyConnection{}, input)
+       conn := &models.PagerDutyConnection{}
+       output, err := connectionHelper.Delete(conn, input)
+       if err != nil {
+               return output, err
+       }
+       output.Body = conn.Sanitize()
+       return output, nil
 }
 
 // @Summary list pagerduty connections
@@ -119,7 +150,9 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-
+       for idx, c := range connections {
+               connections[idx] = c.Sanitize()
+       }
        return &plugin.ApiResourceOutput{Body: connections}, nil
 }
 
@@ -136,5 +169,5 @@ func GetConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, e
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
 }
diff --git a/backend/plugins/pagerduty/impl/impl.go 
b/backend/plugins/pagerduty/impl/impl.go
index ef15eb9c1..8f726e436 100644
--- a/backend/plugins/pagerduty/impl/impl.go
+++ b/backend/plugins/pagerduty/impl/impl.go
@@ -146,6 +146,9 @@ func (p PagerDuty) ApiResources() 
map[string]map[string]plugin.ApiResourceHandle
                        "PATCH":  api.PatchConnection,
                        "DELETE": api.DeleteConnection,
                },
+               "connections/:connectionId/test": {
+                       "POST": api.TestExistingConnection,
+               },
                "connections/:connectionId/remote-scopes": {
                        "GET": api.RemoteScopes,
                },
diff --git a/backend/plugins/pagerduty/models/connection.go 
b/backend/plugins/pagerduty/models/connection.go
index 3d18839f1..e287b4f48 100644
--- a/backend/plugins/pagerduty/models/connection.go
+++ b/backend/plugins/pagerduty/models/connection.go
@@ -62,3 +62,8 @@ type ApiUserResponse struct {
 func (PagerDutyConnection) TableName() string {
        return "_tool_pagerduty_connections"
 }
+
+func (connection PagerDutyConnection) Sanitize() PagerDutyConnection {
+       connection.Token = ""
+       return connection
+}
diff --git a/backend/plugins/slack/api/connection.go 
b/backend/plugins/slack/api/connection.go
index 620230e7b..1094986c0 100644
--- a/backend/plugins/slack/api/connection.go
+++ b/backend/plugins/slack/api/connection.go
@@ -34,6 +34,27 @@ type SlackTestConnResponse struct {
        Connection *models.SlackConn
 }
 
+func testConnection(ctx context.Context, connection models.SlackConn) 
(*SlackTestConnResponse, errors.Error) {
+       // validate
+       if vld != nil {
+               if err := vld.Struct(connection); err != nil {
+                       return nil, errors.Default.Wrap(err, "error validating 
target")
+               }
+       }
+       // test connection
+       _, err := api.NewApiClientFromConnection(context.TODO(), basicRes, 
&connection)
+       if err != nil {
+               return nil, err
+       }
+       connection = connection.Sanitize()
+       body := SlackTestConnResponse{}
+       body.Success = true
+       body.Message = "success"
+       body.Connection = &connection
+       return &body, nil
+}
+
+// TestConnection test slack connection
 // @Summary test slack connection
 // @Description Test slack Connection. endpoint: 
https://open.slack.cn/open-apis/
 // @Tags plugins/slack
@@ -48,18 +69,34 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err := api.Decode(input.Body, &connection, vld); err != nil {
                return nil, errors.BadInput.Wrap(err, "could not decode request 
parameters")
        }
-
        // test connection
-       _, err := api.NewApiClientFromConnection(context.TODO(), basicRes, 
&connection)
+       result, err := testConnection(context.TODO(), connection)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
+}
 
-       body := SlackTestConnResponse{}
-       body.Success = true
-       body.Message = "success"
-       body.Connection = &connection
+// TestExistingConnection test slack connection
+// @Summary test slack connection
+// @Description Test slack Connection. endpoint: 
https://open.slack.cn/open-apis/
+// @Tags plugins/slack
+// @Success 200  {object} SlackTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/slack/{connectionId}/test [POST]
+func TestExistingConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connection := &models.SlackConnection{}
+       err := connectionHelper.First(connection, input.Params)
+       if err != nil {
+               return nil, errors.BadInput.Wrap(err, "find connection from db")
+       }
+       // test connection
+       result, err := testConnection(context.TODO(), connection.SlackConn)
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: body, Status: 200}, nil
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
 }
 
 // @Summary create slack connection
@@ -76,7 +113,7 @@ func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: 
http.StatusOK}, nil
 }
 
 // @Summary patch slack connection
@@ -93,7 +130,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: 
http.StatusOK}, nil
 }
 
 // @Summary delete a slack connection
@@ -105,7 +142,13 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/slack/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       return connectionHelper.Delete(&models.SlackConnection{}, input)
+       conn := &models.SlackConnection{}
+       output, err := connectionHelper.Delete(conn, input)
+       if err != nil {
+               return output, err
+       }
+       output.Body = conn.Sanitize()
+       return output, nil
 }
 
 // @Summary get all slack connections
@@ -121,7 +164,9 @@ func ListConnections(_ *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, err
        if err != nil {
                return nil, err
        }
-
+       for idx, c := range connections {
+               connections[idx] = c.Sanitize()
+       }
        return &plugin.ApiResourceOutput{Body: connections}, nil
 }
 
@@ -138,5 +183,5 @@ func GetConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, e
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection}, err
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, err
 }
diff --git a/backend/plugins/slack/impl/impl.go 
b/backend/plugins/slack/impl/impl.go
index 8bd7ad9c5..e526d0642 100644
--- a/backend/plugins/slack/impl/impl.go
+++ b/backend/plugins/slack/impl/impl.go
@@ -140,6 +140,9 @@ func (p Slack) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler {
                        "DELETE": api.DeleteConnection,
                        "GET":    api.GetConnection,
                },
+               "connections/:connectionId/test": {
+                       "POST": api.TestExistingConnection,
+               },
        }
 }
 
diff --git a/backend/plugins/slack/models/connection.go 
b/backend/plugins/slack/models/connection.go
index 89182e35b..e227e9ba7 100644
--- a/backend/plugins/slack/models/connection.go
+++ b/backend/plugins/slack/models/connection.go
@@ -27,6 +27,11 @@ type SlackConn struct {
        helper.AccessToken    `mapstructure:",squash"`
 }
 
+func (connection SlackConn) Sanitize() SlackConn {
+       connection.Token = ""
+       return connection
+}
+
 // SlackConnection holds SlackConn plus ID/Name for database storage
 type SlackConnection struct {
        helper.BaseConnection `mapstructure:",squash"`
@@ -36,3 +41,8 @@ type SlackConnection struct {
 func (SlackConnection) TableName() string {
        return "_tool_slack_connections"
 }
+
+func (connection SlackConnection) Sanitize() SlackConnection {
+       connection.SlackConn = connection.SlackConn.Sanitize()
+       return connection
+}
diff --git a/backend/plugins/sonarqube/api/connection.go 
b/backend/plugins/sonarqube/api/connection.go
index f34d89462..cb70e715f 100644
--- a/backend/plugins/sonarqube/api/connection.go
+++ b/backend/plugins/sonarqube/api/connection.go
@@ -36,23 +36,14 @@ type SonarqubeTestConnResponse struct {
        Connection *models.SonarqubeConn
 }
 
-// TestConnection test sonarqube connection options
-// @Summary test sonarqube connection
-// @Description Test sonarqube Connection
-// @Tags plugins/sonarqube
-// @Param body body models.SonarqubeConn true "json body"
-// @Success 200  {object} SonarqubeTestConnResponse "Success"
-// @Failure 400  {string} errcode.Error "Bad Request"
-// @Failure 500  {string} errcode.Error "Internal Error"
-// @Router /plugins/sonarqube/test [POST]
-func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       // decode
-       var err errors.Error
-       var connection models.SonarqubeConn
-       if err = api.Decode(input.Body, &connection, vld); err != nil {
-               return nil, err
+func testConnection(ctx context.Context, connection models.SonarqubeConn) 
(*plugin.ApiResourceOutput, errors.Error) {
+       // validate
+       if vld != nil {
+               if err := vld.Struct(connection); err != nil {
+                       return nil, errors.Default.Wrap(err, "error validating 
target")
+               }
        }
-       apiClient, err := api.NewApiClientFromConnection(context.TODO(), 
basicRes, &connection)
+       apiClient, err := api.NewApiClientFromConnection(ctx, basicRes, 
&connection)
        if err != nil {
                return nil, err
        }
@@ -65,24 +56,62 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        case 200: // right StatusCode
                valid := &validation{}
                err = api.UnmarshalResponse(res, valid)
+               if err != nil {
+                       return nil, err
+               }
                body := SonarqubeTestConnResponse{}
                body.Success = true
                body.Message = "success"
+               connection = connection.Sanitize()
                body.Connection = &connection
-               if err != nil {
-                       return nil, err
-               }
                if !valid.Valid {
                        return nil, errors.Default.New("Authentication failed, 
please check your access token.")
                }
                return &plugin.ApiResourceOutput{Body: body, Status: 200}, nil
        case 401: // error secretKey or nonceStr
                return &plugin.ApiResourceOutput{Body: false, Status: 
http.StatusBadRequest}, nil
-       default: // unknow what happen , back to user
+       default: // unknown what happen , back to user
                return &plugin.ApiResourceOutput{Body: res.Body, Status: 
res.StatusCode}, nil
        }
 }
 
+// TestConnection test sonarqube connection options
+// @Summary test sonarqube connection
+// @Description Test sonarqube Connection
+// @Tags plugins/sonarqube
+// @Param body body models.SonarqubeConn true "json body"
+// @Success 200  {object} SonarqubeTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/sonarqube/test [POST]
+func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       // decode
+       var err errors.Error
+       var connection models.SonarqubeConn
+       if err = api.Decode(input.Body, &connection, vld); err != nil {
+               return nil, err
+       }
+       return testConnection(context.TODO(), connection)
+}
+
+// TestExistingConnection test sonarqube connection options
+// @Summary test sonarqube connection
+// @Description Test sonarqube Connection
+// @Tags plugins/sonarqube
+// @Success 200  {object} SonarqubeTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/sonarqube/{connectionId}/test [POST]
+func TestExistingConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connection := &models.SonarqubeConnection{}
+       err := connectionHelper.First(connection, input.Params)
+       if err != nil {
+               return nil, errors.BadInput.Wrap(err, "find connection from db")
+       }
+       // test connection
+       return testConnection(context.TODO(), connection.SonarqubeConn)
+}
+
 // PostConnections create sonarqube connection
 // @Summary create sonarqube connection
 // @Description Create sonarqube connection
@@ -99,7 +128,7 @@ func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: 
http.StatusOK}, nil
 }
 
 // PatchConnection patch sonarqube connection
@@ -118,7 +147,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
 }
 
 // DeleteConnection delete a sonarqube connection
@@ -132,7 +161,14 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/sonarqube/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       return connectionHelper.Delete(&models.SonarqubeConnection{}, input)
+       conn := &models.SonarqubeConnection{}
+       output, err := connectionHelper.Delete(conn, input)
+       if err != nil {
+               return output, err
+       }
+       output.Body = conn.Sanitize()
+       return output, nil
+
 }
 
 // ListConnections get all sonarqube connections
@@ -149,6 +185,9 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
+       for idx, c := range connections {
+               connections[idx] = c.Sanitize()
+       }
        return &plugin.ApiResourceOutput{Body: connections, Status: 
http.StatusOK}, nil
 }
 
@@ -164,5 +203,5 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
        connection := &models.SonarqubeConnection{}
        err := connectionHelper.First(connection, input.Params)
-       return &plugin.ApiResourceOutput{Body: connection}, err
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, err
 }
diff --git a/backend/plugins/sonarqube/impl/impl.go 
b/backend/plugins/sonarqube/impl/impl.go
index 3bf4894ae..1f2de31e7 100644
--- a/backend/plugins/sonarqube/impl/impl.go
+++ b/backend/plugins/sonarqube/impl/impl.go
@@ -176,6 +176,9 @@ func (p Sonarqube) ApiResources() 
map[string]map[string]plugin.ApiResourceHandle
                        "PATCH":  api.PatchConnection,
                        "DELETE": api.DeleteConnection,
                },
+               "connections/:connectionId/test": {
+                       "POST": api.TestExistingConnection,
+               },
                "connections/:connectionId/remote-scopes": {
                        "GET": api.RemoteScopes,
                },
diff --git a/backend/plugins/sonarqube/models/connection.go 
b/backend/plugins/sonarqube/models/connection.go
index 3b8cd3125..f881c6e3a 100644
--- a/backend/plugins/sonarqube/models/connection.go
+++ b/backend/plugins/sonarqube/models/connection.go
@@ -50,6 +50,11 @@ type SonarqubeConn struct {
        SonarqubeAccessToken  `mapstructure:",squash"`
 }
 
+func (connection SonarqubeConn) Sanitize() SonarqubeConn {
+       connection.Token = ""
+       return connection
+}
+
 // This object conforms to what the frontend currently sends.
 type SonarqubeConnection struct {
        helper.BaseConnection `mapstructure:",squash"`
@@ -66,3 +71,8 @@ type SonarqubeResponse struct {
 func (SonarqubeConnection) TableName() string {
        return "_tool_sonarqube_connections"
 }
+
+func (connection SonarqubeConnection) Sanitize() SonarqubeConnection {
+       connection.SonarqubeConn = connection.SonarqubeConn.Sanitize()
+       return connection
+}
diff --git a/backend/plugins/tapd/api/connection.go 
b/backend/plugins/tapd/api/connection.go
index ea2cb946f..ee054209c 100644
--- a/backend/plugins/tapd/api/connection.go
+++ b/backend/plugins/tapd/api/connection.go
@@ -35,24 +35,15 @@ type TapdTestConnResponse struct {
        Connection *models.TapdConn
 }
 
-// @Summary test tapd connection
-// @Description Test Tapd Connection
-// @Tags plugins/tapd
-// @Param body body models.TapdConn true "json body"
-// @Success 200  {object} TapdTestConnResponse "Success"
-// @Failure 400  {string} errcode.Error "Bad Request"
-// @Failure 500  {string} errcode.Error "Internal Error"
-// @Router /plugins/tapd/test [POST]
-func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+func testConnection(ctx context.Context, connection models.TapdConn) 
(*TapdTestConnResponse, errors.Error) {
        // process input
-       var connection models.TapdConn
-       err := api.Decode(input.Body, &connection, vld)
-       if err != nil {
-               return nil, err
+       if vld != nil {
+               if err := vld.Struct(connection); err != nil {
+                       return nil, errors.Default.Wrap(err, "error validating 
target")
+               }
        }
-
        // test connection
-       apiClient, err := api.NewApiClientFromConnection(context.TODO(), 
basicRes, &connection)
+       apiClient, err := api.NewApiClientFromConnection(ctx, basicRes, 
&connection)
        if err != nil {
                return nil, errors.Default.Wrap(err, fmt.Sprintf("verify token 
failed for %s", connection.Username))
        }
@@ -66,12 +57,59 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if res.StatusCode != http.StatusOK {
                return nil, 
errors.HttpStatus(res.StatusCode).New(fmt.Sprintf("unexpected status code: %d", 
res.StatusCode))
        }
+       connection = connection.Sanitize()
        body := TapdTestConnResponse{}
        body.Success = true
        body.Message = "success"
        body.Connection = &connection
        // output
-       return &plugin.ApiResourceOutput{Body: body, Status: 200}, nil
+       return &body, nil
+}
+
+// TestConnection test tap connection
+// @Summary test tapd connection
+// @Description Test Tapd Connection
+// @Tags plugins/tapd
+// @Param body body models.TapdConn true "json body"
+// @Success 200  {object} TapdTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/tapd/test [POST]
+func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       // process input
+       var connection models.TapdConn
+       err := api.Decode(input.Body, &connection, vld)
+       if err != nil {
+               return nil, err
+       }
+       // test connection
+       result, err := testConnection(context.TODO(), connection)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
+}
+
+// TestExistingConnection test tapd connection options
+// @Summary test tapd connection
+// @Description Test Tapd Connection
+// @Tags plugins/tapd
+// @Success 200  {object} TapdTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/tapd/{connectionId}/test [POST]
+func TestExistingConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connection := &models.TapdConnection{}
+       err := connectionHelper.First(connection, input.Params)
+       if err != nil {
+               return nil, errors.BadInput.Wrap(err, "find connection from db")
+       }
+       // test connection
+       result, err := testConnection(context.TODO(), connection.TapdConn)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
 }
 
 // @Summary create tapd connection
@@ -93,7 +131,7 @@ func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
                return nil, err
        }
 
-       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: 
http.StatusOK}, nil
 }
 
 // @Summary patch tapd connection
@@ -111,7 +149,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
                return nil, err
        }
 
-       return &plugin.ApiResourceOutput{Body: connection}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
 }
 
 // @Summary delete a tapd connection
@@ -123,7 +161,13 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/tapd/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       return connectionHelper.Delete(&models.TapdConnection{}, input)
+       conn := &models.TapdConnection{}
+       output, err := connectionHelper.Delete(conn, input)
+       if err != nil {
+               return output, err
+       }
+       output.Body = conn.Sanitize()
+       return output, nil
 }
 
 // @Summary get all tapd connections
@@ -139,7 +183,9 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-
+       for idx, c := range connections {
+               connections[idx] = c.Sanitize()
+       }
        return &plugin.ApiResourceOutput{Body: connections, Status: 
http.StatusOK}, nil
 }
 
diff --git a/backend/plugins/tapd/impl/impl.go 
b/backend/plugins/tapd/impl/impl.go
index 04ec1d62d..c28bf35ce 100644
--- a/backend/plugins/tapd/impl/impl.go
+++ b/backend/plugins/tapd/impl/impl.go
@@ -287,6 +287,9 @@ func (p Tapd) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler {
                        "DELETE": api.DeleteConnection,
                        "GET":    api.GetConnection,
                },
+               "connections/:connectionId/test": {
+                       "POST": api.TestExistingConnection,
+               },
                "connections/:connectionId/proxy/rest/*path": {
                        "GET": api.Proxy,
                },
diff --git a/backend/plugins/tapd/models/connection.go 
b/backend/plugins/tapd/models/connection.go
index 5a13a9c9e..e5d295adc 100644
--- a/backend/plugins/tapd/models/connection.go
+++ b/backend/plugins/tapd/models/connection.go
@@ -27,6 +27,11 @@ type TapdConn struct {
        helper.BasicAuth      `mapstructure:",squash"`
 }
 
+func (connection TapdConn) Sanitize() TapdConn {
+       connection.Password = ""
+       return connection
+}
+
 // TapdConnection holds TapdConn plus ID/Name for database storage
 type TapdConnection struct {
        helper.BaseConnection `mapstructure:",squash"`
@@ -36,3 +41,8 @@ type TapdConnection struct {
 func (TapdConnection) TableName() string {
        return "_tool_tapd_connections"
 }
+
+func (connection TapdConnection) Sanitize() TapdConnection {
+       connection.TapdConn = connection.TapdConn.Sanitize()
+       return connection
+}
diff --git a/backend/plugins/teambition/api/connection.go 
b/backend/plugins/teambition/api/connection.go
index 142ac7fe8..aec27bf42 100644
--- a/backend/plugins/teambition/api/connection.go
+++ b/backend/plugins/teambition/api/connection.go
@@ -35,22 +35,13 @@ type TeambitionTestConnResponse struct {
        Connection *models.TeambitionConn
 }
 
-// TestConnection @Summary test teambition connection
-// @Description Test teambition Connection
-// @Tags plugins/teambition
-// @Param body body models.TeambitionConn true "json body"
-// @Success 200  {object} TeambitionTestConnResponse "Success"
-// @Failure 400  {string} errcode.Error "Bad Request"
-// @Failure 500  {string} errcode.Error "Internal Error"
-// @Router /plugins/teambition/test [POST]
-func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+func testConnection(ctx context.Context, connection models.TeambitionConn) 
(*TeambitionTestConnResponse, errors.Error) {
        // process input
-       var connection models.TeambitionConn
-       err := api.Decode(input.Body, &connection, vld)
-       if err != nil {
-               return nil, err
+       if vld != nil {
+               if err := vld.Struct(connection); err != nil {
+                       return nil, errors.Default.Wrap(err, "error validating 
target")
+               }
        }
-
        // test connection
        apiClient, err := api.NewApiClientFromConnection(context.TODO(), 
basicRes, &connection)
        if err != nil {
@@ -80,12 +71,58 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
                return nil, 
errors.HttpStatus(resBody.Code).New(fmt.Sprintf("unexpected body status code: 
%d", resBody.Code))
        }
 
+       connection = connection.Sanitize()
        body := TeambitionTestConnResponse{}
        body.Success = true
        body.Message = "success"
        body.Connection = &connection
        // output
-       return &plugin.ApiResourceOutput{Body: body, Status: 200}, nil
+       return &body, nil
+}
+
+// TestConnection @Summary test teambition connection
+// @Description Test teambition Connection
+// @Tags plugins/teambition
+// @Param body body models.TeambitionConn true "json body"
+// @Success 200  {object} TeambitionTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/teambition/test [POST]
+func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       // process input
+       var connection models.TeambitionConn
+       err := api.Decode(input.Body, &connection, vld)
+       if err != nil {
+               return nil, err
+       }
+
+       // test connection
+       result, err := testConnection(context.TODO(), connection)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
+}
+
+// TestExistingConnection test teambition connection options
+// @Summary test teambition connection
+// @Description Test teambition Connection
+// @Tags plugins/teambition
+// @Success 200  {object} TeambitionTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/teambition/{connectionId}/test [POST]
+func TestExistingConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connection := &models.TeambitionConnection{}
+       err := connectionHelper.First(connection, input.Params)
+       if err != nil {
+               return nil, errors.BadInput.Wrap(err, "find connection from db")
+       }
+       testConnectionResult, testConnectionErr := 
testConnection(context.TODO(), connection.TeambitionConn)
+       if testConnectionErr != nil {
+               return nil, testConnectionErr
+       }
+       return &plugin.ApiResourceOutput{Body: testConnectionResult, Status: 
http.StatusOK}, nil
 }
 
 // PostConnections @Summary create teambition connection
@@ -103,7 +140,7 @@ func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: 
http.StatusOK}, nil
 }
 
 // PatchConnection @Summary patch teambition connection
@@ -120,7 +157,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
 }
 
 // DeleteConnection @Summary delete a teambition connection
@@ -132,7 +169,14 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/teambition/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       return connectionHelper.Delete(&models.TeambitionConnection{}, input)
+       conn := &models.TeambitionConnection{}
+       output, err := connectionHelper.Delete(conn, input)
+       if err != nil {
+               return output, err
+       }
+       output.Body = conn.Sanitize()
+       return output, nil
+
 }
 
 // ListConnections @Summary get all teambition connections
@@ -148,6 +192,9 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
+       for idx, c := range connections {
+               connections[idx] = c.Sanitize()
+       }
        return &plugin.ApiResourceOutput{Body: connections, Status: 
http.StatusOK}, nil
 }
 
@@ -161,5 +208,5 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
        connection := &models.TeambitionConnection{}
        err := connectionHelper.First(connection, input.Params)
-       return &plugin.ApiResourceOutput{Body: connection}, err
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, err
 }
diff --git a/backend/plugins/teambition/impl/impl.go 
b/backend/plugins/teambition/impl/impl.go
index 410dfd173..2140dfe6d 100644
--- a/backend/plugins/teambition/impl/impl.go
+++ b/backend/plugins/teambition/impl/impl.go
@@ -177,6 +177,9 @@ func (p Teambition) ApiResources() 
map[string]map[string]plugin.ApiResourceHandl
                        "PATCH":  api.PatchConnection,
                        "DELETE": api.DeleteConnection,
                },
+               "connections/:connectionId/test": {
+                       "POST": api.TestExistingConnection,
+               },
        }
 }
 
diff --git a/backend/plugins/teambition/models/connection.go 
b/backend/plugins/teambition/models/connection.go
index b50d4d13b..d58c07035 100644
--- a/backend/plugins/teambition/models/connection.go
+++ b/backend/plugins/teambition/models/connection.go
@@ -34,12 +34,22 @@ type TeambitionConn struct {
        TenantType            string `mapstructure:"tenantType" 
validate:"required" json:"tenantType"`
 }
 
+func (tc TeambitionConn) Sanitize() TeambitionConn {
+       tc.SecretKey = ""
+       return tc
+}
+
 // TeambitionConnection holds TeambitionConn plus ID/Name for database storage
 type TeambitionConnection struct {
        helper.BaseConnection `mapstructure:",squash"`
        TeambitionConn        `mapstructure:",squash"`
 }
 
+func (connection TeambitionConnection) Sanitize() TeambitionConnection {
+       connection.TeambitionConn = connection.TeambitionConn.Sanitize()
+       return connection
+}
+
 func (tc *TeambitionConn) SetupAuthentication(req *http.Request) errors.Error {
        token := jwt.New(jwt.SigningMethodHS256)
        claims := make(jwt.MapClaims)
diff --git a/backend/plugins/trello/api/connection.go 
b/backend/plugins/trello/api/connection.go
index 6e4776112..e6a500218 100644
--- a/backend/plugins/trello/api/connection.go
+++ b/backend/plugins/trello/api/connection.go
@@ -34,23 +34,14 @@ type TrelloTestConnResponse struct {
        Connection *models.TrelloConn
 }
 
-// @Summary test trello connection
-// @Description Test trello Connection
-// @Tags plugins/trello
-// @Param body body models.TrelloConn true "json body"
-// @Success 200  {object} TrelloTestConnResponse "Success"
-// @Failure 400  {string} errcode.Error "Bad Request"
-// @Failure 500  {string} errcode.Error "Internal Error"
-// @Router /plugins/trello/test [POST]
-func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+func testConnection(ctx context.Context, connection models.TrelloConn) 
(*TrelloTestConnResponse, errors.Error) {
        // process input
-       var connection models.TrelloConn
-       err := helper.Decode(input.Body, &connection, vld)
-       if err != nil {
-               return nil, err
+       if vld != nil {
+               if err := vld.Struct(connection); err != nil {
+                       return nil, errors.Default.Wrap(err, "error validating 
target")
+               }
        }
-
-       apiClient, err := helper.NewApiClientFromConnection(context.TODO(), 
basicRes, &connection)
+       apiClient, err := helper.NewApiClientFromConnection(ctx, basicRes, 
&connection)
        if err != nil {
                return nil, err
        }
@@ -66,12 +57,58 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if res.StatusCode != http.StatusOK {
                return nil, errors.HttpStatus(res.StatusCode).New("unexpected 
status code while testing connection")
        }
+       connection = connection.Sanitize()
        body := TrelloTestConnResponse{}
        body.Success = true
        body.Message = "success"
        body.Connection = &connection
        // output
-       return &plugin.ApiResourceOutput{Body: body, Status: 200}, nil
+       return &body, nil
+}
+
+// TestConnection test trello connection
+// @Summary test trello connection
+// @Description Test trello Connection
+// @Tags plugins/trello
+// @Param body body models.TrelloConn true "json body"
+// @Success 200  {object} TrelloTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/trello/test [POST]
+func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       // process input
+       var connection models.TrelloConn
+       err := helper.Decode(input.Body, &connection, vld)
+       if err != nil {
+               return nil, err
+       }
+       // test connection
+       result, err := testConnection(context.TODO(), connection)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
+}
+
+// TestExistingConnection test trello connection options
+// @Summary test trello connection
+// @Description Test trello Connection
+// @Tags plugins/trello
+// @Success 200  {object} TrelloTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/trello/{connectionId}/test [POST]
+func TestExistingConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connection := &models.TrelloConnection{}
+       err := connectionHelper.First(connection, input.Params)
+       if err != nil {
+               return nil, errors.BadInput.Wrap(err, "find connection from db")
+       }
+       testConnectionResult, testConnectionErr := 
testConnection(context.TODO(), connection.TrelloConn)
+       if testConnectionErr != nil {
+               return nil, testConnectionErr
+       }
+       return &plugin.ApiResourceOutput{Body: testConnectionResult, Status: 
http.StatusOK}, nil
 }
 
 // @Summary create trello connection
@@ -89,7 +126,7 @@ func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: 
http.StatusOK}, nil
 }
 
 // @Summary patch trello connection
@@ -106,7 +143,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
 }
 
 // @Summary delete a trello connection
@@ -118,7 +155,14 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/trello/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       return connectionHelper.Delete(&models.TrelloConnection{}, input)
+       conn := &models.TrelloConnection{}
+       output, err := connectionHelper.Delete(conn, input)
+       if err != nil {
+               return output, err
+       }
+       output.Body = conn.Sanitize()
+       return output, nil
+
 }
 
 // @Summary get all trello connections
@@ -134,6 +178,9 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
+       for idx, c := range connections {
+               connections[idx] = c.Sanitize()
+       }
        return &plugin.ApiResourceOutput{Body: connections, Status: 
http.StatusOK}, nil
 }
 
@@ -147,5 +194,5 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
        connection := &models.TrelloConnection{}
        err := connectionHelper.First(connection, input.Params)
-       return &plugin.ApiResourceOutput{Body: connection}, err
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, err
 }
diff --git a/backend/plugins/trello/impl/impl.go 
b/backend/plugins/trello/impl/impl.go
index 2b2edaaea..8454f9b33 100644
--- a/backend/plugins/trello/impl/impl.go
+++ b/backend/plugins/trello/impl/impl.go
@@ -168,6 +168,9 @@ func (p Trello) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler {
                        "DELETE": api.DeleteConnection,
                        "GET":    api.GetConnection,
                },
+               "connections/:connectionId/test": {
+                       "POST": api.TestExistingConnection,
+               },
                "connections/:connectionId/proxy/rest/*path": {
                        "GET": api.Proxy,
                },
diff --git a/backend/plugins/trello/models/connection.go 
b/backend/plugins/trello/models/connection.go
index 92fc59155..9fce2389a 100644
--- a/backend/plugins/trello/models/connection.go
+++ b/backend/plugins/trello/models/connection.go
@@ -30,12 +30,22 @@ type TrelloConn struct {
        helper.AppKey         `mapstructure:",squash"`
 }
 
+func (tc *TrelloConn) Sanitize() TrelloConn {
+       tc.SecretKey = ""
+       return *tc
+}
+
 // TrelloConnection holds TrelloConn plus ID/Name for database storage
 type TrelloConnection struct {
        helper.BaseConnection `mapstructure:",squash"`
        TrelloConn            `mapstructure:",squash"`
 }
 
+func (connection TrelloConnection) Sanitize() TrelloConnection {
+       connection.TrelloConn = connection.TrelloConn.Sanitize()
+       return connection
+}
+
 // SetupAuthentication sets up the HTTP Request Authentication
 func (tc *TrelloConn) SetupAuthentication(req *http.Request) errors.Error {
        req.Header.Set("Authorization", fmt.Sprintf("OAuth 
oauth_consumer_key=\"%s\", oauth_token=\"%s\"", tc.AppId, tc.SecretKey))
diff --git a/backend/plugins/zentao/api/connection.go 
b/backend/plugins/zentao/api/connection.go
index 8bc9fee04..84168c958 100644
--- a/backend/plugins/zentao/api/connection.go
+++ b/backend/plugins/zentao/api/connection.go
@@ -36,24 +36,15 @@ type ZentaoTestConnResponse struct {
        Connection *models.ZentaoConn
 }
 
-// @Summary test zentao connection
-// @Description Test zentao Connection
-// @Tags plugins/zentao
-// @Param body body models.ZentaoConn true "json body"
-// @Success 200  {object} ZentaoTestConnResponse "Success"
-// @Failure 400  {string} errcode.Error "Bad Request"
-// @Failure 500  {string} errcode.Error "Internal Error"
-// @Router /plugins/zentao/test [POST]
-func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+func testConnection(ctx context.Context, connection models.ZentaoConn) 
(*ZentaoTestConnResponse, errors.Error) {
        // process input
-       var connection models.ZentaoConn
-       err := helper.Decode(input.Body, &connection, vld)
-       if err != nil {
-               return nil, errors.BadInput.Wrap(err, "failed to decode input 
to be zentao connection")
+       if vld != nil {
+               if err := vld.Struct(connection); err != nil {
+                       return nil, errors.Default.Wrap(err, "error validating 
target")
+               }
        }
-
        // try to create apiClient
-       client, err := helper.NewApiClientFromConnection(context.TODO(), 
basicRes, &connection)
+       client, err := helper.NewApiClientFromConnection(ctx, basicRes, 
&connection)
        if err != nil {
                return nil, err
        }
@@ -65,21 +56,67 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if resp.StatusCode != http.StatusOK {
                body.Success = false
                body.Message = err.Error()
-               return &plugin.ApiResourceOutput{Body: body, Status: 
http.StatusBadRequest}, nil
+               return &body, nil
        }
        if connection.DbUrl != "" {
                err = runner.CheckDbConnection(connection.DbUrl, 5*time.Second)
                if err != nil {
                        body.Success = false
                        body.Message = "invalid DbUrl"
-                       return &plugin.ApiResourceOutput{Body: body, Status: 
http.StatusBadRequest}, nil
+                       return &body, nil
                }
        }
        body.Success = true
        body.Message = "success"
+       connection = connection.Sanitize()
        body.Connection = &connection
-       // output
-       return &plugin.ApiResourceOutput{Body: body, Status: 200}, nil
+       return &body, nil
+}
+
+// TestConnection test zentao connection
+// @Summary test zentao connection
+// @Description Test zentao Connection
+// @Tags plugins/zentao
+// @Param body body models.ZentaoConn true "json body"
+// @Success 200  {object} ZentaoTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/zentao/test [POST]
+func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       // process input
+       var connection models.ZentaoConn
+       err := helper.Decode(input.Body, &connection, vld)
+       if err != nil {
+               return nil, errors.BadInput.Wrap(err, "failed to decode input 
to be zentao connection")
+       }
+
+       // test connection
+       result, err := testConnection(context.TODO(), connection)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, 
nil
+}
+
+// TestExistingConnection test zentao connection options
+// @Summary test zentao connection
+// @Description Test zentao Connection
+// @Tags plugins/zentao
+// @Success 200  {object} ZentaoTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/zentao/{connectionId}/test [POST]
+func TestExistingConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connection := &models.ZentaoConnection{}
+       err := connectionHelper.First(connection, input.Params)
+       if err != nil {
+               return nil, errors.BadInput.Wrap(err, "find connection from db")
+       }
+       testConnectionResult, testConnectionErr := 
testConnection(context.TODO(), connection.ZentaoConn)
+       if testConnectionErr != nil {
+               return nil, testConnectionErr
+       }
+       return &plugin.ApiResourceOutput{Body: testConnectionResult, Status: 
http.StatusOK}, nil
 }
 
 // @Summary create zentao connection
@@ -97,7 +134,7 @@ func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: 
http.StatusOK}, nil
 }
 
 // @Summary patch zentao connection
@@ -114,7 +151,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection}, nil
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
 }
 
 // @Summary delete a zentao connection
@@ -126,7 +163,13 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/zentao/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       return connectionHelper.Delete(&models.ZentaoConnection{}, input)
+       conn := &models.ZentaoConnection{}
+       output, err := connectionHelper.Delete(conn, input)
+       if err != nil {
+               return output, err
+       }
+       output.Body = conn.Sanitize()
+       return output, nil
 }
 
 // @Summary get all zentao connections
@@ -142,6 +185,9 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
        if err != nil {
                return nil, err
        }
+       for idx, c := range connections {
+               connections[idx] = c.Sanitize()
+       }
        return &plugin.ApiResourceOutput{Body: connections, Status: 
http.StatusOK}, nil
 }
 
@@ -155,5 +201,5 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
        connection := &models.ZentaoConnection{}
        err := connectionHelper.First(connection, input.Params)
-       return &plugin.ApiResourceOutput{Body: connection}, err
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, err
 }
diff --git a/backend/plugins/zentao/impl/impl.go 
b/backend/plugins/zentao/impl/impl.go
index 9d8f0a6b6..3cbbbf296 100644
--- a/backend/plugins/zentao/impl/impl.go
+++ b/backend/plugins/zentao/impl/impl.go
@@ -253,6 +253,9 @@ func (p Zentao) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler {
                        "PATCH":  api.PatchConnection,
                        "DELETE": api.DeleteConnection,
                },
+               "connections/:connectionId/test": {
+                       "POST": api.TestExistingConnection,
+               },
                "connections/:connectionId/scopes": {
                        "PUT": api.PutProjectScope,
                        "GET": api.GetProjectScopeList,
diff --git a/backend/plugins/zentao/models/connection.go 
b/backend/plugins/zentao/models/connection.go
index 09c28cef6..0d68067f3 100644
--- a/backend/plugins/zentao/models/connection.go
+++ b/backend/plugins/zentao/models/connection.go
@@ -20,6 +20,8 @@ package models
 import (
        "fmt"
        "net/http"
+       "net/url"
+       "strings"
 
        "github.com/apache/incubator-devlake/core/errors"
        helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
@@ -67,12 +69,43 @@ type ZentaoConn struct {
        DbMaxConns     int    `json:"dbMaxConns" mapstructure:"dbMaxConns"`
 }
 
+func (connection ZentaoConn) Sanitize() ZentaoConn {
+       connection.Password = ""
+       if connection.DbUrl != "" {
+               connection.DbUrl = connection.SanitizeDbUrl()
+       }
+       return connection
+}
+
 // ZentaoConnection holds ZentaoConn plus ID/Name for database storage
 type ZentaoConnection struct {
        helper.BaseConnection `mapstructure:",squash"`
        ZentaoConn            `mapstructure:",squash"`
 }
 
+func (connection ZentaoConn) SanitizeDbUrl() string {
+       if connection.DbUrl == "" {
+               return connection.DbUrl
+       }
+       dbUrl := connection.DbUrl
+       u, _ := url.Parse(dbUrl)
+       if u != nil && u.User != nil {
+               password, ok := u.User.Password()
+               if ok {
+                       dbUrl = strings.Replace(dbUrl, password, 
strings.Repeat("*", len(password)), -1)
+               }
+       }
+       if dbUrl == connection.DbUrl {
+               dbUrl = ""
+       }
+       return dbUrl
+}
+
+func (connection ZentaoConnection) Sanitize() ZentaoConnection {
+       connection.ZentaoConn = connection.ZentaoConn.Sanitize()
+       return connection
+}
+
 // This object conforms to what the frontend currently expects.
 type ZentaoResponse struct {
        Name string `json:"name"`


Reply via email to