From 209ef1451bf704e603695549ba610d8c1d94eb58 Mon Sep 17 00:00:00 2001 From: wklken Date: Wed, 27 Nov 2024 11:02:17 +0800 Subject: [PATCH] feat: support tenant (#36) --- src/bkauth/.gitignore | 1 + src/bkauth/README.md | 12 +- src/bkauth/build/projects.yaml | 2 +- .../templates/#etc#bkauth_config.yaml | 90 ------------ src/bkauth/cmd/bkauth.go | 7 +- src/bkauth/cmd/cli.go | 5 - src/bkauth/cmd/fixture_init.go | 10 +- src/bkauth/cmd/sync.go | 115 --------------- src/bkauth/config.yaml.tpl | 4 + src/bkauth/pkg/api/app/handler/access_key.go | 34 ++--- src/bkauth/pkg/api/app/handler/app.go | 133 +++++++++++++++++- src/bkauth/pkg/api/app/handler/app_slz.go | 34 ++++- .../pkg/api/app/handler/app_slz_test.go | 81 +++++++++++ src/bkauth/pkg/api/app/router.go | 14 ++ src/bkauth/pkg/api/common/api_allow_list.go | 4 +- src/bkauth/pkg/api/common/middleware.go | 4 +- src/bkauth/pkg/api/common/slz.go | 54 ++++++- src/bkauth/pkg/api/common/validation.go | 4 +- src/bkauth/pkg/api/oauth/handler/oauth_app.go | 2 +- src/bkauth/pkg/cache/impls/access_key.go | 6 +- src/bkauth/pkg/cache/impls/app.go | 52 ++++--- .../config.go => cache/impls/app_exists.go} | 50 +++---- src/bkauth/pkg/cache/impls/app_exists_test.go | 89 ++++++++++++ src/bkauth/pkg/cache/impls/app_test.go | 49 ++----- src/bkauth/pkg/cache/impls/init.go | 13 +- .../pkg/cache/impls/local_access_app.go | 4 +- src/bkauth/pkg/config/config.go | 6 +- src/bkauth/pkg/database/dao/app.go | 82 +++++++++-- src/bkauth/pkg/database/dao/app_test.go | 58 +++++++- src/bkauth/pkg/database/dao/mock/app.go | 38 ++++- src/bkauth/pkg/fixture/access_key.go | 14 +- src/bkauth/pkg/fixture/init.go | 16 ++- .../{sync/util.go => middleware/tenant.go} | 34 ++--- src/bkauth/pkg/server/router.go | 2 + src/bkauth/pkg/service/app.go | 68 +++++++-- src/bkauth/pkg/service/app_test.go | 127 +++++++++++++++++ src/bkauth/pkg/service/mock/app.go | 28 +++- src/bkauth/pkg/service/types/app.go | 2 + src/bkauth/pkg/sync/dao.go | 123 ---------------- src/bkauth/pkg/sync/init.go | 51 ------- src/bkauth/pkg/sync/service.go | 104 -------------- src/bkauth/pkg/sync/sync.go | 122 ---------------- src/bkauth/pkg/util/constant.go | 13 +- src/bkauth/pkg/util/request.go | 8 ++ .../sql_migrations/0004_20241120_1030.sql | 23 +++ .../test/bkauth/apps/create_conflict.bru | 33 +++++ .../apps/create_fail_app_code_invalid.bru | 33 +++++ .../bkauth/apps/create_fail_body_empty.bru | 26 ++++ .../test/bkauth/apps/create_fail_no_auth.bru | 17 +++ .../apps/create_fail_tenant_id_invalid.bru | 33 +++++ .../apps/create_fail_tenant_type_invalid.bru | 33 +++++ src/bkauth/test/bkauth/apps/create_ok.bru | 35 +++++ .../bkauth/apps/get_app_fail_not_exists.bru | 22 +++ src/bkauth/test/bkauth/apps/get_app_ok.bru | 24 ++++ src/bkauth/test/bkauth/apps/list_apps.bru | 23 +++ .../test/bkauth/apps/list_apps_pagination.bru | 28 ++++ .../apps/list_apps_tenant_mode_global.bru | 26 ++++ .../apps/list_apps_tenant_mode_single.bru | 27 ++++ src/bkauth/test/bkauth/bruno.json | 9 ++ .../test/bkauth/environments/dev.bru.tpl | 6 + 60 files changed, 1351 insertions(+), 816 deletions(-) create mode 100644 src/bkauth/.gitignore delete mode 100644 src/bkauth/build/support-files/templates/#etc#bkauth_config.yaml delete mode 100644 src/bkauth/cmd/sync.go create mode 100644 src/bkauth/pkg/api/app/handler/app_slz_test.go rename src/bkauth/pkg/{sync/config.go => cache/impls/app_exists.go} (58%) create mode 100644 src/bkauth/pkg/cache/impls/app_exists_test.go rename src/bkauth/pkg/{sync/util.go => middleware/tenant.go} (62%) delete mode 100644 src/bkauth/pkg/sync/dao.go delete mode 100644 src/bkauth/pkg/sync/init.go delete mode 100644 src/bkauth/pkg/sync/service.go delete mode 100644 src/bkauth/pkg/sync/sync.go create mode 100644 src/bkauth/sql_migrations/0004_20241120_1030.sql create mode 100644 src/bkauth/test/bkauth/apps/create_conflict.bru create mode 100644 src/bkauth/test/bkauth/apps/create_fail_app_code_invalid.bru create mode 100644 src/bkauth/test/bkauth/apps/create_fail_body_empty.bru create mode 100644 src/bkauth/test/bkauth/apps/create_fail_no_auth.bru create mode 100644 src/bkauth/test/bkauth/apps/create_fail_tenant_id_invalid.bru create mode 100644 src/bkauth/test/bkauth/apps/create_fail_tenant_type_invalid.bru create mode 100644 src/bkauth/test/bkauth/apps/create_ok.bru create mode 100644 src/bkauth/test/bkauth/apps/get_app_fail_not_exists.bru create mode 100644 src/bkauth/test/bkauth/apps/get_app_ok.bru create mode 100644 src/bkauth/test/bkauth/apps/list_apps.bru create mode 100644 src/bkauth/test/bkauth/apps/list_apps_pagination.bru create mode 100644 src/bkauth/test/bkauth/apps/list_apps_tenant_mode_global.bru create mode 100644 src/bkauth/test/bkauth/apps/list_apps_tenant_mode_single.bru create mode 100644 src/bkauth/test/bkauth/bruno.json create mode 100644 src/bkauth/test/bkauth/environments/dev.bru.tpl diff --git a/src/bkauth/.gitignore b/src/bkauth/.gitignore new file mode 100644 index 0000000..45e1beb --- /dev/null +++ b/src/bkauth/.gitignore @@ -0,0 +1 @@ +test/bkauth/environments/dev.bru diff --git a/src/bkauth/README.md b/src/bkauth/README.md index d63b4f4..6e7fa2a 100644 --- a/src/bkauth/README.md +++ b/src/bkauth/README.md @@ -7,7 +7,6 @@ note: - depends on database only - use cache, should be fast - ## layers > view(api/*/*.go) -> service -> cache -> dao -> database @@ -20,7 +19,7 @@ note: ## develop -- `go 1.20` required +- `go 1.23` required build and run @@ -58,3 +57,12 @@ make lint # build image make docker-build ``` + +## api testing + +we use [bruno](https://www.usebruno.com/) as the api testing tool. + +1. import `src/bkauth/test/bkauth` into bruno +2. add `cp src/bkauth/test/dev.bru.tpl src/bkauth/test/dev.bru` and update the host + - the `x_bk_app_code: bk_paas3` and `x_bk_app_secret: G3dsdftR9nGQM8WnF1qwjGSVE0ScXrz1hKWM` should be the same in the config.yaml +3. switch the env into dev and test all apis diff --git a/src/bkauth/build/projects.yaml b/src/bkauth/build/projects.yaml index d415d2f..d65d994 100644 --- a/src/bkauth/build/projects.yaml +++ b/src/bkauth/build/projects.yaml @@ -3,5 +3,5 @@ project_dir: bkauth alias: bkauth language: binary - version: 0.0.13 + version: 1.1.0 version_type: ee diff --git a/src/bkauth/build/support-files/templates/#etc#bkauth_config.yaml b/src/bkauth/build/support-files/templates/#etc#bkauth_config.yaml deleted file mode 100644 index 8c831b8..0000000 --- a/src/bkauth/build/support-files/templates/#etc#bkauth_config.yaml +++ /dev/null @@ -1,90 +0,0 @@ -debug: false - -server: - host: __LAN_IP__ - port: __BK_AUTH_PORT__ - - readTimeout: 60 - writeTimeout: 60 - idleTimeout: 180 - -pprofPassword: "__BK_AUTH_PPROF_PASSWORD__" - -# 32 characters,only include uppercase letters or lowercase letters or numbers -# DB Data Encrypt Key which be generated only on first deployment !!! -encryptKey: "__BK_AUTH_ENCRYPT_KEY__" - -crypto: - # contains letters(a-z, A-Z), numbers(0-9), length should be 32 bit - key: "__BK_AUTH_ENCRYPT_KEY__" - # length should be 12 bit - nonce: "__BK_AUTH_ENCRYPT_NONCE__" - -apiAllowLists: - - api: "manage_app" - allowList: "bk_paas,bk_paas3" - - api: "manage_access_key" - allowList: "bk_paas,bk_paas3" - - api: "read_access_key" - allowList: "bk_paas,bk_paas3,bk_apigateway" - - api: "verify_secret" - allowList: "bk_paas,bk_paas3,bk_apigateway,bk_iam,bk_ssm" - -databases: - - id: "bkauth" - host: "__BK_AUTH_MYSQL_HOST__" - port: __BK_AUTH_MYSQL_PORT__ - user: "__BK_AUTH_MYSQL_USER__" - password: "__BK_AUTH_MYSQL_PASSWORD__" - name: "bkauth" - maxOpenConns: 200 - maxIdleConns: 50 - connMaxLifetimeSecond: 600 - - - id: "open_paas" - host: "__BK_PAAS_MYSQL_HOST__" - port: __BK_PAAS_MYSQL_PORT__ - user: "__BK_PAAS_MYSQL_USER__" - password: "__BK_PAAS_MYSQL_PASSWORD__" - name: "open_paas" - -redis: - - id: "__BK_AUTH_REDIS_MODE__" - addr: "__BK_AUTH_REDIS_ADDR__" - password: "__BK_AUTH_REDIS_PASSWORD__" - db: 0 - poolSize: 160 - dialTimeout: 3 - readTimeout: 1 - writeTimeout: 1 - # use comma ”,“ separated when multiple addr - sentinelAddr: "__BK_AUTH_REDIS_SENTINEL_ADDR__" - masterName: "__BK_AUTH_REDIS_SENTINEL_MASTER_NAME__" - sentinelPassword: "__BK_AUTH_REDIS_SENTINEL_PASSWORD__" - -logger: - system: - level: info - encoding: console - writer: file - settings: { name: bkauth.log, size: 100, backups: 10, age: 7, path: __BK_HOME__/logs/bkauth/ } - api: - level: info - encoding: json - writer: file - settings: {name: bkauth_api.log, size: 100, backups: 10, age: 7, path: __BK_HOME__/logs/bkauth/ } - sql: - level: debug - encoding: json - writer: file - settings: {name: bkauth_sql.log, size: 100, backups: 10, age: 7, path: __BK_HOME__/logs/bkauth/ } - audit: - level: info - encoding: json - writer: file - settings: {name: bkauth_audit.log, size: 500, backups: 20, age: 365, path: __BK_HOME__/logs/bkauth/ } - web: - level: info - encoding: json - writer: file - settings: {name: bkauth_web.log, size: 100, backups: 10, age: 7, path: __BK_HOME__/logs/bkauth/ } diff --git a/src/bkauth/cmd/bkauth.go b/src/bkauth/cmd/bkauth.go index eb44867..a481d75 100644 --- a/src/bkauth/cmd/bkauth.go +++ b/src/bkauth/cmd/bkauth.go @@ -21,11 +21,9 @@ package cmd import ( "context" "fmt" - "math/rand" "os" "os/signal" "syscall" - "time" _ "github.com/go-sql-driver/mysql" "github.com/spf13/cobra" @@ -70,10 +68,6 @@ func Execute() { func Start() { fmt.Println("It's BKAuth") - // init rand - // nolint - rand.Seed(time.Now().UnixNano()) - // 0. init config if cfgFile != "" { // Use config file from the flag. @@ -85,6 +79,7 @@ func Start() { if globalConfig.Debug { fmt.Println(globalConfig) } + fmt.Printf("enableMultiTenantMode: %v\n", globalConfig.EnableMultiTenantMode) // 1. init initLogger() diff --git a/src/bkauth/cmd/cli.go b/src/bkauth/cmd/cli.go index 1a8bc36..82d58ce 100644 --- a/src/bkauth/cmd/cli.go +++ b/src/bkauth/cmd/cli.go @@ -20,8 +20,6 @@ package cmd import ( "fmt" - "math/rand" - "time" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -96,9 +94,6 @@ func init() { func cliStart() { fmt.Println("cli start!") - // init rand - // nolint - rand.Seed(time.Now().UnixNano()) // 0. init config if cfgFile != "" { diff --git a/src/bkauth/cmd/fixture_init.go b/src/bkauth/cmd/fixture_init.go index 9d532e0..2ca3db7 100644 --- a/src/bkauth/cmd/fixture_init.go +++ b/src/bkauth/cmd/fixture_init.go @@ -1,6 +1,6 @@ /* * TencentBlueKing is pleased to support the open source community by making - * 蓝鲸智云 - Auth服务(BlueKing - Auth) available. + * 蓝鲸智云 - Auth 服务 (BlueKing - Auth) available. * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -20,8 +20,6 @@ package cmd import ( "fmt" - "math/rand" - "time" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -51,10 +49,6 @@ func init() { } func FixtureInitStart() { - // init rand - // nolint - rand.Seed(time.Now().UnixNano()) - // 0. init config if cfgFile != "" { // Use config file from the flag. @@ -66,6 +60,7 @@ func FixtureInitStart() { if globalConfig.Debug { fmt.Println(globalConfig) } + fmt.Printf("enableMultiTenantMode: %v\n", globalConfig.EnableMultiTenantMode) initLogger() initDatabase() @@ -73,6 +68,7 @@ func FixtureInitStart() { initCaches() initCryptos() + // 这里跟运维确认过,初始化的都是蓝鲸基础服务的数据,保持简单,由 bkauth 配置默认的 tenant_id fixture.InitFixture(globalConfig) // flush logger diff --git a/src/bkauth/cmd/sync.go b/src/bkauth/cmd/sync.go deleted file mode 100644 index ba43364..0000000 --- a/src/bkauth/cmd/sync.go +++ /dev/null @@ -1,115 +0,0 @@ -/* - * TencentBlueKing is pleased to support the open source community by making - * 蓝鲸智云 - Auth服务(BlueKing - Auth) available. - * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. - * Licensed under the MIT License (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * 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. - * - * We undertake not to change the open source license (MIT license) applicable - * to the current version of the project delivered to anyone in the future. - */ - -package cmd - -import ( - "fmt" - "math/rand" - "time" - - "github.com/spf13/cobra" - "github.com/spf13/viper" - "go.uber.org/zap" - - "bkauth/pkg/logging" - "bkauth/pkg/sync" -) - -var openPaaSConfig *sync.OpenPaaSConfig - -var syncCmd = &cobra.Command{ - Use: "sync", - Short: "Sync AppCode/AppSecret", - Long: ``, - Run: func(cmd *cobra.Command, args []string) { - SyncStart() - }, -} - -func init() { - syncCmd.Flags().StringVarP(&cfgFile, "config", "c", "", "config file (default is config.yml;required)") - syncCmd.PersistentFlags().Bool("viper", true, "Use Viper for configuration") - - syncCmd.MarkFlagRequired("config") - - rootCmd.AddCommand(syncCmd) -} - -// initOpenPaaSConfig reads in config file and ENV variables if set. -func initOpenPaaSConfig() { - if cfgFile == "" { - panic("Config file missing") - } - // Use config file from the flag. - // viper.SetConfigFile(cfgFile) - // If a config file is found, read it in. - if err := viper.ReadInConfig(); err != nil { - panic(fmt.Sprintf("Using config file: %s, read fail: err=%v", viper.ConfigFileUsed(), err)) - } - var err error - openPaaSConfig, err = sync.LoadConfig(viper.GetViper()) - if err != nil { - panic(fmt.Sprintf("Could not load configurations from file, error: %v", err)) - } -} - -func initOpenPaaSDatabase() { - openPaaSDBConfig, ok := openPaaSConfig.DatabaseMap["open_paas"] - if !ok { - panic("database open_paas should be configured") - } - - sync.InitOpenPaaSDBClients(&openPaaSDBConfig) - zap.S().Info("init OpenPaaS Database success") -} - -func SyncStart() { - // init rand - // nolint - rand.Seed(time.Now().UnixNano()) - - // 0. init config - if cfgFile != "" { - // Use config file from the flag. - zap.S().Infof("Load config file: %s", cfgFile) - viper.SetConfigFile(cfgFile) - } - initConfig() - initOpenPaaSConfig() - - if globalConfig.Debug { - fmt.Println(globalConfig) - } - - initLogger() - initDatabase() - initRedis() - initCaches() - initCryptos() - - initOpenPaaSDatabase() - - // 同步 - sync.Sync() - - // flush logger - logging.SyncAll() - - fmt.Println("sync finish!") -} diff --git a/src/bkauth/config.yaml.tpl b/src/bkauth/config.yaml.tpl index ad395fd..4b89e2b 100644 --- a/src/bkauth/config.yaml.tpl +++ b/src/bkauth/config.yaml.tpl @@ -1,5 +1,8 @@ debug: true +# enableMultiTenantMode: false +enableMultiTenantMode: true + server: host: 127.0.0.1 port: 9000 @@ -23,6 +26,7 @@ crypto: accessKeys: bkauth: "G3dsdftR9nGQM8WnF1qwjGSVE0ScXrz1hKWM" + bk_paas3: "G3dsdftR9nGQM8WnF1qwjGSVE0ScXrz1hKWM" apiAllowLists: - api: "manage_app" diff --git a/src/bkauth/pkg/api/app/handler/access_key.go b/src/bkauth/pkg/api/app/handler/access_key.go index fdb119c..87c0a76 100644 --- a/src/bkauth/pkg/api/app/handler/access_key.go +++ b/src/bkauth/pkg/api/app/handler/access_key.go @@ -1,6 +1,6 @@ /* * TencentBlueKing is pleased to support the open source community by making - * 蓝鲸智云 - Auth服务(BlueKing - Auth) available. + * 蓝鲸智云 - Auth 服务 (BlueKing - Auth) available. * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -45,11 +45,11 @@ import ( func CreateAccessKey(c *gin.Context) { errorWrapf := errorx.NewLayerFunctionErrorWrapf("Handler", "CreateAccessKey") - // NOTE: 通过API创建, 不支持指定app_secret + // NOTE: 通过 API 创建,不支持指定 app_secret createdSource := util.GetAccessAppCode(c) - // TODO: 统一考虑,如何避免获取URL参数的代码重复 - // 获取URL参数 + // TODO: 统一考虑,如何避免获取 URL 参数的代码重复 + // 获取 URL 参数 var uriParams common.AppCodeSerializer if err := c.ShouldBindUri(&uriParams); err != nil { util.BadRequestErrorJSONResponse(c, util.ValidationErrorMessage(err)) @@ -57,7 +57,7 @@ func CreateAccessKey(c *gin.Context) { } appCode := uriParams.AppCode - // 创建Secret + // 创建 Secret svc := service.NewAccessKeyService() accessKey, err := svc.Create(appCode, createdSource) if err != nil { @@ -73,7 +73,7 @@ func CreateAccessKey(c *gin.Context) { return } - // 缓存里删除appCode的所有Secret + // 缓存里删除 appCode 的所有 Secret cacheImpls.DeleteAccessKey(appCode) util.SuccessJSONResponse(c, "ok", accessKey) @@ -96,7 +96,7 @@ func CreateAccessKey(c *gin.Context) { func DeleteAccessKey(c *gin.Context) { errorWrapf := errorx.NewLayerFunctionErrorWrapf("Handler", "DeleteAccessKey") - // TODO: 校验secret创建来源与删除来源是否一致,只有创建者才可以删除???目前只有PaaS可以管理,即增删 + // TODO: 校验 secret 创建来源与删除来源是否一致,只有创建者才可以删除???目前只有 PaaS 可以管理,即增删 // source := util.GetAccessAppCode(c) var uriParams accessKeyAndAppSerializer @@ -107,7 +107,7 @@ func DeleteAccessKey(c *gin.Context) { appCode := uriParams.AppCode accessKeyID := uriParams.AccessKeyID - // 删除Secret + // 删除 Secret svc := service.NewAccessKeyService() err := svc.DeleteByID(appCode, accessKeyID) if err != nil { @@ -123,7 +123,7 @@ func DeleteAccessKey(c *gin.Context) { return } - // 缓存里删除appCode的所有Secret + // 缓存里删除 appCode 的所有 Secret cacheImpls.DeleteAccessKey(appCode) util.SuccessJSONResponse(c, "ok", nil) @@ -143,7 +143,7 @@ func DeleteAccessKey(c *gin.Context) { // @Header 200 {string} X-Request-Id "the request id" // @Router /api/v1/apps/{bk_app_code}/access-keys [get] func ListAccessKey(c *gin.Context) { - // 获取URL参数 + // 获取 URL 参数 var uriParams common.AppCodeSerializer if err := c.ShouldBindUri(&uriParams); err != nil { util.BadRequestErrorJSONResponse(c, util.ValidationErrorMessage(err)) @@ -151,7 +151,7 @@ func ListAccessKey(c *gin.Context) { } appCode := uriParams.AppCode - // 创建Secret + // 创建 Secret svc := service.NewAccessKeyService() accessKeys, err := svc.ListWithCreatedAtByAppCode(appCode) if err != nil { @@ -178,7 +178,7 @@ func ListAccessKey(c *gin.Context) { // @Header 200 {string} X-Request-Id "the request id" // @Router /api/v1/apps/{bk_app_code}/access-keys/verify [post] func VerifyAccessKey(c *gin.Context) { - // 获取URL参数 + // 获取 URL 参数 var uriParams common.AppCodeSerializer if err := c.ShouldBindUri(&uriParams); err != nil { util.BadRequestErrorJSONResponse(c, util.ValidationErrorMessage(err)) @@ -202,7 +202,7 @@ func VerifyAccessKey(c *gin.Context) { data := gin.H{"is_match": exists} if !exists { - // Note: 这里校验不通过,是业务逻辑,并非接口通讯的认证和鉴权,所以不能返回 401或403 状态码 + // Note: 这里校验不通过,是业务逻辑,并非接口通讯的认证和鉴权,所以不能返回 401 或 403 状态码 util.SuccessJSONResponse(c, "bk_app_code or bk_app_secret invalid", data) return } @@ -227,7 +227,7 @@ func VerifyAccessKey(c *gin.Context) { // @Router /api/v1/apps/{bk_app_code}/access-keys/{access_key_id} [put] func UpdateAccessKey(c *gin.Context) { errorWrapf := errorx.NewLayerFunctionErrorWrapf("Handler", "PutAccessKey") - // 获取URL参数 + // 获取 URL 参数 var uriParams accessKeyAndAppSerializer if err := c.ShouldBindUri(&uriParams); err != nil { util.BadRequestErrorJSONResponse(c, util.ValidationErrorMessage(err)) @@ -243,9 +243,9 @@ func UpdateAccessKey(c *gin.Context) { return } - // 更新accessKey + // 更新 accessKey - // 获取更新的updateFiledMap:如果是空则不更新 + // 获取更新的 updateFiledMap:如果是空则不更新 var updateFiledMap map[string]interface{} err := mapstructure.Decode(body, &updateFiledMap) if err != nil { @@ -267,7 +267,7 @@ func UpdateAccessKey(c *gin.Context) { return } - // 缓存里删除appCode的所有Secret + // 缓存里删除 appCode 的所有 Secret _ = cacheImpls.DeleteAccessKey(uriParams.AppCode) util.SuccessJSONResponse(c, "ok", nil) diff --git a/src/bkauth/pkg/api/app/handler/app.go b/src/bkauth/pkg/api/app/handler/app.go index c150d97..c8cd2da 100644 --- a/src/bkauth/pkg/api/app/handler/app.go +++ b/src/bkauth/pkg/api/app/handler/app.go @@ -1,6 +1,6 @@ /* * TencentBlueKing is pleased to support the open source community by making - * 蓝鲸智云 - Auth服务(BlueKing - Auth) available. + * 蓝鲸智云 - Auth 服务 (BlueKing - Auth) available. * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -43,7 +43,7 @@ import ( // @Header 200 {string} X-Request-Id "the request id" // @Router /api/v1/apps [post] func CreateApp(c *gin.Context) { - // NOTE: 通过API创建, 不支持指定app_secret,默认自动创建对应的app_secret + // NOTE: 通过 API 创建,不支持指定 app_secret,默认自动创建对应的 app_secret var body createAppSerializer if err := c.ShouldBindJSON(&body); err != nil { util.BadRequestErrorJSONResponse(c, util.ValidationErrorMessage(err)) @@ -54,6 +54,19 @@ func CreateApp(c *gin.Context) { util.BadRequestErrorJSONResponse(c, err.Error()) return } + + // extra validate for tenant_id + if !util.GetEnableMultiTenantMode(c) { + if body.Tenant.Mode != util.TenantModeSingle { + util.BadRequestErrorJSONResponse(c, "tenant_mode must be `single` in single tenant mode") + return + } + if body.Tenant.ID != util.TenantIDDefault { + util.BadRequestErrorJSONResponse(c, "tenant_id must be `default` in single tenant mode") + return + } + } + // check app code/name is unique if err := checkAppCreateUnique(body.AppCode, body.Name); err != nil { util.ConflictJSONResponse(c, err.Error()) @@ -64,12 +77,14 @@ func CreateApp(c *gin.Context) { Code: body.AppCode, Name: body.Name, Description: body.Description, + TenantMode: body.Tenant.Mode, + TenantID: body.Tenant.ID, } // 获取请求的来源 createdSource := util.GetAccessAppCode(c) svc := service.NewAppService() - // Note: 兼容PaaS2双写DB和bkauth时AppSecret已经从AppEngine生成,需要支持带Secret的App创建 + // Note: 兼容 PaaS2 双写 DB 和 bkauth 时 AppSecret 已经从 AppEngine 生成,需要支持带 Secret 的 App 创建 if body.AppSecret != "" { err := svc.CreateWithSecret(app, body.AppSecret, createdSource) if err != nil { @@ -87,8 +102,114 @@ func CreateApp(c *gin.Context) { } } - // 由于应用在创建前可能调用相关接口查询,导致`是否存在该App`的查询已被缓存,若不删除缓存,则创建后在缓存未实现前,还是会出现app不存在的 - cacheImpls.DeleteApp(app.Code) + // 由于应用在创建前可能调用相关接口查询,导致`是否存在该App/app基本信息`的查询已被缓存,若不删除缓存,则创建后在缓存未实现前,还是会出现 app 不存在的 + cacheImpls.DeleteAppCache(app.Code) + + data := common.AppResponse{ + AppCode: app.Code, + Name: app.Name, + Description: app.Description, + Tenant: common.TenantResponse{ + ID: app.TenantID, + Mode: app.TenantMode, + }, + } + + util.SuccessJSONResponse(c, "ok", data) +} + +// GetApp godoc +// @Summary get app +// @Description gets an app by app_code +// @ID api-app-get +// @Tags app +// @Accept json +// @Produce json +// @Param X-BK-APP-CODE header string true "app_code" +// @Param X-BK-APP-SECRET header string true "app_secret" +// @Param app_code path string true "App Code" +// @Success 200 {object} util.Response{data=common.AppResponse} +// @Header 200 {string} X-Request-Id "the request id" +// @Router /api/v1/apps/{bk_app_code} [get] +func GetApp(c *gin.Context) { + // 获取 URL 参数 + var uriParams common.AppCodeSerializer + if err := c.ShouldBindUri(&uriParams); err != nil { + util.BadRequestErrorJSONResponse(c, util.ValidationErrorMessage(err)) + return + } + appCode := uriParams.AppCode + + app, err := cacheImpls.GetApp(appCode) + if err != nil { + err = errorx.Wrapf(err, "Handler", "GetApp", "cacheImpls.GetApp appCode=`%s` fail", appCode) + util.SystemErrorJSONResponse(c, err) + return + } + + data := common.AppResponse{ + AppCode: app.Code, + Name: app.Name, + Description: app.Description, + Tenant: common.TenantResponse{ + ID: app.TenantID, + Mode: app.TenantMode, + }, + } + + util.SuccessJSONResponse(c, "ok", data) +} + +// ListApp godoc +// @Summary list apps +// @Description lists apps with optional query parameters +// @ID api-app-list +// @Tags app +// @Accept json +// @Produce json +// @Param tenant_mode query string false "Tenant Type" +// @Param tenant_id query string false "Tenant ID" +// @Param page query int false "Page number" +// @Param page_size query int false "Page size" +// @Success 200 {object} util.Response{data=common.PaginatedResponse{results=[]common.AppResponse}} +// @Header 200 {string} X-Request-Id "the request id" +// @Router /api/v1/apps [get] +func ListApp(c *gin.Context) { + var query listAppSerializer + if err := c.ShouldBindQuery(&query); err != nil { + util.BadRequestErrorJSONResponse(c, util.ValidationErrorMessage(err)) + return + } + + svc := service.NewAppService() + total, apps, err := svc.List( + query.TenantMode, + query.TenantID, + query.GetPage(), + query.GetPageSize(), + query.OrderBy, + query.OrderByDirection) + if err != nil { + err = errorx.Wrapf(err, "Handler", "ListApp", "svc.List fail") + util.SystemErrorJSONResponse(c, err) + return + } + + results := make([]common.AppResponse, 0, len(apps)) + for _, app := range apps { + results = append(results, common.AppResponse{ + AppCode: app.Code, + Name: app.Name, + Description: app.Description, + Tenant: common.TenantResponse{ + ID: app.TenantID, + Mode: app.TenantMode, + }, + }) + } - util.SuccessJSONResponse(c, "ok", common.AppResponse{AppCode: app.Code}) + util.SuccessJSONResponse(c, "ok", common.PaginatedResponse{ + Count: total, + Results: results, + }) } diff --git a/src/bkauth/pkg/api/app/handler/app_slz.go b/src/bkauth/pkg/api/app/handler/app_slz.go index c3223e5..0fbb8ff 100644 --- a/src/bkauth/pkg/api/app/handler/app_slz.go +++ b/src/bkauth/pkg/api/app/handler/app_slz.go @@ -19,16 +19,44 @@ package handler import ( + "errors" + "bkauth/pkg/api/common" + "bkauth/pkg/util" ) +type tenantSerializer struct { + Mode string `json:"mode" binding:"required,oneof=global single" example:"single"` + ID string `json:"id" binding:"omitempty,max=32" example:"default"` +} + type createAppSerializer struct { common.AppCodeSerializer - AppSecret string `json:"bk_app_secret" binding:"omitempty,max=128" example:"bk_paas"` - Name string `json:"name" binding:"required" example:"BK PaaS"` - Description string `json:"description" binding:"omitempty" example:"Platform as A Service"` + AppSecret string `json:"bk_app_secret" binding:"omitempty,max=128" example:"bk_paas"` + Name string `json:"name" binding:"required" example:"BK PaaS"` + Description string `json:"description" binding:"omitempty" example:"Platform as A Service"` + Tenant tenantSerializer `json:"bk_tenant" binding:"required"` } func (s *createAppSerializer) validate() error { + if s.Tenant.Mode == util.TenantModeGlobal { + if s.Tenant.ID != "" { + return errors.New("bk_tenant.id should be empty when tenant_mode is global") + } + } else { + if !common.ValidTenantIDRegex.MatchString(s.Tenant.ID) { + return common.ErrInvalidTenantID + } + } + return s.ValidateAppCode() } + +type listAppSerializer struct { + common.PageParamSerializer + TenantMode string `form:"tenant_mode" binding:"omitempty,oneof=global single" example:"single"` + TenantID string `form:"tenant_id" binding:"omitempty,max=32" example:"default"` + // nolint:lll + OrderBy string `form:"order_by" binding:"omitempty,oneof=code name created_at updated_at" example:"created_at"` + OrderByDirection string `form:"order_by_direction" binding:"omitempty,oneof=asc desc" example:"asc"` +} diff --git a/src/bkauth/pkg/api/app/handler/app_slz_test.go b/src/bkauth/pkg/api/app/handler/app_slz_test.go new file mode 100644 index 0000000..80ebed7 --- /dev/null +++ b/src/bkauth/pkg/api/app/handler/app_slz_test.go @@ -0,0 +1,81 @@ +package handler + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "bkauth/pkg/api/common" + "bkauth/pkg/util" +) + +func TestCreateAppSerializer_Validate(t *testing.T) { + tests := []struct { + name string + serializer createAppSerializer + wantErr bool + errMsg string + }{ + { + name: "tenant_mode=global and tenant_id not empty", + serializer: createAppSerializer{ + Tenant: tenantSerializer{ + Mode: util.TenantModeGlobal, + ID: "some_id", + }, + }, + wantErr: true, + errMsg: "bk_tenant.id should be empty when tenant_mode is global", + }, + { + name: "tenant_mode=single and tenant_id not valid", + serializer: createAppSerializer{ + Tenant: tenantSerializer{ + Mode: util.TenantModeSingle, + ID: "123", + }, + }, + wantErr: true, + errMsg: common.ErrInvalidTenantID.Error(), + }, + { + name: "tenant_id tenant_mode valid, but app_code not valid", + serializer: createAppSerializer{ + Tenant: tenantSerializer{ + Mode: util.TenantModeSingle, + ID: "valid-id", + }, + AppCodeSerializer: common.AppCodeSerializer{ + AppCode: "==1", + }, + }, + wantErr: true, + errMsg: common.ErrInvalidAppCode.Error(), + }, + { + name: "all valid", + serializer: createAppSerializer{ + Tenant: tenantSerializer{ + Mode: util.TenantModeSingle, + ID: "valid-id", + }, + AppCodeSerializer: common.AppCodeSerializer{ + AppCode: "valid_app_code", + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.serializer.validate() + if tt.wantErr { + assert.Error(t, err) + assert.Equal(t, tt.errMsg, err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/src/bkauth/pkg/api/app/router.go b/src/bkauth/pkg/api/app/router.go index 9a0c84c..c37e7c5 100644 --- a/src/bkauth/pkg/api/app/router.go +++ b/src/bkauth/pkg/api/app/router.go @@ -28,8 +28,22 @@ import ( // Register ... func Register(r *gin.RouterGroup) { // App CURD for PaaS + + // Create app r.POST("", common.NewAPIAllowMiddleware(common.ManageAppAPI), handler.CreateApp) + // List app + r.GET("", common.NewAPIAllowMiddleware(common.ManageAppAPI), handler.ListApp) + + // Question: the bkauth would not respect the x-bk-tenant-id header? including the accessKeys api? + // while all the caller are belong to blueking, which are all tenant_scope = * + app := r.Group("/:bk_app_code") + app.Use(common.NewAPIAllowMiddleware(common.ManageAppAPI)) + app.Use(common.AppCodeExists()) + { + app.GET("", handler.GetApp) + } + // AppSecret accessKey := r.Group("/:bk_app_code/access-keys") accessKey.Use(common.AppCodeExists()) diff --git a/src/bkauth/pkg/api/common/api_allow_list.go b/src/bkauth/pkg/api/common/api_allow_list.go index 46779fa..bb5dda5 100644 --- a/src/bkauth/pkg/api/common/api_allow_list.go +++ b/src/bkauth/pkg/api/common/api_allow_list.go @@ -1,6 +1,6 @@ /* * TencentBlueKing is pleased to support the open source community by making - * 蓝鲸智云 - Auth服务(BlueKing - Auth) available. + * 蓝鲸智云 - Auth 服务 (BlueKing - Auth) available. * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -41,7 +41,7 @@ func InitAPIAllowList(cfgs []config.APIAllowList) { } for api, al := range apiAllowListMap { - // 分隔出每个app_code + // 分隔出每个 app_code allowList := strings.Split(al, ",") // 去除空的,避免校验时空字符串被通过 diff --git a/src/bkauth/pkg/api/common/middleware.go b/src/bkauth/pkg/api/common/middleware.go index 0b0d964..a2e019d 100644 --- a/src/bkauth/pkg/api/common/middleware.go +++ b/src/bkauth/pkg/api/common/middleware.go @@ -1,6 +1,6 @@ /* * TencentBlueKing is pleased to support the open source community by making - * 蓝鲸智云 - Auth服务(BlueKing - Auth) available. + * 蓝鲸智云 - Auth 服务 (BlueKing - Auth) available. * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -79,7 +79,7 @@ func TargetExistsAndClientValid() gin.HandlerFunc { } targetID := uriParams.TargetID - // Note: 这里没必要缓存,因为本身Target的注册和变更频率很低 + // Note: 这里没必要缓存,因为本身 Target 的注册和变更频率很低 svc := service.NewTargetService() target, err := svc.Get(targetID) if err != nil { diff --git a/src/bkauth/pkg/api/common/slz.go b/src/bkauth/pkg/api/common/slz.go index 0494a8a..c244e37 100644 --- a/src/bkauth/pkg/api/common/slz.go +++ b/src/bkauth/pkg/api/common/slz.go @@ -1,6 +1,6 @@ /* * TencentBlueKing is pleased to support the open source community by making - * 蓝鲸智云 - Auth服务(BlueKing - Auth) available. + * 蓝鲸智云 - Auth 服务 (BlueKing - Auth) available. * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -23,12 +23,22 @@ import ( "regexp" ) +const ( + // 默认分页大小 + DefaultPageSize = 10 +) + var ( - // ValidAppCodeRegex 小写字母或数字开头, 可以包含小写字母/数字/下划线/连字符 + // ValidAppCodeRegex 小写字母或数字开头,可以包含小写字母/数字/下划线/连字符 ValidAppCodeRegex = regexp.MustCompile("^[a-z0-9][a-z0-9_-]{0,31}$") ErrInvalidAppCode = errors.New("invalid app_code: app_code should begin with a lowercase letter or numbers, " + "contains lowercase letters(a-z), numbers(0-9), underline(_) or hyphen(-), length should be 1 to 32 letters") + + // 租户相关验证 + ValidTenantIDRegex = regexp.MustCompile("^[a-z][a-z0-9-]{1,30}[a-z0-9]$") + ErrInvalidTenantID = errors.New("invalid bk_tenant.id: bk_tenant_id should begin with a letter, " + + "contains letters(a-zA-Z), numbers(0-9) or hyphen(-), length should be 2 to 32") ) type AppCodeSerializer struct { @@ -36,18 +46,54 @@ type AppCodeSerializer struct { } func (s *AppCodeSerializer) ValidateAppCode() error { - // app_code的规则是: - // 由小写英文字母、连接符(-)、下划线(_)或数字组成,长度为[1~32]个字符, 并且以字母或数字开头 (^[a-z0-9][a-z0-9_-]{0,31}$) + // app_code 的规则是: + // 由小写英文字母、连接符 (-)、下划线 (_) 或数字组成,长度为 [1~32] 个字符,并且以字母或数字开头 (^[a-z0-9][a-z0-9_-]{0,31}$) if !ValidAppCodeRegex.MatchString(s.AppCode) { return ErrInvalidAppCode } return nil } +type PageParamSerializer struct { + Page int `form:"page" binding:"omitempty,min=1" example:"1"` + PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" example:"10"` +} + +func (p *PageParamSerializer) GetPage() int { + if p.Page == 0 { + return 1 + } + return p.Page +} + +func (p *PageParamSerializer) GetPageSize() int { + if p.PageSize == 0 { + return DefaultPageSize + } + return p.PageSize +} + +type TenantResponse struct { + Mode string `json:"mode"` + ID string `json:"id"` +} + type AppResponse struct { + AppCode string `json:"bk_app_code"` + Name string `json:"name"` + Description string `json:"description"` + Tenant TenantResponse `json:"bk_tenant"` +} + +type OAuthAppResponse struct { AppCode string `json:"bk_app_code"` } type TargetIDSerializer struct { TargetID string `uri:"target_id" json:"target_id" binding:"required,min=3,max=16" example:"bk_ci"` } + +type PaginatedResponse struct { + Count int `json:"count"` + Results interface{} `json:"results"` +} diff --git a/src/bkauth/pkg/api/common/validation.go b/src/bkauth/pkg/api/common/validation.go index 817fd26..658f995 100644 --- a/src/bkauth/pkg/api/common/validation.go +++ b/src/bkauth/pkg/api/common/validation.go @@ -1,6 +1,6 @@ /* * TencentBlueKing is pleased to support the open source community by making - * 蓝鲸智云 - Auth服务(BlueKing - Auth) available. + * 蓝鲸智云 - Auth 服务 (BlueKing - Auth) available. * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -24,7 +24,7 @@ import ( ) const ( - // 小写字母开头, 可以包含小写字母/数字/下划线/连字符 + // 小写字母开头,可以包含小写字母/数字/下划线/连字符 validIDString = "^[a-z]+[a-z0-9_-]*$" ) diff --git a/src/bkauth/pkg/api/oauth/handler/oauth_app.go b/src/bkauth/pkg/api/oauth/handler/oauth_app.go index e780f13..4c788d5 100644 --- a/src/bkauth/pkg/api/oauth/handler/oauth_app.go +++ b/src/bkauth/pkg/api/oauth/handler/oauth_app.go @@ -80,7 +80,7 @@ func CreateOAuthApp(c *gin.Context) { return } - util.SuccessJSONResponse(c, "ok", common.AppResponse{AppCode: oauthApp.AppCode}) + util.SuccessJSONResponse(c, "ok", common.OAuthAppResponse{AppCode: oauthApp.AppCode}) } // UpdateOAuthApp godoc diff --git a/src/bkauth/pkg/cache/impls/access_key.go b/src/bkauth/pkg/cache/impls/access_key.go index fec1487..ebe7464 100644 --- a/src/bkauth/pkg/cache/impls/access_key.go +++ b/src/bkauth/pkg/cache/impls/access_key.go @@ -1,6 +1,6 @@ /* * TencentBlueKing is pleased to support the open source community by making - * 蓝鲸智云 - Auth服务(BlueKing - Auth) available. + * 蓝鲸智云 - Auth 服务 (BlueKing - Auth) available. * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -27,7 +27,7 @@ import ( ) // AccessKeysKey ... -// TODO: 优化调整为类似IAM的二级缓存,LocalMemoryCache -> RedisCache -> DB +// TODO: 优化调整为类似 IAM 的二级缓存,LocalMemoryCache -> RedisCache -> DB type AccessKeysKey struct { AppCode string } @@ -45,7 +45,7 @@ func retrieveAccessKeys(key cache.Key) (interface{}, error) { if err != nil { return nil, err } - // 构造map: appSecret:enabled,方便查询校验 + // 构造 map: appSecret:enabled,方便查询校验 secretsMap := make(map[string]bool) for _, secret := range secretList { secretsMap[secret.AppSecret] = secret.Enabled diff --git a/src/bkauth/pkg/cache/impls/app.go b/src/bkauth/pkg/cache/impls/app.go index 71791ed..82b9582 100644 --- a/src/bkauth/pkg/cache/impls/app.go +++ b/src/bkauth/pkg/cache/impls/app.go @@ -19,45 +19,65 @@ package impls import ( + "go.uber.org/zap" + "bkauth/pkg/cache" "bkauth/pkg/errorx" "bkauth/pkg/service" + "bkauth/pkg/service/types" ) -type AppCodeKey struct { +type AppKey struct { AppCode string } -func (k AppCodeKey) Key() string { +func (k AppKey) Key() string { return k.AppCode } -func retrieveAppCode(key cache.Key) (interface{}, error) { - k := key.(AppCodeKey) +func retrieveApp(key cache.Key) (interface{}, error) { + k := key.(AppKey) svc := service.NewAppService() - return svc.Exists(k.AppCode) + return svc.Get(k.AppCode) } -// AppExists ... -func AppExists(appCode string) (exists bool, err error) { - key := AppCodeKey{ +func GetApp(appCode string) (app types.App, err error) { + key := AppKey{ AppCode: appCode, } - err = AppCodeCache.GetInto(key, &exists, retrieveAppCode) + err = AppCache.GetInto(key, &app, retrieveApp) if err != nil { - err = errorx.Wrapf(err, CacheLayer, "AppExists", - "AppCodeCache.Get appCode=`%s` fail", appCode) - return exists, err + err = errorx.Wrapf(err, CacheLayer, "GetApp", + "AppCache.GetInto appCode=`%s` fail", appCode) + return app, err } - return exists, nil + return app, nil } -func DeleteApp(appCode string) (err error) { - key := AppCodeKey{ +func DeleteAppCache(appCode string) (err error) { + // delete app exists cache + key := AppExistsKey{ + AppCode: appCode, + } + err = AppExistsCache.Delete(key) + if err != nil { + zap.S().Errorf("delete app exists cache fail, appCode=%s, err=%v", appCode, err) + return err + } + + // delete app info cache + key2 := AppKey{ AppCode: appCode, } - return AppCodeCache.Delete(key) + + err = AppCache.Delete(key2) + if err != nil { + zap.S().Errorf("delete app cache fail, appCode=%s, err=%v", appCode, err) + return err + } + + return nil } diff --git a/src/bkauth/pkg/sync/config.go b/src/bkauth/pkg/cache/impls/app_exists.go similarity index 58% rename from src/bkauth/pkg/sync/config.go rename to src/bkauth/pkg/cache/impls/app_exists.go index 6811663..ac05770 100644 --- a/src/bkauth/pkg/sync/config.go +++ b/src/bkauth/pkg/cache/impls/app_exists.go @@ -16,39 +16,41 @@ * to the current version of the project delivered to anyone in the future. */ -package sync +package impls import ( - "errors" - - "github.com/spf13/viper" - - "bkauth/pkg/config" + "bkauth/pkg/cache" + "bkauth/pkg/errorx" + "bkauth/pkg/service" ) -type OpenPaaSConfig struct { - Databases []config.Database - DatabaseMap map[string]config.Database +type AppExistsKey struct { + AppCode string } -// LoadConfig 从viper中读取配置文件 -func LoadConfig(v *viper.Viper) (*OpenPaaSConfig, error) { - var cfg OpenPaaSConfig - // 将配置信息绑定到结构体上 - if err := v.Unmarshal(&cfg); err != nil { - return nil, err - } +func (k AppExistsKey) Key() string { + return k.AppCode +} + +func retrieveAppExists(key cache.Key) (interface{}, error) { + k := key.(AppExistsKey) + + svc := service.NewAppService() + return svc.Exists(k.AppCode) +} - // parse the list to map - // 1. database - cfg.DatabaseMap = make(map[string]config.Database) - for _, db := range cfg.Databases { - cfg.DatabaseMap[db.ID] = db +// AppExists ... +func AppExists(appCode string) (exists bool, err error) { + key := AppExistsKey{ + AppCode: appCode, } - if len(cfg.DatabaseMap) == 0 { - return nil, errors.New("database cannot be empty") + err = AppExistsCache.GetInto(key, &exists, retrieveAppExists) + if err != nil { + err = errorx.Wrapf(err, CacheLayer, "AppExists", + "AppExistsCache.GetInto appCode=`%s` fail", appCode) + return exists, err } - return &cfg, nil + return exists, nil } diff --git a/src/bkauth/pkg/cache/impls/app_exists_test.go b/src/bkauth/pkg/cache/impls/app_exists_test.go new file mode 100644 index 0000000..cb5c637 --- /dev/null +++ b/src/bkauth/pkg/cache/impls/app_exists_test.go @@ -0,0 +1,89 @@ +/* + * TencentBlueKing is pleased to support the open source community by making + * 蓝鲸智云 - Auth服务(BlueKing - Auth) available. + * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the MIT License (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * 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. + * + * We undertake not to change the open source license (MIT license) applicable + * to the current version of the project delivered to anyone in the future. + */ + +package impls + +import ( + "errors" + "time" + + "github.com/agiledragon/gomonkey" + . "github.com/onsi/ginkgo/v2" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "bkauth/pkg/cache/redis" + "bkauth/pkg/service" + "bkauth/pkg/service/mock" + "bkauth/pkg/util" +) + +var _ = Describe("AppCodeCache", func() { + BeforeEach(func() { + expiration := 5 * time.Minute + cli := util.NewTestRedisClient() + mockCache := redis.NewMockCache(cli, "mockCache", expiration) + + AppExistsCache = mockCache + }) + + It("Key", func() { + key := AppExistsKey{ + AppCode: "test", + } + assert.Equal(GinkgoT(), key.Key(), "test") + }) + + Context("AppExists", func() { + var ctl *gomock.Controller + var patches *gomonkey.Patches + BeforeEach(func() { + ctl = gomock.NewController(GinkgoT()) + }) + AfterEach(func() { + ctl.Finish() + patches.Reset() + }) + It("AppCodeCache Get ok", func() { + mockService := mock.NewMockAppService(ctl) + mockService.EXPECT().Exists("test").Return(true, nil).AnyTimes() + + patches = gomonkey.ApplyFunc(service.NewAppService, + func() service.AppService { + return mockService + }) + + exists, err := AppExists("test") + assert.NoError(GinkgoT(), err) + assert.Equal(GinkgoT(), exists, true) + }) + It("AppCodeCache Get fail", func() { + mockService := mock.NewMockAppService(ctl) + mockService.EXPECT().Exists("test").Return(false, errors.New("error")).AnyTimes() + + patches = gomonkey.ApplyFunc(service.NewAppService, + func() service.AppService { + return mockService + }) + + exists, err := AppExists("test") + assert.Error(GinkgoT(), err) + assert.Equal(GinkgoT(), exists, false) + }) + }) +}) diff --git a/src/bkauth/pkg/cache/impls/app_test.go b/src/bkauth/pkg/cache/impls/app_test.go index ba0f0a3..97bf6ef 100644 --- a/src/bkauth/pkg/cache/impls/app_test.go +++ b/src/bkauth/pkg/cache/impls/app_test.go @@ -1,21 +1,3 @@ -/* - * TencentBlueKing is pleased to support the open source community by making - * 蓝鲸智云 - Auth服务(BlueKing - Auth) available. - * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. - * Licensed under the MIT License (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * 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. - * - * We undertake not to change the open source license (MIT license) applicable - * to the current version of the project delivered to anyone in the future. - */ - package impls import ( @@ -30,26 +12,27 @@ import ( "bkauth/pkg/cache/redis" "bkauth/pkg/service" "bkauth/pkg/service/mock" + "bkauth/pkg/service/types" "bkauth/pkg/util" ) -var _ = Describe("AppCodeCache", func() { +var _ = Describe("AppCache", func() { BeforeEach(func() { expiration := 5 * time.Minute cli := util.NewTestRedisClient() mockCache := redis.NewMockCache(cli, "mockCache", expiration) - AppCodeCache = mockCache + AppCache = mockCache }) It("Key", func() { - key := AppCodeKey{ + key := AppKey{ AppCode: "test", } assert.Equal(GinkgoT(), key.Key(), "test") }) - Context("AppExists", func() { + Context("GetApp", func() { var ctl *gomock.Controller var patches *gomonkey.Patches BeforeEach(func() { @@ -59,36 +42,32 @@ var _ = Describe("AppCodeCache", func() { ctl.Finish() patches.Reset() }) - It("AppCodeCache Get ok", func() { + It("AppCache Get ok", func() { mockService := mock.NewMockAppService(ctl) - mockService.EXPECT().Exists("test").Return(true, nil).AnyTimes() + mockApp := types.App{Code: "test"} + mockService.EXPECT().Get("test").Return(mockApp, nil).AnyTimes() patches = gomonkey.ApplyFunc(service.NewAppService, func() service.AppService { return mockService }) - exists, err := AppExists("test") + app, err := GetApp("test") assert.NoError(GinkgoT(), err) - assert.Equal(GinkgoT(), exists, true) + assert.Equal(GinkgoT(), app, mockApp) }) - It("AppCodeCache Get fail", func() { + It("AppCache Get fail", func() { mockService := mock.NewMockAppService(ctl) - mockService.EXPECT().Exists("test").Return(false, errors.New("error")).AnyTimes() + mockService.EXPECT().Get("test").Return(types.App{}, errors.New("error")).AnyTimes() patches = gomonkey.ApplyFunc(service.NewAppService, func() service.AppService { return mockService }) - exists, err := AppExists("test") + app, err := GetApp("test") assert.Error(GinkgoT(), err) - assert.Equal(GinkgoT(), exists, false) + assert.Equal(GinkgoT(), app, types.App{}) }) }) - - It("DeleteApp", func() { - err := DeleteApp("test") - assert.NoError(GinkgoT(), err) - }) }) diff --git a/src/bkauth/pkg/cache/impls/init.go b/src/bkauth/pkg/cache/impls/init.go index 1d7bc65..fa78f17 100644 --- a/src/bkauth/pkg/cache/impls/init.go +++ b/src/bkauth/pkg/cache/impls/init.go @@ -31,7 +31,8 @@ const CacheLayer = "Cache" var ( LocalAccessAppCache memory.Cache - AppCodeCache *redis.Cache + AppExistsCache *redis.Cache + AppCache *redis.Cache AccessKeysCache *redis.Cache ) @@ -46,9 +47,15 @@ func InitCaches(disabled bool) { nil, ) - AppCodeCache = redis.NewCache( + AppExistsCache = redis.NewCache( bkauthredis.GetDefaultRedisClient(), - "app_code", + "app_exists", + 5*time.Minute, + ) + + AppCache = redis.NewCache( + bkauthredis.GetDefaultRedisClient(), + "app_info", 5*time.Minute, ) diff --git a/src/bkauth/pkg/cache/impls/local_access_app.go b/src/bkauth/pkg/cache/impls/local_access_app.go index c46f18c..2a98729 100644 --- a/src/bkauth/pkg/cache/impls/local_access_app.go +++ b/src/bkauth/pkg/cache/impls/local_access_app.go @@ -1,6 +1,6 @@ /* * TencentBlueKing is pleased to support the open source community by making - * 蓝鲸智云 - Auth服务(BlueKing - Auth) available. + * 蓝鲸智云 - Auth 服务 (BlueKing - Auth) available. * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -26,7 +26,7 @@ import ( ) // AccessAppCacheKey ... -// Note: 当前缓存只用于API的认证,不用于任何业务逻辑 +// Note: 当前缓存只用于 API 的认证,不用于任何业务逻辑 type AccessAppCacheKey struct { AppCode string AppSecret string diff --git a/src/bkauth/pkg/config/config.go b/src/bkauth/pkg/config/config.go index 42a0258..7a93496 100644 --- a/src/bkauth/pkg/config/config.go +++ b/src/bkauth/pkg/config/config.go @@ -1,6 +1,6 @@ /* * TencentBlueKing is pleased to support the open source community by making - * 蓝鲸智云 - Auth服务(BlueKing - Auth) available. + * 蓝鲸智云 - Auth 服务 (BlueKing - Auth) available. * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -121,6 +121,8 @@ type APIAllowList struct { type Config struct { Debug bool + // 是否开启多租户模式 + EnableMultiTenantMode bool Server Server Sentry Sentry @@ -142,7 +144,7 @@ type Config struct { Logger Logger } -// Load 从viper中读取配置文件 +// Load 从 viper 中读取配置文件 func Load(v *viper.Viper) (*Config, error) { var cfg Config // 将配置信息绑定到结构体上 diff --git a/src/bkauth/pkg/database/dao/app.go b/src/bkauth/pkg/database/dao/app.go index 15b7d27..1166798 100644 --- a/src/bkauth/pkg/database/dao/app.go +++ b/src/bkauth/pkg/database/dao/app.go @@ -1,6 +1,6 @@ /* * TencentBlueKing is pleased to support the open source community by making - * 蓝鲸智云 - Auth服务(BlueKing - Auth) available. + * 蓝鲸智云 - Auth 服务 (BlueKing - Auth) available. * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -33,15 +33,17 @@ type App struct { Code string `db:"code"` Name string `db:"name"` Description string `db:"description"` + TenantMode string `db:"tenant_mode"` + TenantID string `db:"tenant_id"` - // Note: APP 是一个主表, oauth2相关信息是关联表(外键code),这里只是备注一下而已,后续删除注释 + // Note: APP 是一个主表,oauth2 相关信息是关联表 (外键 code),这里只是备注一下而已,后续删除注释 // Oauth2.0 相关信息 - // Scopes 和 RedirectURLs,但是由于这些都可能需要支持多个,可能得考虑json(List)存储或另外一对多的表存储 + // Scopes 和 RedirectURLs,但是由于这些都可能需要支持多个,可能得考虑 json(List) 存储或另外一对多的表存储 - // AppCode: 蓝鲸体系里 app_code=client_id,实际Oauth2.0协议里建议ClientID是随机字符串 + // AppCode: 蓝鲸体系里 app_code=client_id,实际 Oauth2.0 协议里建议 ClientID 是随机字符串 // https://datatracker.ietf.org/doc/html/rfc6749#section-2.2 // https://www.oauth.com/oauth2-servers/client-registration/client-id-secret/ - // ClientType: Oauth2.0协议里根据安全性来区分类型,https://datatracker.ietf.org/doc/html/rfc6749#section-2.1 + // ClientType: Oauth2.0 协议里根据安全性来区分类型,https://datatracker.ietf.org/doc/html/rfc6749#section-2.1 // AppCode string `db:"client_id"` // ClientType string `db:"client_type"` } @@ -50,7 +52,9 @@ type AppManager interface { CreateWithTx(tx *sqlx.Tx, app App) error Exists(code string) (bool, error) NameExists(name string) (bool, error) - List() ([]App, error) + List(tenantMode, tenantID string, limit, offset int, orderBy, orderByDirection string) ([]App, error) + Get(code string) (App, error) + Count(tenantMode, tenantID string) (int, error) } type appManager struct { @@ -64,8 +68,19 @@ func NewAppManager() AppManager { } } +func (m *appManager) Get(code string) (app App, err error) { + query := `SELECT code, name, description, tenant_mode, tenant_id FROM app where code = ? LIMIT 1` + + err = database.SqlxGet(m.DB, &app, query, code) + if errors.Is(err, sql.ErrNoRows) { + return app, nil + } + return app, err +} + func (m *appManager) CreateWithTx(tx *sqlx.Tx, app App) error { - query := `INSERT INTO app (code, name, description) VALUES (:code, :name, :description)` + query := `INSERT INTO app (code, name, description, tenant_mode, tenant_id) + VALUES (:code, :name, :description, :tenant_mode, :tenant_id)` _, err := database.SqlxInsertWithTx(tx, query, app) return err } @@ -106,11 +121,56 @@ func (m *appManager) selectNameExistence(existCode *string, name string) error { return database.SqlxGet(m.DB, existCode, query, name) } -func (m *appManager) List() (apps []App, err error) { - query := `SELECT code, name, description FROM app` - err = database.SqlxSelect(m.DB, &apps, query) +func (m *appManager) List( + tenantMode, tenantID string, + limit, offset int, + orderBy, orderByDirection string, +) (apps []App, err error) { + query := `SELECT code, name, description, tenant_mode, tenant_id FROM app WHERE 1=1` + args := []interface{}{} + + if tenantMode != "" { + query += ` AND tenant_mode = ?` + args = append(args, tenantMode) + } + if tenantID != "" { + query += ` AND tenant_id = ?` + args = append(args, tenantID) + } + + // order by + if orderBy == "" { + orderBy = "created_at" + } + if orderByDirection == "" { + orderByDirection = "ASC" + } + query += ` ORDER BY ` + orderBy + ` ` + orderByDirection + + // limit and offset + query += ` LIMIT ? OFFSET ?` + args = append(args, limit, offset) + + err = database.SqlxSelect(m.DB, &apps, query, args...) if errors.Is(err, sql.ErrNoRows) { return apps, nil } - return + return apps, err +} + +func (m *appManager) Count(tenantMode, tenantID string) (total int, err error) { + query := `SELECT COUNT(*) FROM app WHERE 1=1` + args := []interface{}{} + + if tenantMode != "" { + query += ` AND tenant_mode = ?` + args = append(args, tenantMode) + } + if tenantID != "" { + query += ` AND tenant_id = ?` + args = append(args, tenantID) + } + + err = database.SqlxGet(m.DB, &total, query, args...) + return total, err } diff --git a/src/bkauth/pkg/database/dao/app_test.go b/src/bkauth/pkg/database/dao/app_test.go index e4147b4..df48542 100644 --- a/src/bkauth/pkg/database/dao/app_test.go +++ b/src/bkauth/pkg/database/dao/app_test.go @@ -32,7 +32,7 @@ func Test_appManager_CreateWithTx(t *testing.T) { database.RunWithMock(t, func(db *sqlx.DB, mock sqlmock.Sqlmock, t *testing.T) { mock.ExpectBegin() mock.ExpectExec(`INSERT INTO app`).WithArgs( - "bkauth", "bkauth", "bkauth intro", + "bkauth", "bkauth", "bkauth intro", "type1", "default", ).WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectCommit() @@ -43,6 +43,8 @@ func Test_appManager_CreateWithTx(t *testing.T) { Code: "bkauth", Name: "bkauth", Description: "bkauth intro", + TenantMode: "type1", + TenantID: "default", } manager := &appManager{DB: db} @@ -85,3 +87,57 @@ func Test_appManager_NameExists(t *testing.T) { assert.Equal(t, exists, true) }) } + +func Test_appManager_Get(t *testing.T) { + database.RunWithMock(t, func(db *sqlx.DB, mock sqlmock.Sqlmock, t *testing.T) { + mockQuery := `^SELECT code, name, description, tenant_mode, tenant_id FROM app where code = (.*) LIMIT 1$` + mockRows := sqlmock.NewRows([]string{"code", "name", "description", "tenant_mode", "tenant_id"}). + AddRow("bkauth", "bkauth", "bkauth intro", "type1", "default") + mock.ExpectQuery(mockQuery).WithArgs("bkauth").WillReturnRows(mockRows) + + manager := &appManager{DB: db} + + app, err := manager.Get("bkauth") + + assert.NoError(t, err, "query from db fail.") + assert.Equal(t, app.Code, "bkauth") + assert.Equal(t, app.Name, "bkauth") + assert.Equal(t, app.Description, "bkauth intro") + assert.Equal(t, app.TenantMode, "type1") + assert.Equal(t, app.TenantID, "default") + }) +} + +func Test_appManager_List(t *testing.T) { + database.RunWithMock(t, func(db *sqlx.DB, mock sqlmock.Sqlmock, t *testing.T) { + mockQuery := `^SELECT code, name, description, tenant_mode, tenant_id FROM app WHERE 1=1 AND tenant_mode = (.*) AND tenant_id = (.*) LIMIT (.*) OFFSET (.*)$` + mockRows := sqlmock.NewRows([]string{"code", "name", "description", "tenant_mode", "tenant_id"}). + AddRow("bkauth1", "bkauth1", "bkauth1 intro", "type1", "default"). + AddRow("bkauth2", "bkauth2", "bkauth2 intro", "type1", "default") + mock.ExpectQuery(mockQuery).WithArgs("type1", "default", 10, 0).WillReturnRows(mockRows) + + manager := &appManager{DB: db} + + apps, err := manager.List("type1", "default", 10, 0, "", "") + + assert.NoError(t, err, "query from db fail.") + assert.Len(t, apps, 2) + assert.Equal(t, apps[0].Code, "bkauth1") + assert.Equal(t, apps[1].Code, "bkauth2") + }) +} + +func Test_appManager_Count(t *testing.T) { + database.RunWithMock(t, func(db *sqlx.DB, mock sqlmock.Sqlmock, t *testing.T) { + mockQuery := `^SELECT COUNT\(\*\) FROM app WHERE 1=1 AND tenant_mode = (.*) AND tenant_id = (.*)$` + mockRows := sqlmock.NewRows([]string{"count"}).AddRow(2) + mock.ExpectQuery(mockQuery).WithArgs("type1", "default").WillReturnRows(mockRows) + + manager := &appManager{DB: db} + + count, err := manager.Count("type1", "default") + + assert.NoError(t, err, "query from db fail.") + assert.Equal(t, count, 2) + }) +} diff --git a/src/bkauth/pkg/database/dao/mock/app.go b/src/bkauth/pkg/database/dao/mock/app.go index 10c1374..4bbb74d 100644 --- a/src/bkauth/pkg/database/dao/mock/app.go +++ b/src/bkauth/pkg/database/dao/mock/app.go @@ -41,6 +41,21 @@ func (m *MockAppManager) EXPECT() *MockAppManagerMockRecorder { return m.recorder } +// Count mocks base method. +func (m *MockAppManager) Count(tenantMode, tenantID string) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Count", tenantMode, tenantID) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Count indicates an expected call of Count. +func (mr *MockAppManagerMockRecorder) Count(tenantMode, tenantID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Count", reflect.TypeOf((*MockAppManager)(nil).Count), tenantMode, tenantID) +} + // CreateWithTx mocks base method. func (m *MockAppManager) CreateWithTx(tx *sqlx.Tx, app dao.App) error { m.ctrl.T.Helper() @@ -70,19 +85,34 @@ func (mr *MockAppManagerMockRecorder) Exists(code any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exists", reflect.TypeOf((*MockAppManager)(nil).Exists), code) } +// Get mocks base method. +func (m *MockAppManager) Get(code string) (dao.App, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", code) + ret0, _ := ret[0].(dao.App) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockAppManagerMockRecorder) Get(code any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockAppManager)(nil).Get), code) +} + // List mocks base method. -func (m *MockAppManager) List() ([]dao.App, error) { +func (m *MockAppManager) List(tenantMode, tenantID string, limit, offset int, orderBy, orderByDirection string) ([]dao.App, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "List") + ret := m.ctrl.Call(m, "List", tenantMode, tenantID, limit, offset, orderBy, orderByDirection) ret0, _ := ret[0].([]dao.App) ret1, _ := ret[1].(error) return ret0, ret1 } // List indicates an expected call of List. -func (mr *MockAppManagerMockRecorder) List() *gomock.Call { +func (mr *MockAppManagerMockRecorder) List(tenantMode, tenantID, limit, offset, orderBy, orderByDirection any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockAppManager)(nil).List)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockAppManager)(nil).List), tenantMode, tenantID, limit, offset, orderBy, orderByDirection) } // NameExists mocks base method. diff --git a/src/bkauth/pkg/fixture/access_key.go b/src/bkauth/pkg/fixture/access_key.go index 11942db..447b352 100644 --- a/src/bkauth/pkg/fixture/access_key.go +++ b/src/bkauth/pkg/fixture/access_key.go @@ -1,6 +1,6 @@ /* * TencentBlueKing is pleased to support the open source community by making - * 蓝鲸智云 - Auth服务(BlueKing - Auth) available. + * 蓝鲸智云 - Auth 服务 (BlueKing - Auth) available. * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -27,15 +27,15 @@ import ( "bkauth/pkg/service/types" ) -func createAccessKey(appCode, appSecret string) { +func createAccessKey(appCode, appSecret, tenantMode, tenantID string) { createdSource := "deploy_init" - // TODO: 校验appCode和appSecret格式是否正确 + // TODO: 校验 appCode 和 appSecret 格式是否正确 if appCode == "" || appSecret == "" { return } - // 查询App是否存在 + // 查询 App 是否存在 appSvc := service.NewAppService() exists, err := appSvc.Exists(appCode) if err != nil { @@ -44,7 +44,7 @@ func createAccessKey(appCode, appSecret string) { // 不存在则创建 if !exists { err = appSvc.CreateWithSecret( - types.App{Code: appCode, Name: appCode, Description: appCode}, + types.App{Code: appCode, Name: appCode, Description: appCode, TenantMode: tenantMode, TenantID: tenantID}, appSecret, createdSource, ) @@ -54,8 +54,8 @@ func createAccessKey(appCode, appSecret string) { return } - // APP存在则只需要创建Secret - // 查询对应的AppCode和AppSecret是否已存在 + // APP 存在则只需要创建 Secret + // 查询对应的 AppCode 和 AppSecret 是否已存在 svc := service.NewAccessKeyService() exists, err = svc.Verify(appCode, appSecret) if err != nil { diff --git a/src/bkauth/pkg/fixture/init.go b/src/bkauth/pkg/fixture/init.go index f269aa7..ad4e607 100644 --- a/src/bkauth/pkg/fixture/init.go +++ b/src/bkauth/pkg/fixture/init.go @@ -19,11 +19,25 @@ package fixture import ( + "go.uber.org/zap" + "bkauth/pkg/config" + "bkauth/pkg/util" ) func InitFixture(cfg *config.Config) { + var tenantMode, tenantID string + if cfg.EnableMultiTenantMode { + tenantMode = util.TenantModeGlobal + tenantID = "" + zap.S().Info("enableMultiTenantMode=True, all init data would be tenantMode=global, tenantID={empty}") + } else { + tenantMode = util.TenantModeSingle + tenantID = util.TenantIDDefault + zap.S().Info("enableMultiTenantMode=True, all init data would be tenantMode=single, tenantID=default") + } + for appCode, appSecret := range cfg.AccessKeys { - createAccessKey(appCode, appSecret) + createAccessKey(appCode, appSecret, tenantMode, tenantID) } } diff --git a/src/bkauth/pkg/sync/util.go b/src/bkauth/pkg/middleware/tenant.go similarity index 62% rename from src/bkauth/pkg/sync/util.go rename to src/bkauth/pkg/middleware/tenant.go index 6bf0827..33e727e 100644 --- a/src/bkauth/pkg/sync/util.go +++ b/src/bkauth/pkg/middleware/tenant.go @@ -16,31 +16,17 @@ * to the current version of the project delivered to anyone in the future. */ -package sync +package middleware -// AccessKeySet ... -type AccessKeySet struct { - Data map[string]struct{} -} - -// NewAccessKeySet ... -func NewAccessKeySet() *AccessKeySet { - return &AccessKeySet{ - Data: map[string]struct{}{}, - } -} +import ( + "github.com/gin-gonic/gin" -func (s *AccessKeySet) key(appCode, appSecret string) string { - return appCode + ":" + appSecret -} - -// Has ... -func (s *AccessKeySet) Has(appCode, appSecret string) bool { - _, ok := s.Data[s.key(appCode, appSecret)] - return ok -} + "bkauth/pkg/util" +) -// Add ... -func (s *AccessKeySet) Add(appCode, appSecret string) { - s.Data[s.key(appCode, appSecret)] = struct{}{} +func NewEnableMultiTenantModeMiddleware(enableMultiTenantMode bool) gin.HandlerFunc { + return func(c *gin.Context) { + util.SetEnableMultiTenantMode(c, enableMultiTenantMode) + c.Next() + } } diff --git a/src/bkauth/pkg/server/router.go b/src/bkauth/pkg/server/router.go index 23480b9..19ffab5 100644 --- a/src/bkauth/pkg/server/router.go +++ b/src/bkauth/pkg/server/router.go @@ -58,6 +58,7 @@ func NewRouter(cfg *config.Config) *gin.Engine { // TODO: 接口日志有些敏感有些不敏感,校验接口也有使用POST的, 目前一刀切 appRouter.Use(middleware.APILogger()) appRouter.Use(middleware.AccessAppAuthMiddleware()) + appRouter.Use(middleware.NewEnableMultiTenantModeMiddleware(cfg.EnableMultiTenantMode)) app.Register(appRouter) // oauth apis for oauth2.0 accessToken @@ -65,6 +66,7 @@ func NewRouter(cfg *config.Config) *gin.Engine { oauthRouter.Use(middleware.Metrics()) oauthRouter.Use(middleware.APILogger()) oauthRouter.Use(middleware.AccessAppAuthMiddleware()) + // appRouter.Use(middleware.NewEnableMultiTenantModeMiddleware(cfg.EnableMultiTenantMode)) oauth.Register(oauthRouter) return router diff --git a/src/bkauth/pkg/service/app.go b/src/bkauth/pkg/service/app.go index d20301b..05cdab0 100644 --- a/src/bkauth/pkg/service/app.go +++ b/src/bkauth/pkg/service/app.go @@ -1,6 +1,6 @@ /* * TencentBlueKing is pleased to support the open source community by making - * 蓝鲸智云 - Auth服务(BlueKing - Auth) available. + * 蓝鲸智云 - Auth 服务 (BlueKing - Auth) available. * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -30,11 +30,12 @@ import ( const AppSVC = "AppSVC" type AppService interface { + Get(code string) (types.App, error) Exists(code string) (bool, error) NameExists(name string) (bool, error) Create(app types.App, createdSource string) error CreateWithSecret(app types.App, appSecret, createdSource string) error - List() ([]types.App, error) + List(tenantMode, tenantID string, page, pageSize int, orderBy, orderByDirection string) (int, []types.App, error) } type appService struct { @@ -49,6 +50,23 @@ func NewAppService() AppService { } } +func (s *appService) Get(code string) (app types.App, err error) { + errorWrapf := errorx.NewLayerFunctionErrorWrapf(AppSVC, "Get") + + daoApp, err := s.manager.Get(code) + if err != nil { + return app, errorWrapf(err, "manager.Get fail") + } + + return types.App{ + Code: daoApp.Code, + Name: daoApp.Name, + Description: daoApp.Description, + TenantMode: daoApp.TenantMode, + TenantID: daoApp.TenantID, + }, nil +} + func (s *appService) Exists(code string) (bool, error) { errorWrapf := errorx.NewLayerFunctionErrorWrapf(AppSVC, "Exists") @@ -69,7 +87,7 @@ func (s *appService) NameExists(name string) (bool, error) { return exists, nil } -// Create :创建应用,createdSource为创建的来源,即哪个系统创建了该APP +// Create :创建应用,createdSource 为创建的来源,即哪个系统创建了该 APP func (s *appService) Create(app types.App, createdSource string) (err error) { errorWrapf := errorx.NewLayerFunctionErrorWrapf(AppSVC, "Create") @@ -83,13 +101,19 @@ func (s *appService) Create(app types.App, createdSource string) (err error) { } // 创建应用 - daoApp := dao.App{Code: app.Code, Name: app.Name, Description: app.Description} + daoApp := dao.App{ + Code: app.Code, + Name: app.Name, + Description: app.Description, + TenantMode: app.TenantMode, + TenantID: app.TenantID, + } err = s.manager.CreateWithTx(tx, daoApp) if err != nil { return errorWrapf(err, "manager.CreateWithTx app=`%+v` fail", daoApp) } - // 创建应用对应Secret + // 创建应用对应 Secret daoAccessKey := newDaoAccessKey(app.Code, createdSource) _, err = s.accessKeyManager.CreateWithTx(tx, daoAccessKey) if err != nil { @@ -100,7 +124,7 @@ func (s *appService) Create(app types.App, createdSource string) (err error) { return } -// CreateWithSecret :创建应用,但支持指定appSecret的值,createdSource为创建的来源,即哪个系统创建了该APP +// CreateWithSecret :创建应用,但支持指定 appSecret 的值,createdSource 为创建的来源,即哪个系统创建了该 APP func (s *appService) CreateWithSecret(app types.App, appSecret, createdSource string) (err error) { errorWrapf := errorx.NewLayerFunctionErrorWrapf(AppSVC, "CreateWithSecret") @@ -113,13 +137,19 @@ func (s *appService) CreateWithSecret(app types.App, appSecret, createdSource st } // 创建应用 - daoApp := dao.App{Code: app.Code, Name: app.Name, Description: app.Description} + daoApp := dao.App{ + Code: app.Code, + Name: app.Name, + Description: app.Description, + TenantMode: app.TenantMode, + TenantID: app.TenantID, + } err = s.manager.CreateWithTx(tx, daoApp) if err != nil { return errorWrapf(err, "manager.CreateWithTx app=`%+v` fail", daoApp) } - // 创建应用对应Secret + // 创建应用对应 Secret daoAccessKey := newDaoAccessKeyWithAppSecret(app.Code, appSecret, createdSource) _, err = s.accessKeyManager.CreateWithTx(tx, daoAccessKey) if err != nil { @@ -131,12 +161,24 @@ func (s *appService) CreateWithSecret(app types.App, appSecret, createdSource st return } -func (s *appService) List() (apps []types.App, err error) { +func (s *appService) List( + tenantMode, tenantID string, + page, pageSize int, + orderBy, orderByDirection string, +) (total int, apps []types.App, err error) { errorWrapf := errorx.NewLayerFunctionErrorWrapf(AppSVC, "List") - daoApps, err := s.manager.List() + total, err = s.manager.Count(tenantMode, tenantID) + if err != nil { + return 0, nil, errorWrapf(err, "manager.Count fail") + } + + limit := pageSize + offset := (page - 1) * pageSize + + daoApps, err := s.manager.List(tenantMode, tenantID, limit, offset, orderBy, orderByDirection) if err != nil { - return apps, errorWrapf(err, "manager.List fail") + return 0, nil, errorWrapf(err, "manager.List fail") } apps = make([]types.App, 0, len(daoApps)) @@ -145,8 +187,10 @@ func (s *appService) List() (apps []types.App, err error) { Code: daoApp.Code, Name: daoApp.Name, Description: daoApp.Description, + TenantMode: daoApp.TenantMode, + TenantID: daoApp.TenantID, }) } - return + return total, apps, nil } diff --git a/src/bkauth/pkg/service/app_test.go b/src/bkauth/pkg/service/app_test.go index 0276124..8eef1c1 100644 --- a/src/bkauth/pkg/service/app_test.go +++ b/src/bkauth/pkg/service/app_test.go @@ -358,4 +358,131 @@ var _ = Describe("App", func() { assert.Contains(GinkgoT(), err.Error(), "accessKeyManager.CreateWithTx") }) }) + + Describe("Get cases", func() { + var ctl *gomock.Controller + + BeforeEach(func() { + ctl = gomock.NewController(GinkgoT()) + }) + + AfterEach(func() { + ctl.Finish() + }) + + It("ok", func() { + mockAppManager := mock.NewMockAppManager(ctl) + mockAppManager.EXPECT().Get("bkauth").Return(dao.App{ + Code: "bkauth", + Name: "bkauth", + Description: "bkauth intro", + TenantMode: "type1", + TenantID: "tenant1", + }, nil) + + mockAccessKeyManager := mock.NewMockAccessKeyManager(ctl) + + svc := appService{ + manager: mockAppManager, + accessKeyManager: mockAccessKeyManager, + } + + app, err := svc.Get("bkauth") + assert.NoError(GinkgoT(), err) + assert.Equal(GinkgoT(), "bkauth", app.Code) + assert.Equal(GinkgoT(), "bkauth", app.Name) + assert.Equal(GinkgoT(), "bkauth intro", app.Description) + assert.Equal(GinkgoT(), "type1", app.TenantMode) + assert.Equal(GinkgoT(), "tenant1", app.TenantID) + }) + + It("error", func() { + mockAppManager := mock.NewMockAppManager(ctl) + mockAppManager.EXPECT().Get("bkauth").Return(dao.App{}, errors.New("error")) + + mockAccessKeyManager := mock.NewMockAccessKeyManager(ctl) + + svc := appService{ + manager: mockAppManager, + accessKeyManager: mockAccessKeyManager, + } + + _, err := svc.Get("bkauth") + assert.Error(GinkgoT(), err) + }) + }) + + Describe("List cases", func() { + var ctl *gomock.Controller + + BeforeEach(func() { + ctl = gomock.NewController(GinkgoT()) + }) + + AfterEach(func() { + ctl.Finish() + }) + + It("ok", func() { + mockAppManager := mock.NewMockAppManager(ctl) + mockAppManager.EXPECT().Count("type1", "tenant1").Return(1, nil) + mockAppManager.EXPECT().List("type1", "tenant1", 10, 0, "", "").Return([]dao.App{ + { + Code: "bkauth", + Name: "bkauth", + Description: "bkauth intro", + TenantMode: "type1", + TenantID: "tenant1", + }, + }, nil) + + mockAccessKeyManager := mock.NewMockAccessKeyManager(ctl) + + svc := appService{ + manager: mockAppManager, + accessKeyManager: mockAccessKeyManager, + } + + total, apps, err := svc.List("type1", "tenant1", 1, 10, "", "") + assert.NoError(GinkgoT(), err) + assert.Equal(GinkgoT(), 1, total) + assert.Len(GinkgoT(), apps, 1) + assert.Equal(GinkgoT(), "bkauth", apps[0].Code) + assert.Equal(GinkgoT(), "bkauth", apps[0].Name) + assert.Equal(GinkgoT(), "bkauth intro", apps[0].Description) + assert.Equal(GinkgoT(), "type1", apps[0].TenantMode) + assert.Equal(GinkgoT(), "tenant1", apps[0].TenantID) + }) + + It("count error", func() { + mockAppManager := mock.NewMockAppManager(ctl) + mockAppManager.EXPECT().Count("type1", "tenant1").Return(0, errors.New("error")) + + mockAccessKeyManager := mock.NewMockAccessKeyManager(ctl) + + svc := appService{ + manager: mockAppManager, + accessKeyManager: mockAccessKeyManager, + } + + _, _, err := svc.List("type1", "tenant1", 1, 10, "", "") + assert.Error(GinkgoT(), err) + }) + + It("list error", func() { + mockAppManager := mock.NewMockAppManager(ctl) + mockAppManager.EXPECT().Count("type1", "tenant1").Return(1, nil) + mockAppManager.EXPECT().List("type1", "tenant1", 10, 0, "", "").Return(nil, errors.New("error")) + + mockAccessKeyManager := mock.NewMockAccessKeyManager(ctl) + + svc := appService{ + manager: mockAppManager, + accessKeyManager: mockAccessKeyManager, + } + + _, _, err := svc.List("type1", "tenant1", 1, 10, "", "") + assert.Error(GinkgoT(), err) + }) + }) }) diff --git a/src/bkauth/pkg/service/mock/app.go b/src/bkauth/pkg/service/mock/app.go index 5ed6e5a..98aea98 100644 --- a/src/bkauth/pkg/service/mock/app.go +++ b/src/bkauth/pkg/service/mock/app.go @@ -83,19 +83,35 @@ func (mr *MockAppServiceMockRecorder) Exists(code any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exists", reflect.TypeOf((*MockAppService)(nil).Exists), code) } -// List mocks base method. -func (m *MockAppService) List() ([]types.App, error) { +// Get mocks base method. +func (m *MockAppService) Get(code string) (types.App, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "List") - ret0, _ := ret[0].([]types.App) + ret := m.ctrl.Call(m, "Get", code) + ret0, _ := ret[0].(types.App) ret1, _ := ret[1].(error) return ret0, ret1 } +// Get indicates an expected call of Get. +func (mr *MockAppServiceMockRecorder) Get(code any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockAppService)(nil).Get), code) +} + +// List mocks base method. +func (m *MockAppService) List(tenantMode, tenantID string, page, pageSize int, orderBy, orderByDirection string) (int, []types.App, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", tenantMode, tenantID, page, pageSize, orderBy, orderByDirection) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].([]types.App) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + // List indicates an expected call of List. -func (mr *MockAppServiceMockRecorder) List() *gomock.Call { +func (mr *MockAppServiceMockRecorder) List(tenantMode, tenantID, page, pageSize, orderBy, orderByDirection any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockAppService)(nil).List)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockAppService)(nil).List), tenantMode, tenantID, page, pageSize, orderBy, orderByDirection) } // NameExists mocks base method. diff --git a/src/bkauth/pkg/service/types/app.go b/src/bkauth/pkg/service/types/app.go index 6ca3a4c..699df87 100644 --- a/src/bkauth/pkg/service/types/app.go +++ b/src/bkauth/pkg/service/types/app.go @@ -22,4 +22,6 @@ type App struct { Code string `json:"bk_app_code"` Name string `json:"name"` Description string `json:"description"` + TenantMode string `json:"bk_tenant_mode"` + TenantID string `json:"bk_tenant_id"` } diff --git a/src/bkauth/pkg/sync/dao.go b/src/bkauth/pkg/sync/dao.go deleted file mode 100644 index a342837..0000000 --- a/src/bkauth/pkg/sync/dao.go +++ /dev/null @@ -1,123 +0,0 @@ -/* - * TencentBlueKing is pleased to support the open source community by making - * 蓝鲸智云 - Auth服务(BlueKing - Auth) available. - * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. - * Licensed under the MIT License (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * 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. - * - * We undertake not to change the open source license (MIT license) applicable - * to the current version of the project delivered to anyone in the future. - */ - -package sync - -import ( - "database/sql" - "errors" - "time" - - "github.com/jmoiron/sqlx" - - "bkauth/pkg/database" -) - -// BKPaaSApp ... -type BKPaaSApp struct { - Code string `db:"code"` - AuthToken string `db:"auth_token"` -} - -// ESBAppAccount ... -type ESBAppAccount struct { - AppCode string `db:"app_code"` - AppToken string `db:"app_token"` -} - -type OpenPaaSManager interface { - ListBKPaaSApp() (apps []BKPaaSApp, err error) - ListESBAppAccount() (esbAccounts []ESBAppAccount, err error) - AuthTokenEmptyExists(appCode string) (bool, error) - UpdateBKPaaSApp(code, authToken string) error - CreateESBAppAccount(appCode, appToken string) error -} - -type openPaaSManager struct { - DB *sqlx.DB -} - -func NewOpenPaaSManager() OpenPaaSManager { - return &openPaaSManager{ - DB: GetOpenPaaSDBClient().DB, - } -} - -func (m *openPaaSManager) ListBKPaaSApp() (apps []BKPaaSApp, err error) { - query := `SELECT code, auth_token FROM paas_app WHERE auth_token IS NOT NULL AND auth_token != ""` - err = database.SqlxSelect(m.DB, &apps, query) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return - } - - return -} - -func (m *openPaaSManager) ListESBAppAccount() (esbAccounts []ESBAppAccount, err error) { - query := `SELECT app_code, app_token FROM esb_app_account` - err = database.SqlxSelect(m.DB, &esbAccounts, query) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return - } - - return -} - -func (m *openPaaSManager) AuthTokenEmptyExists(code string) (bool, error) { - query := `SELECT code FROM paas_app WHERE code = ? AND (auth_token IS NULL or auth_token = "") LIMIT 1` - var existingCode string - err := database.SqlxGet(m.DB, &existingCode, query, code) - if errors.Is(err, sql.ErrNoRows) { - return false, nil - } - if err != nil { - return false, err - } - - return true, nil -} - -func (m *openPaaSManager) UpdateBKPaaSApp(code, authToken string) error { - query := `UPDATE paas_app SET auth_token=:auth_token WHERE code=:code` - data := map[string]interface{}{ - "code": code, - "auth_token": authToken, - } - - _, err := database.SqlxUpdate(m.DB, query, data) - - return err -} - -func (m *openPaaSManager) CreateESBAppAccount(appCode, appToken string) error { - query := `INSERT INTO esb_app_account ( - app_code, - app_token, - introduction, - created_time - ) VALUES (:app_code, :app_token, :introduction, :created_time)` - data := map[string]interface{}{ - "app_code": appCode, - "app_token": appToken, - "introduction": appCode, - "created_time": time.Now(), - } - - _, err := database.SqlxInsert(m.DB, query, data) - return err -} diff --git a/src/bkauth/pkg/sync/init.go b/src/bkauth/pkg/sync/init.go deleted file mode 100644 index ea84aeb..0000000 --- a/src/bkauth/pkg/sync/init.go +++ /dev/null @@ -1,51 +0,0 @@ -/* - * TencentBlueKing is pleased to support the open source community by making - * 蓝鲸智云 - Auth服务(BlueKing - Auth) available. - * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. - * Licensed under the MIT License (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * 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. - * - * We undertake not to change the open source license (MIT license) applicable - * to the current version of the project delivered to anyone in the future. - */ - -package sync - -import ( - "sync" - - "bkauth/pkg/config" - "bkauth/pkg/database" -) - -// OpenPaaSDBClient ... -var ( - // OpenPaaSDBClient 默认DB实例 - OpenPaaSDBClient *database.DBClient -) - -var openPaaSDBClientOnce sync.Once - -// InitOpenPaaSDBClients ... -func InitOpenPaaSDBClients(openPaaSDBConfig *config.Database) { - if OpenPaaSDBClient == nil { - openPaaSDBClientOnce.Do(func() { - OpenPaaSDBClient = database.NewDBClient(openPaaSDBConfig) - if err := OpenPaaSDBClient.Connect(); err != nil { - panic(err) - } - }) - } -} - -// GetOpenPaaSDBClient 获取默认的DB实例 -func GetOpenPaaSDBClient() *database.DBClient { - return OpenPaaSDBClient -} diff --git a/src/bkauth/pkg/sync/service.go b/src/bkauth/pkg/sync/service.go deleted file mode 100644 index a18f422..0000000 --- a/src/bkauth/pkg/sync/service.go +++ /dev/null @@ -1,104 +0,0 @@ -/* - * TencentBlueKing is pleased to support the open source community by making - * 蓝鲸智云 - Auth服务(BlueKing - Auth) available. - * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. - * Licensed under the MIT License (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * 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. - * - * We undertake not to change the open source license (MIT license) applicable - * to the current version of the project delivered to anyone in the future. - */ - -package sync - -import ( - "bkauth/pkg/errorx" -) - -const ( - OpenPaaSAccessKeySVC = "OpenPaaSAccessKeySVC" -) - -type OpenPaaSAccessKey struct { - AppCode string - AppSecret string -} - -type OpenPaaSService interface { - List() ([]OpenPaaSAccessKey, error) - Create(appCode, appSecret string) error -} - -type openPaaSService struct { - manager OpenPaaSManager -} - -func NewOpenPaaSService() OpenPaaSService { - return &openPaaSService{ - manager: NewOpenPaaSManager(), - } -} - -func (s *openPaaSService) List() (openPaaSAccessKeys []OpenPaaSAccessKey, err error) { - errorWrapf := errorx.NewLayerFunctionErrorWrapf(OpenPaaSAccessKeySVC, "List") - apps, err := s.manager.ListBKPaaSApp() - if err != nil { - return openPaaSAccessKeys, errorWrapf(err, "manager.ListBKPaaSApp fail") - } - - esbAccounts, err := s.manager.ListESBAppAccount() - if err != nil { - return openPaaSAccessKeys, errorWrapf(err, "manager.ListESBAppAccount fail") - } - - openPaaSAccessKeys = make([]OpenPaaSAccessKey, 0, len(apps)+len(esbAccounts)) - - for _, app := range apps { - openPaaSAccessKeys = append(openPaaSAccessKeys, OpenPaaSAccessKey{ - AppCode: app.Code, - AppSecret: app.AuthToken, - }) - } - - for _, esbAccount := range esbAccounts { - openPaaSAccessKeys = append(openPaaSAccessKeys, OpenPaaSAccessKey{ - AppCode: esbAccount.AppCode, - AppSecret: esbAccount.AppToken, - }) - } - - return -} - -func (s *openPaaSService) Create(appCode, appSecret string) (err error) { - errorWrapf := errorx.NewLayerFunctionErrorWrapf(OpenPaaSAccessKeySVC, "Create") - - // 1. 判断是否在paas_app表里,存在auth_token为NULL 或者 空字符串的,若存在,则更新 - exists, err := s.manager.AuthTokenEmptyExists(appCode) - if err != nil { - return errorWrapf(err, "manager.AuthTokenEmptyExists appCode=`%s` fail", appCode) - } - // paas_app里存在,但是auth_token为空,则更新 - if exists { - err = s.manager.UpdateBKPaaSApp(appCode, appSecret) - if err != nil { - return errorWrapf(err, "manager.UpdateBKPaaSApp appCode=`%s` fail", appCode) - } - return - - } - - // 2. 直接添加到esb_app_account表里 - err = s.manager.CreateESBAppAccount(appCode, appSecret) - if err != nil { - return errorWrapf(err, "manager.CreateESBAppAccount appCode=`%s` fail", appCode) - } - return -} diff --git a/src/bkauth/pkg/sync/sync.go b/src/bkauth/pkg/sync/sync.go deleted file mode 100644 index 1fb8f59..0000000 --- a/src/bkauth/pkg/sync/sync.go +++ /dev/null @@ -1,122 +0,0 @@ -/* - * TencentBlueKing is pleased to support the open source community by making - * 蓝鲸智云 - Auth服务(BlueKing - Auth) available. - * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. - * Licensed under the MIT License (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * 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. - * - * We undertake not to change the open source license (MIT license) applicable - * to the current version of the project delivered to anyone in the future. - */ - -package sync - -import ( - "go.uber.org/zap" - - "bkauth/pkg/service" - "bkauth/pkg/service/types" - "bkauth/pkg/util" -) - -const ( - CreatedSource = "auto_sync" -) - -// Sync : 双向同步,且仅仅同步增量数据 -func Sync() { - // 1. 查询OpenPaaS里的数据 - openPaaSSvc := NewOpenPaaSService() - openPaaSAccessKeys, err := openPaaSSvc.List() - if err != nil { - zap.S().Errorf("openPaaSSvc List fail, error=`+%v`", err) - return - } - openPaaSAccessKeySet := NewAccessKeySet() - for _, openPaaSAccessKey := range openPaaSAccessKeys { - openPaaSAccessKeySet.Add(openPaaSAccessKey.AppCode, openPaaSAccessKey.AppSecret) - } - - // 2. 查询BKAuth里的数据 - accessKeySvc := service.NewAccessKeyService() - accessKeys, err := accessKeySvc.List() - if err != nil { - zap.S().Errorf("accessKeySvc List fail, error=`%v`", err) - return - } - accessKeySet := NewAccessKeySet() - for _, accessKey := range accessKeys { - accessKeySet.Add(accessKey.AppCode, accessKey.AppSecret) - } - - // 3. 查询已存在的App - appSvc := service.NewAppService() - apps, err := appSvc.List() - if err != nil { - zap.S().Errorf("appSvc List fail, error=`+%v`", err) - return - } - appCodeSet := util.NewStringSet() - for _, app := range apps { - appCodeSet.Add(app.Code) - } - - // 4. 将OpenPaaS的数据增量数据同步到BKAuth - // 检查App是否存在,若不存在,直接创建App,若存在,则再检查appSecret是否存在 - for _, openPaaSAccessKey := range openPaaSAccessKeys { - appCode := openPaaSAccessKey.AppCode - appSecret := openPaaSAccessKey.AppSecret - // App不存在则创建 - if !appCodeSet.Has(appCode) { - err = appSvc.CreateWithSecret( - types.App{Code: appCode, Name: appCode, Description: appCode}, - appSecret, - CreatedSource, - ) - if err != nil { - zap.S().Errorf("appSvc.CreateWithSecret appCode=%s fail, error=`+%v`", appCode, err) - return - } - // Note: 这里需要对创建后的数据进行添加,否则若遇到一个AppCode对应多个Secret时,第二次创建必然会失败 - appCodeSet.Add(appCode) - accessKeySet.Add(appCode, appSecret) - continue - } - // App存在,则判断appSecret是否存在,若不存在则新增,存在则忽略 - if accessKeySet.Has(appCode, appSecret) { - continue - } - err = accessKeySvc.CreateWithSecret(appCode, appSecret, CreatedSource) - if err != nil { - zap.S().Errorf("accessKeySvc.CreateWithSecret appCode=%s fail, error=`+%v`", appCode, err) - return - } - // Note: 为避免重复数据的出现,需要记录已存在 - accessKeySet.Add(appCode, appSecret) - } - - // 5. 将BKAuth的数据增量数据同步到OpenPaaS - for _, accessKey := range accessKeys { - appCode := accessKey.AppCode - appSecret := accessKey.AppSecret - // 已存在则忽略 - if openPaaSAccessKeySet.Has(appCode, appSecret) { - continue - } - // 创建 - err = openPaaSSvc.Create(appCode, appSecret) - if err != nil { - zap.S().Errorf("openPaaSSvc.Create appCode=%s fail, error=`+%v`", appCode, err) - return - } - // Note: 为避免重复数据的出现,需要记录已存在 - openPaaSAccessKeySet.Add(appCode, appSecret) - } -} diff --git a/src/bkauth/pkg/util/constant.go b/src/bkauth/pkg/util/constant.go index 2446354..77bdb28 100644 --- a/src/bkauth/pkg/util/constant.go +++ b/src/bkauth/pkg/util/constant.go @@ -1,6 +1,6 @@ /* * TencentBlueKing is pleased to support the open source community by making - * 蓝鲸智云 - Auth服务(BlueKing - Auth) available. + * 蓝鲸智云 - Auth 服务 (BlueKing - Auth) available. * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -23,7 +23,16 @@ const ( RequestIDKey = "request_id" RequestIDHeaderKey = "X-Request-Id" - AccessAppCodeKey = "access_app_code" + AccessAppCodeKey = "access_app_code" + EnableMultiTenantModeKey = "enable_multi_tenant_mode" ErrorIDKey = "err" + + // 应用在租户层的可用模式:全租户 + TenantModeGlobal = "global" + // 应用在租户层的可用模式:单租户 + TenantModeSingle = "single" + + // 单租户模式下,默认租户 id 为 default + TenantIDDefault = "default" ) diff --git a/src/bkauth/pkg/util/request.go b/src/bkauth/pkg/util/request.go index abf74a0..09360ff 100644 --- a/src/bkauth/pkg/util/request.go +++ b/src/bkauth/pkg/util/request.go @@ -70,3 +70,11 @@ func GetError(c *gin.Context) (interface{}, bool) { func SetError(c *gin.Context, err error) { c.Set(ErrorIDKey, err) } + +func SetEnableMultiTenantMode(c *gin.Context, enableMultiTenantMode bool) { + c.Set(EnableMultiTenantModeKey, enableMultiTenantMode) +} + +func GetEnableMultiTenantMode(c *gin.Context) bool { + return c.GetBool(EnableMultiTenantModeKey) +} diff --git a/src/bkauth/sql_migrations/0004_20241120_1030.sql b/src/bkauth/sql_migrations/0004_20241120_1030.sql new file mode 100644 index 0000000..c0cf554 --- /dev/null +++ b/src/bkauth/sql_migrations/0004_20241120_1030.sql @@ -0,0 +1,23 @@ +-- TencentBlueKing is pleased to support the open source community by making +-- 蓝鲸智云 - Auth服务(BlueKing - Auth) available. +-- Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. +-- Licensed under the MIT License (the "License"); you may not use this file except +-- in compliance with the License. You may obtain a copy of the License at +-- http://opensource.org/licenses/MIT +-- 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. +-- We undertake not to change the open source license (MIT license) applicable +-- to the current version of the project delivered to anyone in the future. + +-- add fields +ALTER TABLE `bkauth`.`app` ADD COLUMN tenant_mode VARCHAR(32) NULL; +ALTER TABLE `bkauth`.`app` ADD COLUMN tenant_id VARCHAR(32) NULL; + +-- update legacy data +UPDATE `bkauth`.`app` SET tenant_mode = 'single', tenant_id = 'default' WHERE tenant_mode IS NULL; + +-- update fields +ALTER TABLE `bkauth`.`app` MODIFY COLUMN tenant_mode VARCHAR(32) NOT NULL COMMENT 'global or single'; +ALTER TABLE `bkauth`.`app` MODIFY COLUMN tenant_id VARCHAR(32) NOT NULL COMMENT 'empty or specific tenant_id'; diff --git a/src/bkauth/test/bkauth/apps/create_conflict.bru b/src/bkauth/test/bkauth/apps/create_conflict.bru new file mode 100644 index 0000000..a0f07fe --- /dev/null +++ b/src/bkauth/test/bkauth/apps/create_conflict.bru @@ -0,0 +1,33 @@ +meta { + name: create_conflict + type: http + seq: 7 +} + +post { + url: http://{{host}}:{{port}}/api/v1/apps + body: json + auth: none +} + +headers { + X-Bk-App-Code: {{x_bk_app_code}} + X-Bk-App-Secret: {{x_bk_app_secret}} +} + +body:json { + { + "bk_app_code": "demo", + "name": "demo", + "bk_tenant": { + "mode": "single", + "id": "default" + } + } +} + +assert { + res.status: eq 409 + res.body.code: eq 1903409 + res.body.message: eq conflict:app(demo) already exists +} diff --git a/src/bkauth/test/bkauth/apps/create_fail_app_code_invalid.bru b/src/bkauth/test/bkauth/apps/create_fail_app_code_invalid.bru new file mode 100644 index 0000000..f0009ff --- /dev/null +++ b/src/bkauth/test/bkauth/apps/create_fail_app_code_invalid.bru @@ -0,0 +1,33 @@ +meta { + name: create_fail_app_code_invalid + type: http + seq: 3 +} + +post { + url: http://{{host}}:{{port}}/api/v1/apps + body: json + auth: none +} + +headers { + X-Bk-App-Code: {{x_bk_app_code}} + X-Bk-App-Secret: {{x_bk_app_secret}} +} + +body:json { + { + "bk_app_code": "1$", + "name": "demo", + "bk_tenant": { + "mode": "single", + "id": "default" + } + } +} + +assert { + res.status: eq 400 + res.body.code: eq 1903400 + res.body.message: eq bad request:invalid app_code: app_code should begin with a lowercase letter or numbers, contains lowercase letters(a-z), numbers(0-9), underline(_) or hyphen(-), length should be 1 to 32 letters +} diff --git a/src/bkauth/test/bkauth/apps/create_fail_body_empty.bru b/src/bkauth/test/bkauth/apps/create_fail_body_empty.bru new file mode 100644 index 0000000..9d24ee2 --- /dev/null +++ b/src/bkauth/test/bkauth/apps/create_fail_body_empty.bru @@ -0,0 +1,26 @@ +meta { + name: create_fail_body_empty + type: http + seq: 2 +} + +post { + url: http://{{host}}:{{port}}/api/v1/apps + body: json + auth: none +} + +headers { + X-Bk-App-Code: {{x_bk_app_code}} + X-Bk-App-Secret: {{x_bk_app_secret}} +} + +body:json { + {} +} + +assert { + res.status: eq 400 + res.body.code: eq 1903400 + res.body.message: eq bad request:AppCode is required +} diff --git a/src/bkauth/test/bkauth/apps/create_fail_no_auth.bru b/src/bkauth/test/bkauth/apps/create_fail_no_auth.bru new file mode 100644 index 0000000..5176759 --- /dev/null +++ b/src/bkauth/test/bkauth/apps/create_fail_no_auth.bru @@ -0,0 +1,17 @@ +meta { + name: create_fail_no_auth + type: http + seq: 1 +} + +post { + url: http://{{host}}:{{port}}/api/v1/apps + body: none + auth: none +} + +assert { + res.status: eq 401 + res.body.code: eq 1903401 + res.body.message: eq unauthorized:app code and app secret required +} diff --git a/src/bkauth/test/bkauth/apps/create_fail_tenant_id_invalid.bru b/src/bkauth/test/bkauth/apps/create_fail_tenant_id_invalid.bru new file mode 100644 index 0000000..6f1d34b --- /dev/null +++ b/src/bkauth/test/bkauth/apps/create_fail_tenant_id_invalid.bru @@ -0,0 +1,33 @@ +meta { + name: create_fail_tenant_id_invalid + type: http + seq: 5 +} + +post { + url: http://{{host}}:{{port}}/api/v1/apps + body: json + auth: none +} + +headers { + X-Bk-App-Code: {{x_bk_app_code}} + X-Bk-App-Secret: {{x_bk_app_secret}} +} + +body:json { + { + "bk_app_code": "1", + "name": "demo", + "bk_tenant": { + "mode": "single", + "id": "1a" + } + } +} + +assert { + res.status: eq 400 + res.body.code: eq 1903400 + res.body.message: eq bad request:invalid bk_tenant.id: bk_tenant_id should begin with a letter, contains letters(a-zA-Z), numbers(0-9) or hyphen(-), length should be 2 to 32 +} diff --git a/src/bkauth/test/bkauth/apps/create_fail_tenant_type_invalid.bru b/src/bkauth/test/bkauth/apps/create_fail_tenant_type_invalid.bru new file mode 100644 index 0000000..c309ca6 --- /dev/null +++ b/src/bkauth/test/bkauth/apps/create_fail_tenant_type_invalid.bru @@ -0,0 +1,33 @@ +meta { + name: create_fail_tenant_mode_invalid + type: http + seq: 4 +} + +post { + url: http://{{host}}:{{port}}/api/v1/apps + body: json + auth: none +} + +headers { + X-Bk-App-Code: {{x_bk_app_code}} + X-Bk-App-Secret: {{x_bk_app_secret}} +} + +body:json { + { + "bk_app_code": "1", + "name": "demo", + "bk_tenant": { + "mode": "abc", + "id": "default" + } + } +} + +assert { + res.status: eq 400 + res.body.code: eq 1903400 + res.body.message: eq bad request:Mode must be one of 'global single' +} diff --git a/src/bkauth/test/bkauth/apps/create_ok.bru b/src/bkauth/test/bkauth/apps/create_ok.bru new file mode 100644 index 0000000..c0382ed --- /dev/null +++ b/src/bkauth/test/bkauth/apps/create_ok.bru @@ -0,0 +1,35 @@ +meta { + name: create_ok + type: http + seq: 6 +} + +post { + url: http://{{host}}:{{port}}/api/v1/apps + body: json + auth: none +} + +headers { + X-Bk-App-Code: {{x_bk_app_code}} + X-Bk-App-Secret: {{x_bk_app_secret}} +} + +body:json { + { + "bk_app_code": "demo", + "name": "demo", + "bk_tenant": { + "mode": "global", + "id": "" + } + } +} + +assert { + res.status: eq 200 + res.body.code: eq 0 + res.body.data.bk_app_code: eq demo + res.body.data.bk_tenant.mode: eq single + res.body.data.bk_tenant.id: eq default +} diff --git a/src/bkauth/test/bkauth/apps/get_app_fail_not_exists.bru b/src/bkauth/test/bkauth/apps/get_app_fail_not_exists.bru new file mode 100644 index 0000000..c587133 --- /dev/null +++ b/src/bkauth/test/bkauth/apps/get_app_fail_not_exists.bru @@ -0,0 +1,22 @@ +meta { + name: get_app_fail_not_exists + type: http + seq: 8 +} + +get { + url: http://{{host}}:{{port}}/api/v1/apps/not_exists + body: json + auth: none +} + +headers { + X-Bk-App-Code: {{x_bk_app_code}} + X-Bk-App-Secret: {{x_bk_app_secret}} +} + +assert { + res.status: eq 404 + res.body.code: eq 1903404 + res.body.message: eq not found:App(not_exists) not exists +} diff --git a/src/bkauth/test/bkauth/apps/get_app_ok.bru b/src/bkauth/test/bkauth/apps/get_app_ok.bru new file mode 100644 index 0000000..6dba531 --- /dev/null +++ b/src/bkauth/test/bkauth/apps/get_app_ok.bru @@ -0,0 +1,24 @@ +meta { + name: get_app_ok + type: http + seq: 9 +} + +get { + url: http://{{host}}:{{port}}/api/v1/apps/demo + body: json + auth: none +} + +headers { + X-Bk-App-Code: {{x_bk_app_code}} + X-Bk-App-Secret: {{x_bk_app_secret}} +} + +assert { + res.status: eq 200 + res.body.message: eq ok + res.body.data.bk_app_code: eq demo + res.body.data.bk_tenant.mode: eq global + res.body.data.bk_tenant.id: eq +} diff --git a/src/bkauth/test/bkauth/apps/list_apps.bru b/src/bkauth/test/bkauth/apps/list_apps.bru new file mode 100644 index 0000000..47d687c --- /dev/null +++ b/src/bkauth/test/bkauth/apps/list_apps.bru @@ -0,0 +1,23 @@ +meta { + name: list_apps + type: http + seq: 10 +} + +get { + url: http://{{host}}:{{port}}/api/v1/apps + body: json + auth: none +} + +headers { + X-Bk-App-Code: {{x_bk_app_code}} + X-Bk-App-Secret: {{x_bk_app_secret}} +} + +assert { + res.status: eq 200 + res.body.message: eq ok + res.body.data.count: eq 3 + res.body.data.results: length 3 +} diff --git a/src/bkauth/test/bkauth/apps/list_apps_pagination.bru b/src/bkauth/test/bkauth/apps/list_apps_pagination.bru new file mode 100644 index 0000000..0ecb27d --- /dev/null +++ b/src/bkauth/test/bkauth/apps/list_apps_pagination.bru @@ -0,0 +1,28 @@ +meta { + name: list_apps_pagination + type: http + seq: 11 +} + +get { + url: http://{{host}}:{{port}}/api/v1/apps?page=2&page_size=2 + body: json + auth: none +} + +params:query { + page: 2 + page_size: 2 +} + +headers { + X-Bk-App-Code: {{x_bk_app_code}} + X-Bk-App-Secret: {{x_bk_app_secret}} +} + +assert { + res.status: eq 200 + res.body.message: eq ok + res.body.data.count: eq 3 + res.body.data.results: length 1 +} diff --git a/src/bkauth/test/bkauth/apps/list_apps_tenant_mode_global.bru b/src/bkauth/test/bkauth/apps/list_apps_tenant_mode_global.bru new file mode 100644 index 0000000..387ed85 --- /dev/null +++ b/src/bkauth/test/bkauth/apps/list_apps_tenant_mode_global.bru @@ -0,0 +1,26 @@ +meta { + name: list_apps_tenant_mode_global + type: http + seq: 12 +} + +get { + url: http://{{host}}:{{port}}/api/v1/apps?tenant_mode=global + body: json + auth: none +} + +params:query { + tenant_mode: global +} + +headers { + X-Bk-App-Code: {{x_bk_app_code}} + X-Bk-App-Secret: {{x_bk_app_secret}} +} + +assert { + res.status: eq 200 + res.body.message: eq ok + res.body.data.count: eq 1 +} diff --git a/src/bkauth/test/bkauth/apps/list_apps_tenant_mode_single.bru b/src/bkauth/test/bkauth/apps/list_apps_tenant_mode_single.bru new file mode 100644 index 0000000..6f6f18b --- /dev/null +++ b/src/bkauth/test/bkauth/apps/list_apps_tenant_mode_single.bru @@ -0,0 +1,27 @@ +meta { + name: list_apps_tenant_mode_single + type: http + seq: 13 +} + +get { + url: http://{{host}}:{{port}}/api/v1/apps?tenant_mode=single&tenant_id=default + body: json + auth: none +} + +params:query { + tenant_mode: single + tenant_id: default +} + +headers { + X-Bk-App-Code: {{x_bk_app_code}} + X-Bk-App-Secret: {{x_bk_app_secret}} +} + +assert { + res.status: eq 200 + res.body.message: eq ok + res.body.data.count: eq 2 +} diff --git a/src/bkauth/test/bkauth/bruno.json b/src/bkauth/test/bkauth/bruno.json new file mode 100644 index 0000000..d8e38cc --- /dev/null +++ b/src/bkauth/test/bkauth/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "bkauth", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/src/bkauth/test/bkauth/environments/dev.bru.tpl b/src/bkauth/test/bkauth/environments/dev.bru.tpl new file mode 100644 index 0000000..7b7954b --- /dev/null +++ b/src/bkauth/test/bkauth/environments/dev.bru.tpl @@ -0,0 +1,6 @@ +vars { + host: __IP__ + port: 8080 + x_bk_app_code: bk_paas3 + x_bk_app_secret: G3dsdftR9nGQM8WnF1qwjGSVE0ScXrz1hKWM +}