feat : add non-invasive user access control framework (#2589)

diff --git a/.licenserc.yaml b/.licenserc.yaml
index e6ec0ed..e3cea2b 100644
--- a/.licenserc.yaml
+++ b/.licenserc.yaml
@@ -45,6 +45,7 @@
     - 'api/logs/.gitkeep'
     - 'api/service/apisix-dashboard.service'
     - 'api/VERSION'
+    - 'api/pkg/iam/demo/policy.csv'
 
     # Other files
     - 'go.mod'
diff --git a/api/config/config.yaml b/api/config/config.yaml
index 2142af9..75e990d 100644
--- a/api/config/config.yaml
+++ b/api/config/config.yaml
@@ -65,3 +65,4 @@
       password: user
 
 feature_gate:
+  demoIAMAccess: false
diff --git a/api/internal/config/config.go b/api/internal/config/config.go
index e70d7cf..70d7781 100644
--- a/api/internal/config/config.go
+++ b/api/internal/config/config.go
@@ -82,7 +82,9 @@
 				},
 			},
 		},
-		FeatureGate: FeatureGate{},
+		FeatureGate: FeatureGate{
+			DemoIAMAccess: true,
+		},
 	}
 }
 
diff --git a/api/internal/config/structs.go b/api/internal/config/structs.go
index f5412ee..fe04ee3 100644
--- a/api/internal/config/structs.go
+++ b/api/internal/config/structs.go
@@ -105,4 +105,5 @@
 }
 
 type FeatureGate struct {
+	DemoIAMAccess bool `mapstructure:"demoIAMAccess"`
 }
diff --git a/api/internal/filter/authentication.go b/api/internal/filter/authentication.go
index 90323a6..f8b9d14 100644
--- a/api/internal/filter/authentication.go
+++ b/api/internal/filter/authentication.go
@@ -84,6 +84,7 @@
 			return
 		}
 
+		c.Set("identity", claims.Subject)
 		c.Next()
 	}
 }
diff --git a/api/internal/filter/iam/iam.go b/api/internal/filter/iam/iam.go
new file mode 100644
index 0000000..296d691
--- /dev/null
+++ b/api/internal/filter/iam/iam.go
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package iam
+
+import (
+	"net/http"
+
+	"github.com/gin-gonic/gin"
+
+	"github.com/apache/apisix-dashboard/api/internal/config"
+	iamDef "github.com/apache/apisix-dashboard/api/pkg/iam"
+	"github.com/apache/apisix-dashboard/api/pkg/iam/demo"
+)
+
+var (
+	access     iamDef.Access
+	accessLock bool
+)
+
+func Filter(cfg config.Config) gin.HandlerFunc {
+	// When feature gate demoIAMAccess is configured to be on,
+	// set the access implementation to Demo
+	if cfg.FeatureGate.DemoIAMAccess {
+		access = demo.Access{}
+		accessLock = true
+	}
+
+	return func(c *gin.Context) {
+		if access != nil && c.Request.URL.Path != "/apisix/admin/user/login" {
+			identity := c.MustGet("identity").(string)
+			err := access.Check(identity, c.Request.URL.Path, c.Request.Method)
+			if err != nil {
+				c.AbortWithStatus(http.StatusForbidden)
+				return
+			}
+		}
+
+		c.Next()
+	}
+}
+
+// SetAccessImplementation provides a function that allows developers to replace the built-in access control implementation
+// This function is allowed to be called only once and returns true on success.
+// After setting, the access implementation will be locked and another attempt to set it will return false.
+func SetAccessImplementation(impl iamDef.Access) bool {
+	if accessLock {
+		return false
+	}
+	access = impl
+	accessLock = true
+	return true
+}
diff --git a/api/internal/filter/iam/iam_test.go b/api/internal/filter/iam/iam_test.go
new file mode 100644
index 0000000..1447f56
--- /dev/null
+++ b/api/internal/filter/iam/iam_test.go
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package iam
+
+import (
+	"errors"
+	"github.com/apache/apisix-dashboard/api/internal/config"
+	"github.com/apache/apisix-dashboard/api/pkg/iam"
+	"github.com/gin-gonic/gin"
+	"github.com/stretchr/testify/assert"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+)
+
+var cfg = config.NewDefaultConfig()
+
+func performRequest(r http.Handler, method, path string) *httptest.ResponseRecorder {
+	req := httptest.NewRequest(method, path, nil)
+	w := httptest.NewRecorder()
+	r.ServeHTTP(w, req)
+	return w
+}
+
+func TestFilter(t *testing.T) {
+	r := gin.Default()
+	r.Use(func(c *gin.Context) {
+		c.Set("identity", "user")
+	})
+	r.Use(Filter(cfg))
+	r.POST("/apisix/admin/user/login", func(ctx *gin.Context) {})
+	r.PUT("/apisix/admin/global_rules/:id", func(ctx *gin.Context) {})
+	r.DELETE("/apisix/admin/stream_routes/:ids", func(ctx *gin.Context) {})
+	r.GET("/*path", func(ctx *gin.Context) {})
+	r.POST("/success", func(ctx *gin.Context) {})
+
+	w := performRequest(r, http.MethodPost, "/apisix/admin/user/login")
+	assert.Equal(t, http.StatusOK, w.Code)
+
+	w = performRequest(r, http.MethodDelete, "/apisix/admin/global_rules/12")
+	assert.Equal(t, http.StatusForbidden, w.Code)
+
+	w = performRequest(r, http.MethodDelete, "/apisix/admin/stream_routes/67")
+	assert.Equal(t, http.StatusForbidden, w.Code)
+
+	w = performRequest(r, http.MethodGet, "/apisix/admin/ssl/98")
+	assert.Equal(t, http.StatusOK, w.Code)
+
+	w = performRequest(r, http.MethodPost, "/success")
+	assert.Equal(t, http.StatusOK, w.Code)
+}
+
+type test struct{}
+
+var _ iam.Access = test{}
+
+func (test) Check(identity, resource, action string) error {
+	return errors.New("no permission")
+}
+
+func TestSetAccessImplementation(t *testing.T) {
+	// close the default gate to use the customized one
+	cfg.FeatureGate.DemoIAMAccess = false
+	// because the last test. we should reset the value
+	accessLock = false
+	SetAccessImplementation(test{})
+	r := gin.Default()
+	r.Use(func(c *gin.Context) {
+		c.Set("identity", "user")
+	})
+	r.Use(Filter(cfg))
+	r.POST("/apisix/admin/user/login", func(ctx *gin.Context) {})
+	r.PUT("/apisix/admin/route/:id", func(ctx *gin.Context) {})
+	r.DELETE("/apisix/admin/upstream", func(ctx *gin.Context) {})
+
+	w := performRequest(r, http.MethodPost, "/apisix/admin/user/login")
+	assert.Equal(t, http.StatusOK, w.Code)
+
+	w = performRequest(r, http.MethodPut, "/apisix/admin/route/2")
+	assert.Equal(t, http.StatusForbidden, w.Code)
+
+	w = performRequest(r, http.MethodDelete, "/apisix/admin/upstream")
+	assert.Equal(t, http.StatusForbidden, w.Code)
+}
diff --git a/api/internal/route.go b/api/internal/route.go
index 486004d..b20524c 100644
--- a/api/internal/route.go
+++ b/api/internal/route.go
@@ -31,6 +31,7 @@
 
 	"github.com/apache/apisix-dashboard/api/internal/config"
 	"github.com/apache/apisix-dashboard/api/internal/filter"
+	"github.com/apache/apisix-dashboard/api/internal/filter/iam"
 	"github.com/apache/apisix-dashboard/api/internal/handler"
 	"github.com/apache/apisix-dashboard/api/internal/log"
 )
@@ -43,8 +44,15 @@
 	}
 	r := gin.New()
 	logger := log.GetLogger(log.AccessLog)
+
 	// security
-	r.Use(filter.RequestLogHandler(logger), filter.IPFilter(cfg.Security), filter.InvalidRequest(), filter.Authentication(cfg.Authentication))
+	r.Use(
+		filter.RequestLogHandler(logger),
+		filter.IPFilter(cfg.Security),
+		filter.InvalidRequest(),
+		filter.Authentication(cfg.Authentication),
+		iam.Filter(cfg),
+	)
 
 	// misc
 	staticPath := "./html/"
diff --git a/api/pkg/iam/demo/access.go b/api/pkg/iam/demo/access.go
new file mode 100644
index 0000000..dc939cc
--- /dev/null
+++ b/api/pkg/iam/demo/access.go
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package demo
+
+import (
+	"embed"
+	"errors"
+	"strings"
+
+	"github.com/casbin/casbin/v2"
+	casbinFSAdapter "github.com/naucon/casbin-fs-adapter"
+
+	"github.com/apache/apisix-dashboard/api/pkg/iam"
+)
+
+var (
+	//go:embed model.conf policy.csv
+	fs embed.FS
+
+	// Ensure that demo Access conforms to the iam.Access interface definition
+	_ iam.Access = Access{}
+)
+
+type Access struct{}
+
+func (Access) Check(identity, resource, action string) error {
+	// Load casbin model and adapter from Go embed FS
+	model, _ := casbinFSAdapter.NewModel(fs, "model.conf")
+	policies := casbinFSAdapter.NewAdapter(fs, "policy.csv")
+	policies.LoadPolicy(model)
+	// Create enforcer
+	enforce, err := casbin.NewEnforcer(model, policies)
+	if err != nil {
+		return err
+	}
+	enforce.AddFunction("identify", KeyMatchFunc)
+	// get all the permission the user has
+	pers, _ := enforce.GetImplicitPermissionsForUser("role_admin")
+	admin, _ := enforce.HasRoleForUser(identity, "role_admin")
+	if !admin {
+		for _, v := range pers {
+			if KeyMatch(resource, v[1]) && action == v[2] {
+				return errors.New("no permission")
+			}
+		}
+	}
+
+	// the normal url can be requested
+	return nil
+}
+
+// KeyMatchFunc wrap KeyMatch to meet with casbin's need of custom functions
+func KeyMatchFunc(args ...interface{}) (interface{}, error) {
+	key1, key2 := args[0].(string), args[1].(string)
+	return (bool)(KeyMatch(key1, key2)), nil
+}
+
+// KeyMatch can match three patterns of route /* && /:id && /:id/*
+func KeyMatch(key1 string, key2 string) bool {
+	i, j := strings.Index(key2, ":"), strings.Index(key2, "*")
+	if len(key1) < i+1 {
+		return false
+	}
+	if i != -1 {
+		ok := key1[:i-1] == key2[:i-1]
+		if j != -1 && ok {
+			k, p := strings.Index(key2[i:], "/"), strings.Index(key1[i:], "/")
+			if key2[i+k+1] == '*' {
+				return true
+			}
+			return key2[i+k:j] == key1[i+p:i+p+k+1]
+		}
+		return ok
+	} else if j != -1 {
+		ok := key1[:j-1] == key2[:j-1]
+		if i != -1 && ok {
+			k, p := strings.Index(key2[i:], "/"), strings.Index(key1[i:], "/")
+			if key2[i+k+1] == '*' {
+				return true
+			}
+			return key2[i+k:j] == key1[i+p:i+p+k+1]
+		}
+		return ok
+	}
+	return key1 == key2
+}
diff --git a/api/pkg/iam/demo/model.conf b/api/pkg/iam/demo/model.conf
new file mode 100644
index 0000000..875617a
--- /dev/null
+++ b/api/pkg/iam/demo/model.conf
@@ -0,0 +1,31 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[role_definition]
+g = _, _
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub) && (r.obj == p.obj || identify(r.obj,p.obj)) && r.act == p.act
diff --git a/api/pkg/iam/demo/policy.csv b/api/pkg/iam/demo/policy.csv
new file mode 100644
index 0000000..3a8c406
--- /dev/null
+++ b/api/pkg/iam/demo/policy.csv
@@ -0,0 +1,53 @@
+p,role_admin,/apisix/admin/ssl,POST
+p,role_admin,/apisix/admin/services,POST
+p,role_admin,/apisix/admin/stream_routes,POST
+p,role_admin,/apisix/admin/system_config,POST
+p,role_admin,/apisix/admin/upstreams,POST
+p,role_admin,/apisix/admin/plugin_configs,POST
+p,role_admin,/apisix/admin/proto,POST
+p,role_admin,/apisix/admin/routes,POST
+p,role_admin,/apisix/admin/import/routes,POST
+p,role_admin,/apisix/admin/ssl,PUT
+p,role_admin,/apisix/admin/ssl/:id,PUT
+p,role_admin,/apisix/admin/services,PUT
+p,role_admin,/apisix/admin/services/:id,PUT
+p,role_admin,/apisix/admin/stream_routes,PUT
+p,role_admin,/apisix/admin/stream_routes/:id,PUT
+p,role_admin,/apisix/admin/system_config,PUT
+p,role_admin,/apisix/admin/plugin_configs,PUT
+p,role_admin,/apisix/admin/plugin_configs/:id,PUT
+p,role_admin,/apisix/admin/proto,PUT
+p,role_admin,/apisix/admin/proto/:id,PUT
+p,role_admin,/apisix/admin/routes,PUT
+p,role_admin,/apisix/admin/routes/:id,PUT
+p,role_admin,/apisix/admin/consumers,PUT
+p,role_admin,/apisix/admin/consumers/:username,PUT
+p,role_admin,/apisix/admin/upstreams,PUT
+p,role_admin,/apisix/admin/upstreams/:id,PUT
+p,role_admin,/apisix/admin/global_rules,PUT
+p,role_admin,/apisix/admin/global_rules/:id,PUT
+p,role_admin,/apisix/admin/ssl/:ids,DELETE
+p,role_admin,/apisix/admin/services/:ids,DELETE
+p,role_admin,/apisix/admin/stream_routes/:ids,DELETE
+p,role_admin,/apisix/admin/system_config/:config_name,DELETE
+p,role_admin,/apisix/admin/plugin_configs/:ids,DELETE
+p,role_admin,/apisix/admin/proto/:ids,DELETE
+p,role_admin,/apisix/admin/routes/:ids,DELETE
+p,role_admin,/apisix/admin/consumers/:usernames,DELETE
+p,role_admin,/apisix/admin/upstreams/:ids,DELETE
+p,role_admin,/apisix/admin/global_rules/:id,DELETE
+p,role_admin,/apisix/admin/ssl/:id,PATCH
+p,role_admin,/apisix/admin/ssl/:id/*path,PATCH
+p,role_admin,/apisix/admin/services/:id,PATCH
+p,role_admin,/apisix/admin/services/:id/*path,PATCH
+p,role_admin,/apisix/admin/plugin_configs/:id,PATCH
+p,role_admin,/apisix/admin/plugin_configs/:id/*path,PATCH
+p,role_admin,/apisix/admin/proto/:id,PATCH
+p,role_admin,/apisix/admin/proto/:id/*path,PATCH
+p,role_admin,/apisix/admin/routes/:id,PATCH
+p,role_admin,/apisix/admin/routes/:id/*path,PATCH
+p,role_admin,/apisix/admin/upstreams/:id,PATCH
+p,role_admin,/apisix/admin/upstreams/:id/*path,PATCH
+p,role_admin,/apisix/admin/global_rules/:id,PATCH
+p,role_admin,/apisix/admin/global_rules/:id/*path,PATCH
+g,user_admin,role_admin
diff --git a/api/pkg/iam/interface.go b/api/pkg/iam/interface.go
new file mode 100644
index 0000000..ab5deca
--- /dev/null
+++ b/api/pkg/iam/interface.go
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package iam
+
+// Access interface defines the pattern of functions required for access control in the IAM
+type Access interface {
+	Check(identity, resource, action string) error
+}
diff --git a/api/test/shell/cli_test.sh b/api/test/shell/cli_test.sh
index 2c10463..ebbdba1 100755
--- a/api/test/shell/cli_test.sh
+++ b/api/test/shell/cli_test.sh
@@ -431,6 +431,45 @@
   stop_dashboard 6
 }
 
+#15
+@test "Check FeatureGate effected" {
+  recover_conf
+  start_dashboard 3
+
+  # validate process is right by requesting login api
+  run curl http://127.0.0.1:9000/apisix/admin/user/login -H "Content-Type: application/json" -d '{"username":"user", "password": "user"}'
+  token=$(echo "$output" | sed 's/{/\n/g' | sed 's/,/\n/g' | grep "token" | sed 's/:/\n/g' | sed '1d' | sed 's/}//g'  | sed 's/"//g')
+
+  [ -n "${token}" ]
+
+  # more validation to make sure it's ok to access etcd
+  run curl -ig -XPUT http://127.0.0.1:9000/apisix/admin/consumers -i -H "Content-Type: application/json" -H "Authorization: $token" -d '{"username":"etcd_basic_auth_test"}'
+  respCode=$(echo "$output" | sed 's/{/\n/g'| sed 's/,/\n/g' | grep "code" | sed 's/:/\n/g' | sed '1d')
+
+  [ "$respCode" = "0" ]
+
+  stop_dashboard 6
+
+  recover_conf
+  yq -y '.feature_gate.demoIAMAccess=true' config/config.yaml > config/config.yaml.tmp && mv config/config.yaml.tmp ${CONF_FILE}
+
+  start_dashboard 3
+
+  # validate process is right by requesting login api
+  run curl http://127.0.0.1:9000/apisix/admin/user/login -H "Content-Type: application/json" -d '{"username":"user", "password": "user"}'
+  token=$(echo "$output" | sed 's/{/\n/g' | sed 's/,/\n/g' | grep "token" | sed 's/:/\n/g' | sed '1d' | sed 's/}//g'  | sed 's/"//g')
+
+  [ -n "${token}" ]
+
+  # more validation to make sure it's ok to access etcd
+  run curl -ig -XPUT http://127.0.0.1:9000/apisix/admin/consumers -i -H "Content-Type: application/json" -H "Authorization: $token" -d '{"username":"etcd_basic_auth_test"}'
+  respCode=$(echo "$output" | sed 's/{/\n/g'| sed 's/,/\n/g' | grep "code" | sed 's/:/\n/g' | sed '1d')
+
+  [ "$respCode" != "0" ]
+
+  stop_dashboard 6
+}
+
 #post
 @test "Clean test environment" {
   # kill etcd
diff --git a/docs/en/latest/FAQ.md b/docs/en/latest/FAQ.md
index 52133c0..4ade2eb 100644
--- a/docs/en/latest/FAQ.md
+++ b/docs/en/latest/FAQ.md
@@ -79,8 +79,8 @@
 
 ```yaml
 conf:
-  allow_list:
-    - 0.0.0.0/0
+   allow_list:
+      - 0.0.0.0/0
 ```
 
 2. Allow all IPv6 access
@@ -99,7 +99,7 @@
 
 ```yaml
 conf:
-  allow_list:
+   allow_list:
 ```
 
 Restart `manager-api`, all IPs can access `APISIX Dashboard`.
@@ -135,4 +135,4 @@
 
 If the domain name of the address is configured as HTTPS, the embedded grafana will jump to the login page after logging in. You can refer to this solution:
 
-It's best for Grafana to configure the domain name in the same way. Otherwise there will be problems with address resolution.
+It's best for Grafana to configure the domain name in the same way. Otherwise there will be problems with address resolution.
\ No newline at end of file
diff --git a/docs/en/latest/backend-authentication.md b/docs/en/latest/backend-authentication.md
new file mode 100644
index 0000000..8605aaa
--- /dev/null
+++ b/docs/en/latest/backend-authentication.md
@@ -0,0 +1,45 @@
+### How to use this Non-intrusive framework

+

+- **target**: we can use this framework to achieve adding customized authentication method to dashboard

+- **implementation**: we use middleware to check user permissions. The way to check can be decided by developer, if not, we provide a default way for developers

+- **usage**:

+

+1.we open the switch to adapt the  **default authentication method**:

+

+```shell

+feature_gate:

+  demoIAMAccess:true

+```

+

+​	the default strategy where we use the `casbin` framework to achieve. Also, we can add and delete this route in **`internal/pkg/iam/demo/policy.csv`** that only can be accessed by **admin**

+

+2.Adopt a **customized authentication method**

+

+```shell

+# at first, we should close this switch to support customized authentication method

+feature_gate:

+  demoIAMAcess:false

+```

+

+```go

+// then, we should create struct to implement this interface

+// parameters explanation. identity -> username(user or admin) resource -> url action -> method

+// in the method Check, you can customize some way to authenticate these interviewers

+// if interviewers aren't permitted to request this resource. you can throw an error

+type Access interface {

+    Check(identity, resource, action string) error

+}

+

+type MyAccess struct{}

+func (m MyAccess)Check(identity, resource, action string) error {

+	// customized way

+}

+func main(){

+	// add your customized method into APISIX-DashBoard

+    ok := SetAccessImplementation(MyAccess{})

+	if ok {

+		// add successfully

+    } else {

+		// there is an existing method in dashboard

+}

+```
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 4dba40c..c6edf75 100644
--- a/go.mod
+++ b/go.mod
@@ -2,10 +2,18 @@
 
 go 1.18
 
+replace google.golang.org/grpc => google.golang.org/grpc v1.26.0
+
+replace github.com/coreos/bbolt => go.etcd.io/bbolt v1.3.5
+
 require (
+	github.com/casbin/casbin/v2 v2.36.1
 	github.com/evanphx/json-patch/v5 v5.1.0
 	github.com/gin-contrib/gzip v0.0.3
 	github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e
+	github.com/juliangruber/go-intersect v1.1.0
+	github.com/naucon/casbin-fs-adapter v0.1.0
+	github.com/shiningrush/droplet v0.2.6-0.20210127040147-53817015cd1b
 	github.com/gin-gonic/gin v1.8.1
 	github.com/go-playground/validator/v10 v10.10.0
 	github.com/go-resty/resty/v2 v2.7.0
@@ -13,16 +21,55 @@
 	github.com/pkg/errors v0.9.1
 	github.com/satori/go.uuid v1.2.0
 	github.com/shiningrush/droplet v0.2.6-0.20210127040147-53817015cd1b
+	github.com/shiningrush/droplet/wrapper/gin v0.2.1
 	github.com/sony/sonyflake v1.0.0
 	github.com/spf13/cobra v0.0.3
 	github.com/spf13/viper v1.8.1
 	github.com/stretchr/testify v1.7.1
 	github.com/tidwall/gjson v1.6.7
+	github.com/xeipuuv/gojsonschema v1.2.0
 	github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da
 	go.uber.org/zap v1.22.0
 )
 
 require (
+	github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
+	github.com/coreos/bbolt v1.3.2 // indirect
+	github.com/coreos/etcd v3.3.25+incompatible // indirect
+	github.com/coreos/go-semver v0.3.0 // indirect
+	github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect
+	github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/dustin/go-humanize v1.0.0 // indirect
+	github.com/fsnotify/fsnotify v1.4.9 // indirect
+	github.com/ghodss/yaml v1.0.0 // indirect
+	github.com/gin-contrib/sse v0.1.0 // indirect
+	github.com/go-openapi/jsonpointer v0.19.5 // indirect
+	github.com/go-openapi/swag v0.19.5 // indirect
+	github.com/go-playground/locales v0.13.0 // indirect
+	github.com/go-playground/universal-translator v0.17.0 // indirect
+	github.com/go-playground/validator/v10 v10.3.0 // indirect
+	github.com/gogo/protobuf v1.3.2 // indirect
+	github.com/golang/protobuf v1.5.2 // indirect
+	github.com/google/uuid v1.2.0 // indirect
+	github.com/gorilla/websocket v1.4.2 // indirect
+	github.com/grpc-ecosystem/go-grpc-middleware v1.2.2 // indirect
+	github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
+	github.com/hashicorp/hcl v1.0.0 // indirect
+	github.com/inconshreveable/mousetrap v1.0.0 // indirect
+	github.com/jonboulle/clockwork v0.2.2 // indirect
+	github.com/json-iterator/go v1.1.11 // indirect
+	github.com/leodido/go-urn v1.2.0 // indirect
+	github.com/magiconair/properties v1.8.5 // indirect
+	github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e // indirect
+	github.com/mattn/go-isatty v0.0.12 // indirect
+	github.com/mitchellh/mapstructure v1.4.1 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.1 // indirect
+	github.com/pelletier/go-toml v1.9.3 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/prometheus/client_golang v1.8.0 // indirect
+	github.com/sirupsen/logrus v1.7.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/fsnotify/fsnotify v1.4.9 // indirect
 	github.com/gin-contrib/sse v0.1.0 // indirect
@@ -49,6 +96,25 @@
 	github.com/subosito/gotenv v1.2.0 // indirect
 	github.com/tidwall/match v1.0.3 // indirect
 	github.com/tidwall/pretty v1.0.2 // indirect
+	github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966 // indirect
+	github.com/ugorji/go/codec v1.1.7 // indirect
+	github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
+	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
+	go.uber.org/atomic v1.7.0 // indirect
+	go.uber.org/multierr v1.6.0 // indirect
+	golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
+	golang.org/x/net v0.0.0-20211029224645-99673261e6eb // indirect
+	golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 // indirect
+	golang.org/x/text v0.3.6 // indirect
+	golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
+	google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect
+	google.golang.org/grpc v1.38.0 // indirect
+	google.golang.org/protobuf v1.26.0 // indirect
+	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
+	gopkg.in/ini.v1 v1.62.0 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+	sigs.k8s.io/yaml v1.2.0 // indirect
 	github.com/ugorji/go/codec v1.2.7 // indirect
 	go.uber.org/atomic v1.7.0 // indirect
 	go.uber.org/multierr v1.6.0 // indirect
diff --git a/go.sum b/go.sum
index 9cad34a..99a5f13 100644
--- a/go.sum
+++ b/go.sum
@@ -39,11 +39,34 @@
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw=
+github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
+github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
+github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
+github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
+github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
 github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
+github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
+github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
+github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
+github.com/casbin/casbin/v2 v2.36.1 h1:6b7PQuOEcNR4ZGvQcN82+E1o/n2KMNSUk+np9iryU8A=
+github.com/casbin/casbin/v2 v2.36.1/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
+github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -89,6 +112,16 @@
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
+github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
 github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
 github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
@@ -122,6 +155,7 @@
 github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
 github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -249,6 +283,33 @@
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
+github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
+github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
+github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
+github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
+github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
+github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
+github.com/naucon/casbin-fs-adapter v0.1.0 h1:eCXphqrNmk9o/DhcT8s+840goaqarhbfol/7/nRvxXw=
+github.com/naucon/casbin-fs-adapter v0.1.0/go.mod h1:PB2sx43snq6cf9VHjcCdg2ZEL6bEQKv+pPUu0GK6OyE=
+github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
+github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
+github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
+github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
+github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
+github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
+github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
+github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
+github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
+github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@@ -313,6 +374,9 @@
 github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
 github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU=
 github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966 h1:j6JEOq5QWFker+d7mFQYOhjTZonQ7YkLTHm56dbn+yM=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
 github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
 github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
@@ -325,6 +389,12 @@
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da h1:NimzV1aGyq29m5ukMK0AMWEhFaL/lrEOaephfuoiARg=
 github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA=
+go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
+go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
+go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
+go.etcd.io/etcd v3.3.25+incompatible h1:V1RzkZJj9LqsJRy+TUBgpWSbZXITLB819lstuTFoZOY=
+go.etcd.io/etcd v3.3.25+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI=
 go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
 go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
 go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
@@ -338,6 +408,8 @@
 go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
 go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
 go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
@@ -526,6 +598,8 @@
 golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=