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

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


The following commit(s) were added to refs/heads/main by this push:
     new 19b35ab15 feat(api-keys): support api keys management (#5749)
19b35ab15 is described below

commit 19b35ab153b814e050cf7d6cc8a19343e581b475
Author: Linwei <[email protected]>
AuthorDate: Wed Aug 2 10:21:59 2023 +0800

    feat(api-keys): support api keys management (#5749)
    
    * feat(api-keys): add CRUD apis for api keys management
    
    * feat(api-keys): add authentication middleware
    
    * feat(api-keys): set api keys used by webhook never expire
    
    * feat(api-keys): add user/email to api keys
    
    * feat(api-keys): add user info from http basic auth
    
    * feat(api-keys): fix discussions in PR, make up functions about api keys
    
    * feat(api-keys): fix discussions, update package structure
    
    * feat(api-keys): fix discussions, make plenty of update
    
    * feat(api-keys): set alias for `ApiOutputApiKey`
    
    * feat(api-keys): simplify package `services`'s logic, remove them to 
`apikeyhelper`
    
    * feat(api-keys): update comments
    
    * feat(api-keys): remove `tx` when deleting api key
    
    * feat(api-keys): merge `apikeyhelper.Create` and 
`apikeyhelper.CreateForPlugin`
    
    * feat(api-keys): fix golangci-lint errors and warns, rename `digestApiKey` 
to `generateApiKey`
---
 .asf.yaml                                          |   1 +
 backend/core/models/api_key.go                     |  49 +++++
 backend/core/models/common/base.go                 |  19 ++
 .../20230725_add_api_key_tables.go                 |  63 ++++++
 .../core/models/migrationscripts/archived/base.go  |  10 +
 backend/core/models/migrationscripts/register.go   |   1 +
 backend/core/plugin/plugin_api.go                  |   3 +
 backend/helpers/apikeyhelper/apikeyhelper.go       | 226 +++++++++++++++++++++
 .../helpers/pluginhelper/api/connection_helper.go  |  22 +-
 backend/impls/dalgorm/dalgorm.go                   |   1 -
 backend/plugins/webhook/api/connection.go          |  53 ++++-
 backend/plugins/webhook/api/init.go                |   9 +-
 backend/plugins/webhook/models/connection.go       |   6 +
 backend/server/api/api.go                          |   7 +-
 backend/server/api/apikeys/apikeys.go              | 141 +++++++++++++
 backend/server/api/middlewares.go                  | 202 ++++++++++++++++++
 backend/server/api/router.go                       |  30 ++-
 .../api/shared/gin_utils.go}                       |  18 +-
 backend/server/services/apikeys.go                 | 115 +++++++++++
 backend/server/services/base.go                    |   4 +-
 20 files changed, 949 insertions(+), 31 deletions(-)

diff --git a/.asf.yaml b/.asf.yaml
index 577510739..a2e65d3a7 100644
--- a/.asf.yaml
+++ b/.asf.yaml
@@ -77,6 +77,7 @@ github:
     - mintsweet
     - keon94
     - CamilleTeruel
+    - d4x1
 
 notifications:
   commits: [email protected]
diff --git a/backend/core/models/api_key.go b/backend/core/models/api_key.go
new file mode 100644
index 000000000..c567c4826
--- /dev/null
+++ b/backend/core/models/api_key.go
@@ -0,0 +1,49 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+       "github.com/apache/incubator-devlake/core/models/common"
+       "time"
+)
+
+// ApiKey is the basic of api key management.
+type ApiKey struct {
+       common.Model
+       common.Creator
+       common.Updater
+       Name        string     `json:"name"`
+       ApiKey      string     `json:"apiKey"`
+       ExpiredAt   *time.Time `json:"expiredAt"`
+       AllowedPath string     `json:"allowedPath"`
+       Type        string     `json:"type"`
+       Extra       string     `json:"extra"`
+}
+
+func (ApiKey) TableName() string {
+       return "_devlake_api_keys"
+}
+
+type ApiInputApiKey struct {
+       Name        string     `json:"name" validate:"required,max=255"`
+       Type        string     `json:"type" validate:"required"`
+       AllowedPath string     `json:"allowedPath" validate:"required"`
+       ExpiredAt   *time.Time `json:"expiredAt" validate:"required"`
+}
+
+type ApiOutputApiKey = ApiKey
diff --git a/backend/core/models/common/base.go 
b/backend/core/models/common/base.go
index e33631735..f2165d8dd 100644
--- a/backend/core/models/common/base.go
+++ b/backend/core/models/common/base.go
@@ -22,12 +22,31 @@ import (
        "time"
 )
 
+const (
+       USER = "user"
+)
+
+type User struct {
+       Name  string
+       Email string
+}
+
 type Model struct {
        ID        uint64    `gorm:"primaryKey" json:"id"`
        CreatedAt time.Time `json:"createdAt"`
        UpdatedAt time.Time `json:"updatedAt"`
 }
 
+type Creator struct {
+       Creator      string `json:"creator"`
+       CreatorEmail string `json:"creatorEmail"`
+}
+
+type Updater struct {
+       Updater      string `json:"updater"`
+       UpdaterEmail string `json:"updater_email"`
+}
+
 type ScopeConfig struct {
        Model
        Entities []string `gorm:"type:json;serializer:json" json:"entities" 
mapstructure:"entities"`
diff --git 
a/backend/core/models/migrationscripts/20230725_add_api_key_tables.go 
b/backend/core/models/migrationscripts/20230725_add_api_key_tables.go
new file mode 100644
index 000000000..5ac63e2b5
--- /dev/null
+++ b/backend/core/models/migrationscripts/20230725_add_api_key_tables.go
@@ -0,0 +1,63 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+       "github.com/apache/incubator-devlake/core/context"
+       "github.com/apache/incubator-devlake/core/errors"
+       
"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/migrationhelper"
+       "time"
+)
+
+var _ plugin.MigrationScript = (*addApiKeyTables)(nil)
+
+type addApiKeyTables struct{}
+
+type apiKey20230728 struct {
+       archived.Model
+       archived.Creator
+       archived.Updater
+       Name        string     `json:"name" 
gorm:"type:varchar(255);uniqueIndex"`
+       ApiKey      string     `json:"apiKey" 
gorm:"type:varchar(255);column:api_key;uniqueIndex"`
+       ExpiredAt   *time.Time `json:"expiredAt" gorm:"column:expired_at"`
+       AllowedPath string     `json:"allowedPath" 
gorm:"type:varchar(255);column:allowed_path"`
+       Type        string     `json:"type" 
gorm:"type:varchar(40);column:type;index"`
+       Extra       string     `json:"extra" 
gorm:"type:varchar(255);column:extra;index"`
+}
+
+func (apiKey20230728) TableName() string {
+       return "_devlake_api_keys"
+}
+
+func (script *addApiKeyTables) Up(basicRes context.BasicRes) errors.Error {
+       // To create multiple tables with migration helper
+       return migrationhelper.AutoMigrateTables(
+               basicRes,
+               &apiKey20230728{},
+       )
+}
+
+func (*addApiKeyTables) Version() uint64 {
+       return 20230725142900
+}
+
+func (*addApiKeyTables) Name() string {
+       return "add api key tables"
+}
diff --git a/backend/core/models/migrationscripts/archived/base.go 
b/backend/core/models/migrationscripts/archived/base.go
index 2e30cf1d1..86bfeef06 100644
--- a/backend/core/models/migrationscripts/archived/base.go
+++ b/backend/core/models/migrationscripts/archived/base.go
@@ -61,3 +61,13 @@ type RawDataOrigin struct {
        // we can store record index into this field, which is helpful for 
debugging
        RawDataRemark string `gorm:"column:_raw_data_remark" 
json:"_raw_data_remark"`
 }
+
+type Creator struct {
+       Creator      string `json:"creator" 
gorm:"type:varchar(255);column:creator"`
+       CreatorEmail string `json:"creatorEmail" 
gorm:"type:varchar(255);column:creator_email"`
+}
+
+type Updater struct {
+       Updater      string `json:"updater" 
gorm:"type:varchar(255);column:updater"`
+       UpdaterEmail string `json:"updater_email" 
gorm:"type:varchar(255);column:updater_email"`
+}
diff --git a/backend/core/models/migrationscripts/register.go 
b/backend/core/models/migrationscripts/register.go
index 3aad7517b..248260244 100644
--- a/backend/core/models/migrationscripts/register.go
+++ b/backend/core/models/migrationscripts/register.go
@@ -86,6 +86,7 @@ func All() []plugin.MigrationScript {
                new(modifyPrLabelsAndComments),
                new(renameFinishedCommitsDiffs),
                new(addUpdatedDateToIssueComments),
+               new(addApiKeyTables),
                new(addIssueRelationship),
        }
 }
diff --git a/backend/core/plugin/plugin_api.go 
b/backend/core/plugin/plugin_api.go
index 9ee36c2ff..5b0c613ea 100644
--- a/backend/core/plugin/plugin_api.go
+++ b/backend/core/plugin/plugin_api.go
@@ -19,6 +19,7 @@ package plugin
 
 import (
        "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/models/common"
        "net/http"
        "net/url"
 )
@@ -29,6 +30,8 @@ type ApiResourceInput struct {
        Query   url.Values             // query string
        Body    map[string]interface{} // json body
        Request *http.Request
+
+       User *common.User
 }
 
 // GetPlugin get the plugin in context
diff --git a/backend/helpers/apikeyhelper/apikeyhelper.go 
b/backend/helpers/apikeyhelper/apikeyhelper.go
new file mode 100644
index 000000000..53b9fcde5
--- /dev/null
+++ b/backend/helpers/apikeyhelper/apikeyhelper.go
@@ -0,0 +1,226 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package apikeyhelper
+
+import (
+       "crypto/hmac"
+       "crypto/sha256"
+       "fmt"
+       "github.com/apache/incubator-devlake/core/config"
+       "github.com/apache/incubator-devlake/core/context"
+       "github.com/apache/incubator-devlake/core/dal"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/log"
+       "github.com/apache/incubator-devlake/core/models"
+       common "github.com/apache/incubator-devlake/core/models/common"
+       "github.com/apache/incubator-devlake/core/utils"
+       "github.com/spf13/viper"
+       "regexp"
+       "strings"
+       "time"
+)
+
+const (
+       EncodeKeyEnvStr = "ENCRYPTION_SECRET"
+       apiKeyLen       = 128
+)
+
+type ApiKeyHelper struct {
+       basicRes         context.BasicRes
+       cfg              *viper.Viper
+       logger           log.Logger
+       encryptionSecret string
+}
+
+func NewApiKeyHelper(basicRes context.BasicRes, logger log.Logger) 
*ApiKeyHelper {
+       cfg := config.GetConfig()
+       encryptionSecret := strings.TrimSpace(cfg.GetString(EncodeKeyEnvStr))
+       if encryptionSecret == "" {
+               panic("ENCRYPTION_SECRET must be set in environment variable or 
.env file")
+       }
+       return &ApiKeyHelper{
+               basicRes:         basicRes,
+               cfg:              cfg,
+               logger:           logger,
+               encryptionSecret: encryptionSecret,
+       }
+}
+
+func (c *ApiKeyHelper) Create(tx dal.Transaction, user *common.User, name 
string, expiredAt *time.Time, allowedPath string, apiKeyType string, extra 
string) (*models.ApiKey, errors.Error) {
+       if _, err := regexp.Compile(allowedPath); err != nil {
+               c.logger.Error(err, "Compile allowed path")
+               return nil, errors.Default.Wrap(err, fmt.Sprintf("compile 
allowed path: %s", allowedPath))
+       }
+       apiKey, hashedApiKey, err := c.generateApiKey()
+       if err != nil {
+               c.logger.Error(err, "generateApiKey")
+               return nil, err
+       }
+       now := time.Now()
+       apiKeyRecord := &models.ApiKey{
+               Model: common.Model{
+                       CreatedAt: now,
+                       UpdatedAt: now,
+               },
+               Name:        name,
+               ApiKey:      hashedApiKey,
+               ExpiredAt:   expiredAt,
+               AllowedPath: allowedPath,
+               Type:        apiKeyType,
+               Extra:       extra,
+       }
+       if user != nil {
+               apiKeyRecord.Creator = common.Creator{
+                       Creator:      user.Name,
+                       CreatorEmail: user.Email,
+               }
+               apiKeyRecord.Updater = common.Updater{
+                       Updater:      user.Name,
+                       UpdaterEmail: user.Email,
+               }
+       }
+       if err := tx.Create(apiKeyRecord); err != nil {
+               c.logger.Error(err, "create api key record")
+               if tx.IsDuplicationError(err) {
+                       return nil, errors.BadInput.New(fmt.Sprintf("An api key 
with name [%s] has already exists", name))
+               }
+               return nil, errors.Default.Wrap(err, "error creating DB api 
key")
+       }
+       apiKeyRecord.ApiKey = apiKey
+       return apiKeyRecord, nil
+}
+
+func (c *ApiKeyHelper) CreateForPlugin(tx dal.Transaction, user *common.User, 
name string, pluginName string, allowedPath string, extra string) 
(*models.ApiKey, errors.Error) {
+       return c.Create(tx, user, name, nil, fmt.Sprintf("plugin:%s", 
pluginName), allowedPath, extra)
+}
+
+func (c *ApiKeyHelper) Put(user *common.User, id uint64) (*models.ApiKey, 
errors.Error) {
+       db := c.basicRes.GetDal()
+       // verify exists
+       apiKey, err := c.getApiKeyById(db, id)
+       if err != nil {
+               c.logger.Error(err, "get api key by id: %d", id)
+               return nil, err
+       }
+
+       apiKeyStr, hashApiKey, err := c.generateApiKey()
+       if err != nil {
+               c.logger.Error(err, "generateApiKey")
+               return nil, err
+       }
+       apiKey.ApiKey = hashApiKey
+       apiKey.UpdatedAt = time.Now()
+       if user != nil {
+               apiKey.Updater = common.Updater{
+                       Updater:      user.Name,
+                       UpdaterEmail: user.Email,
+               }
+       }
+       if err = db.Update(apiKey); err != nil {
+               c.logger.Error(err, "update api key, id: %d", id)
+               return nil, errors.Default.Wrap(err, "error deleting api key")
+       }
+       apiKey.ApiKey = apiKeyStr
+       return apiKey, nil
+}
+
+func (c *ApiKeyHelper) Delete(id uint64) errors.Error {
+       // verify exists
+       db := c.basicRes.GetDal()
+       _, err := c.getApiKeyById(db, id)
+       if err != nil {
+               c.logger.Error(err, "get api key by id: %d", id)
+               return err
+       }
+       err = db.Delete(&models.ApiKey{}, dal.Where("id = ?", id))
+       if err != nil {
+               c.logger.Error(err, "delete api key, id: %d", id)
+               return errors.Default.Wrap(err, "error deleting api key")
+       }
+       return nil
+}
+
+func (c *ApiKeyHelper) DeleteForPlugin(tx dal.Transaction, pluginName string, 
extra string) errors.Error {
+       // delete api key generated by plugin, for example webhook
+       var apiKey models.ApiKey
+       var clauses []dal.Clause
+       if pluginName != "" {
+               clauses = append(clauses, dal.Where("type = ?", 
fmt.Sprintf("plugin:%s", pluginName)))
+       }
+       if extra != "" {
+               clauses = append(clauses, dal.Where("extra = ?", extra))
+       }
+       if err := tx.First(&apiKey, clauses...); err != nil {
+               c.logger.Error(err, "query api key record")
+               // if api key doesn't exist, just return success
+               if tx.IsErrorNotFound(err.Unwrap()) {
+                       return nil
+               } else {
+                       return err
+               }
+       }
+       if err := tx.Delete(apiKey); err != nil {
+               c.logger.Error(err, "delete api key record")
+               return err
+       }
+       return nil
+}
+
+func (c *ApiKeyHelper) getApiKeyById(tx dal.Dal, id uint64, additionalClauses 
...dal.Clause) (*models.ApiKey, errors.Error) {
+       if tx == nil {
+               tx = c.basicRes.GetDal()
+       }
+       apiKey := &models.ApiKey{}
+       err := tx.First(apiKey, append([]dal.Clause{dal.Where("id = ?", id)}, 
additionalClauses...)...)
+       if err != nil {
+               if tx.IsErrorNotFound(err) {
+                       return nil, errors.NotFound.Wrap(err, 
fmt.Sprintf("could not find api key id[%d] in DB", id))
+               }
+               return nil, errors.Default.Wrap(err, "error getting api key 
from DB")
+       }
+       return apiKey, nil
+}
+
+func (c *ApiKeyHelper) GetApiKey(tx dal.Dal, additionalClauses ...dal.Clause) 
(*models.ApiKey, errors.Error) {
+       if tx == nil {
+               tx = c.basicRes.GetDal()
+       }
+       apiKey := &models.ApiKey{}
+       err := tx.First(apiKey, additionalClauses...)
+       return apiKey, err
+}
+
+func (c *ApiKeyHelper) generateApiKey() (apiKey string, hashedApiKey string, 
err errors.Error) {
+       apiKey, randomLetterErr := utils.RandLetterBytes(apiKeyLen)
+       if randomLetterErr != nil {
+               err = errors.Default.Wrap(randomLetterErr, "random letters")
+               return
+       }
+       hashedApiKey, err = c.DigestToken(apiKey)
+       return apiKey, hashedApiKey, err
+}
+
+func (c *ApiKeyHelper) DigestToken(token string) (string, errors.Error) {
+       h := hmac.New(sha256.New, []byte(c.encryptionSecret))
+       if _, err := h.Write([]byte(token)); err != nil {
+               c.logger.Error(err, "hmac write api key")
+               return "", errors.Default.Wrap(err, "hmac write token")
+       }
+       hashedApiKey := fmt.Sprintf("%x", h.Sum(nil))
+       return hashedApiKey, nil
+}
diff --git a/backend/helpers/pluginhelper/api/connection_helper.go 
b/backend/helpers/pluginhelper/api/connection_helper.go
index 001cc25a8..6ae72755d 100644
--- a/backend/helpers/pluginhelper/api/connection_helper.go
+++ b/backend/helpers/pluginhelper/api/connection_helper.go
@@ -19,17 +19,16 @@ package api
 
 import (
        "fmt"
-       "strconv"
-
        "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "github.com/apache/incubator-devlake/server/api/shared"
+       "strconv"
 
        "github.com/apache/incubator-devlake/core/context"
        "github.com/apache/incubator-devlake/core/dal"
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/log"
        "github.com/apache/incubator-devlake/core/models"
-       plugin "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/core/plugin"
        "github.com/go-playground/validator/v10"
 )
 
@@ -64,12 +63,25 @@ func NewConnectionHelper(
 
 // Create a connection record based on request body
 func (c *ConnectionApiHelper) Create(connection interface{}, input 
*plugin.ApiResourceInput) errors.Error {
+       return c.CreateWithTx(nil, connection, input)
+}
+
+// Create a connection record based on request body
+func (c *ConnectionApiHelper) CreateWithTx(tx dal.Transaction, connection 
interface{}, input *plugin.ApiResourceInput) errors.Error {
        // update fields from request body
+       db := c.db
+       if tx != nil {
+               db = tx
+       }
        err := c.merge(connection, input.Body)
        if err != nil {
                return err
        }
-       return c.save(connection, c.db.Create)
+       if err := c.save(connection, db.Create); err != nil {
+               c.log.Error(err, "create connection")
+               return err
+       }
+       return nil
 }
 
 // Patch (Modify) a connection record based on request body
@@ -185,7 +197,7 @@ func (c *ConnectionApiHelper) getPluginSource() 
(plugin.PluginSource, errors.Err
        pluginMeta, _ := plugin.GetPlugin(c.pluginName)
        pluginSrc, ok := pluginMeta.(plugin.PluginSource)
        if !ok {
-               return nil, errors.Default.New("plugin doesn't implement 
PluginSource")
+               return nil, errors.Default.New(fmt.Sprintf("plugin %s doesn't 
implement PluginSource", c.pluginName))
        }
        return pluginSrc, nil
 }
diff --git a/backend/impls/dalgorm/dalgorm.go b/backend/impls/dalgorm/dalgorm.go
index 7d0429b33..2ef94fcc8 100644
--- a/backend/impls/dalgorm/dalgorm.go
+++ b/backend/impls/dalgorm/dalgorm.go
@@ -125,7 +125,6 @@ func buildTx(tx *gorm.DB, clauses []dal.Clause) *gorm.DB {
                        if nowait {
                                locking.Options = "NOWAIT"
                        }
-
                        tx = tx.Clauses(locking)
                }
        }
diff --git a/backend/plugins/webhook/api/connection.go 
b/backend/plugins/webhook/api/connection.go
index 36cf66fad..1a91b3b74 100644
--- a/backend/plugins/webhook/api/connection.go
+++ b/backend/plugins/webhook/api/connection.go
@@ -20,12 +20,11 @@ package api
 import (
        "fmt"
        "github.com/apache/incubator-devlake/core/dal"
-       "net/http"
-       "strconv"
-
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/plugin"
        "github.com/apache/incubator-devlake/plugins/webhook/models"
+       "net/http"
+       "strconv"
 )
 
 // PostConnections
@@ -40,11 +39,34 @@ import (
 func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
        // update from request and save to database
        connection := &models.WebhookConnection{}
-       err := connectionHelper.Create(connection, input)
+       tx := basicRes.GetDal().Begin()
+       err := connectionHelper.CreateWithTx(tx, connection, input)
        if err != nil {
                return nil, err
        }
-       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+       logger.Info("connection: %+v", connection)
+       name := fmt.Sprintf("%s-%d", pluginName, connection.ID)
+       allowedPath := fmt.Sprintf("/plugins/%s/connections/%d/.*", pluginName, 
connection.ID)
+       extra := fmt.Sprintf("connectionId:%d", connection.ID)
+       apiKeyRecord, err := apiKeyHelper.CreateForPlugin(tx, input.User, name, 
pluginName, allowedPath, extra)
+       if err != nil {
+               if err := tx.Rollback(); err != nil {
+                       logger.Error(err, "transaction Rollback")
+               }
+               logger.Error(err, "CreateForPlugin")
+               return nil, err
+       }
+       if err := tx.Commit(); err != nil {
+               logger.Info("transaction commit: %s", err)
+       }
+
+       apiOutputConnection := models.ApiOutputWebhookConnection{
+               WebhookConnection: *connection,
+               ApiKey:            apiKeyRecord,
+       }
+       logger.Info("api output connection: %+v", apiOutputConnection)
+
+       return &plugin.ApiResourceOutput{Body: apiOutputConnection, Status: 
http.StatusOK}, nil
 }
 
 // PatchConnection
@@ -79,10 +101,29 @@ func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput
        if e != nil {
                return nil, errors.BadInput.WrapRaw(e)
        }
-       err := basicRes.GetDal().Delete(&models.WebhookConnection{}, 
dal.Where("id = ?", connectionId))
+       var connection models.WebhookConnection
+       tx := basicRes.GetDal().Begin()
+       err := tx.Delete(&connection, dal.Where("id = ?", connectionId))
+       if err != nil {
+               if err := tx.Rollback(); err != nil {
+                       logger.Error(err, "transaction Rollback")
+               }
+               logger.Error(err, "delete connection: %d", connectionId)
+               return nil, err
+       }
+       extra := fmt.Sprintf("connectionId:%d", connectionId)
+       err = apiKeyHelper.DeleteForPlugin(tx, pluginName, extra)
        if err != nil {
+               if err := tx.Rollback(); err != nil {
+                       logger.Error(err, "transaction Rollback")
+               }
+               logger.Error(err, "delete connection extra: %d, name: %s", 
extra, pluginName)
                return nil, err
        }
+       if err := tx.Commit(); err != nil {
+               logger.Info("transaction commit: %s", err)
+       }
+
        return &plugin.ApiResourceOutput{Status: http.StatusOK}, nil
 }
 
diff --git a/backend/plugins/webhook/api/init.go 
b/backend/plugins/webhook/api/init.go
index 1da6ff414..c2304e402 100644
--- a/backend/plugins/webhook/api/init.go
+++ b/backend/plugins/webhook/api/init.go
@@ -19,22 +19,29 @@ package api
 
 import (
        "github.com/apache/incubator-devlake/core/context"
+       "github.com/apache/incubator-devlake/core/log"
        "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/apikeyhelper"
        "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
        "github.com/go-playground/validator/v10"
 )
 
+const pluginName = "webhook"
+
 var vld *validator.Validate
 var connectionHelper *api.ConnectionApiHelper
+var apiKeyHelper *apikeyhelper.ApiKeyHelper
 var basicRes context.BasicRes
+var logger log.Logger
 
 func Init(br context.BasicRes, p plugin.PluginMeta) {
-
        basicRes = br
+       logger = basicRes.GetLogger()
        vld = validator.New()
        connectionHelper = api.NewConnectionHelper(
                basicRes,
                vld,
                p.Name(),
        )
+       apiKeyHelper = apikeyhelper.NewApiKeyHelper(basicRes, logger)
 }
diff --git a/backend/plugins/webhook/models/connection.go 
b/backend/plugins/webhook/models/connection.go
index c9d56b9f6..53185a8cf 100644
--- a/backend/plugins/webhook/models/connection.go
+++ b/backend/plugins/webhook/models/connection.go
@@ -18,9 +18,15 @@ limitations under the License.
 package models
 
 import (
+       "github.com/apache/incubator-devlake/core/models"
        helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
 )
 
+type ApiOutputWebhookConnection struct {
+       WebhookConnection `mapstructure:",squash"`
+       ApiKey            *models.ApiKey `json:"apiKey,omitempty"`
+}
+
 type WebhookConnection struct {
        helper.BaseConnection `mapstructure:",squash"`
 }
diff --git a/backend/server/api/api.go b/backend/server/api/api.go
index b0fa46290..444e3bb71 100644
--- a/backend/server/api/api.go
+++ b/backend/server/api/api.go
@@ -94,6 +94,11 @@ func CreateApiService() {
                shared.ApiOutputSuccess(ctx, nil, http.StatusOK)
        })
 
+       // Api keys
+       basicRes := services.GetBasicRes()
+       router.Use(RestAuthentication(router, basicRes))
+       router.Use(OAuth2ProxyAuthentication(basicRes))
+
        // Restrict access if database migration is required
        router.Use(func(ctx *gin.Context) {
                if !services.MigrationRequireConfirmation() {
@@ -133,7 +138,7 @@ func CreateApiService() {
        }))
 
        // Register API endpoints
-       RegisterRouter(router)
+       RegisterRouter(router, basicRes)
        // Get port from config
        port := v.GetString("PORT")
        // Trim any : from the start
diff --git a/backend/server/api/apikeys/apikeys.go 
b/backend/server/api/apikeys/apikeys.go
new file mode 100644
index 000000000..c8d307052
--- /dev/null
+++ b/backend/server/api/apikeys/apikeys.go
@@ -0,0 +1,141 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package apikeys
+
+import (
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/models"
+       "github.com/apache/incubator-devlake/impls/logruslog"
+       "github.com/apache/incubator-devlake/server/api/shared"
+       "github.com/apache/incubator-devlake/server/services"
+       "github.com/gin-gonic/gin"
+       "net/http"
+       "strconv"
+)
+
+type PaginatedApiKeys struct {
+       ApiKeys []*models.ApiKey `json:"apikeys"`
+       Count   int64            `json:"count"`
+}
+
+// @Summary Get list of api keys
+// @Description GET /api-keys?page=1&pageSize=10
+// @Tags framework/api-keys
+// @Param page query int true "query"
+// @Param pageSize query int true "query"
+// @Success 200  {object} PaginatedApiKeys
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /api-keys [get]
+func GetApiKeys(c *gin.Context) {
+       var query services.ApiKeysQuery
+       err := c.ShouldBindQuery(&query)
+       if err != nil {
+               shared.ApiOutputError(c, errors.BadInput.Wrap(err, 
shared.BadRequestBody))
+               return
+       }
+       apiKeys, count, err := services.GetApiKeys(&query)
+       if err != nil {
+               shared.ApiOutputAbort(c, errors.Default.Wrap(err, "error 
getting api keys"))
+               return
+       }
+
+       shared.ApiOutputSuccess(c, PaginatedApiKeys{
+               ApiKeys: apiKeys,
+               Count:   count,
+       }, http.StatusOK)
+}
+
+// @Summary Delete an api key
+// @Description Delete an api key
+// @Tags framework/api-keys
+// @Accept application/json
+// @Success 200
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /api-keys/:apiKeyId [delete]
+func DeleteApiKey(c *gin.Context) {
+       apiKeyId := c.Param("apiKeyId")
+       id, err := strconv.ParseUint(apiKeyId, 10, 64)
+       if err != nil {
+               shared.ApiOutputError(c, errors.BadInput.Wrap(err, "bad 
apiKeyId format supplied"))
+               return
+       }
+       err = services.DeleteApiKey(id)
+       if err != nil {
+               shared.ApiOutputError(c, errors.Default.Wrap(err, "error 
deleting api key"))
+               return
+       }
+       shared.ApiOutputSuccess(c, nil, http.StatusOK)
+}
+
+// @Summary Refresh an api key
+// @Description Refresh an api key
+// @Tags framework/api-keys
+// @Accept application/json
+// @Success 200
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /api-keys/:apiKeyId [put]
+func PutApiKey(c *gin.Context) {
+       apiKeyId := c.Param("apiKeyId")
+       id, err := strconv.ParseUint(apiKeyId, 10, 64)
+       if err != nil {
+               shared.ApiOutputError(c, errors.BadInput.Wrap(err, "bad 
apiKeyId format supplied"))
+               return
+       }
+       user, exist := shared.GetUser(c)
+       if !exist {
+               logruslog.Global.Warn(nil, "user doesn't exist")
+       }
+       apiOutputApiKey, err := services.PutApiKey(user, id)
+       if err != nil {
+               shared.ApiOutputError(c, errors.Default.Wrap(err, "error 
regenerate api key"))
+               return
+       }
+       shared.ApiOutputSuccess(c, apiOutputApiKey, http.StatusOK)
+}
+
+// @Summary Create a new api key
+// @Description Create a new api key
+// @Tags framework/api-keys
+// @Accept application/json
+// @Param apikey body models.ApiInputApiKey true "json"
+// @Success 200  {object} models.ApiOutputApiKey
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /api-keys [post]
+func PostApiKey(c *gin.Context) {
+       apiKeyInput := &models.ApiInputApiKey{}
+       err := c.ShouldBind(apiKeyInput)
+       if err != nil {
+               shared.ApiOutputError(c, errors.BadInput.Wrap(err, 
shared.BadRequestBody))
+               return
+       }
+       user, exist := shared.GetUser(c)
+       if !exist {
+               logruslog.Global.Warn(nil, "user doesn't exist")
+       }
+       apiKeyOutput, err := services.CreateApiKey(user, apiKeyInput)
+       if err != nil {
+               shared.ApiOutputError(c, errors.Default.Wrap(err, "error 
creating api key"))
+               return
+       }
+
+       shared.ApiOutputSuccess(c, apiKeyOutput, http.StatusCreated)
+}
diff --git a/backend/server/api/middlewares.go 
b/backend/server/api/middlewares.go
new file mode 100644
index 000000000..7145691f1
--- /dev/null
+++ b/backend/server/api/middlewares.go
@@ -0,0 +1,202 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+       "encoding/base64"
+       "fmt"
+       "github.com/apache/incubator-devlake/core/context"
+       "github.com/apache/incubator-devlake/core/dal"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/models/common"
+       "github.com/apache/incubator-devlake/helpers/apikeyhelper"
+       "github.com/gin-gonic/gin"
+       "net/http"
+       "regexp"
+       "strings"
+       "time"
+)
+
+func getOAuthUserInfo(c *gin.Context) (*common.User, error) {
+       if c == nil {
+               return nil, errors.Default.New("request is nil")
+       }
+       user := c.GetHeader("X-Forwarded-User")
+       email := c.GetHeader("X-Forwarded-Email")
+       return &common.User{
+               Name:  user,
+               Email: email,
+       }, nil
+}
+
+func getBasicAuthUserInfo(c *gin.Context) (*common.User, error) {
+       if c == nil {
+               return nil, errors.Default.New("request is nil")
+       }
+       authHeader := c.GetHeader("Authorization")
+       if authHeader == "" {
+               return nil, errors.Default.New("Authorization is empty")
+       }
+       basicAuth := strings.TrimPrefix(authHeader, "Basic ")
+       if basicAuth == authHeader || basicAuth == "" {
+               return nil, errors.Default.New("invalid basic auth")
+       }
+       userInfoData, err := base64.StdEncoding.DecodeString(basicAuth)
+       if err != nil {
+               return nil, errors.Default.Wrap(err, "base64 decode")
+       }
+       userInfo := strings.Split(string(userInfoData), ":")
+       if len(userInfo) != 2 {
+               return nil, errors.Default.New("invalid user info data")
+       }
+       return &common.User{
+               Name: userInfo[0],
+       }, nil
+}
+
+func OAuth2ProxyAuthentication(basicRes context.BasicRes) gin.HandlerFunc {
+       logger := basicRes.GetLogger()
+       return func(c *gin.Context) {
+               _, exist := c.Get(common.USER)
+               if !exist {
+                       user, err := getOAuthUserInfo(c)
+                       if err != nil {
+                               logger.Error(err, "getOAuthUserInfo")
+                       }
+                       if user == nil || user.Name == "" {
+                               // fetch with basic auth header
+                               user, err = getBasicAuthUserInfo(c)
+                               if err != nil {
+                                       logger.Error(err, 
"getBasicAuthUserInfo")
+                               }
+                       }
+                       if user != nil && user.Name != "" {
+                               c.Set(common.USER, user)
+                       }
+               }
+               c.Next()
+       }
+}
+
+func RestAuthentication(router *gin.Engine, basicRes context.BasicRes) 
gin.HandlerFunc {
+       type ApiBody struct {
+               Success bool   `json:"success"`
+               Message string `json:"message"`
+       }
+       db := basicRes.GetDal()
+       logger := basicRes.GetLogger()
+       if db == nil {
+               panic(fmt.Errorf("db is not initialised"))
+       }
+       apiKeyHelper := apikeyhelper.NewApiKeyHelper(basicRes, logger)
+       return func(c *gin.Context) {
+               path := c.Request.URL.Path
+               path = strings.TrimPrefix(path, "/api")
+               // Only open api needs to check api key
+               if !strings.HasPrefix(path, "/rest") {
+                       logger.Info("path %s will continue", path)
+                       c.Next()
+                       return
+               }
+
+               authHeader := c.GetHeader("Authorization")
+               if authHeader == "" {
+                       c.Abort()
+                       c.JSON(http.StatusUnauthorized, &ApiBody{
+                               Success: false,
+                               Message: "token is missing",
+                       })
+                       return
+               }
+               apiKeyStr := strings.TrimPrefix(authHeader, "Bearer ")
+               if apiKeyStr == authHeader || apiKeyStr == "" {
+                       c.Abort()
+                       c.JSON(http.StatusUnauthorized, &ApiBody{
+                               Success: false,
+                               Message: "token is not present or malformed",
+                       })
+                       return
+               }
+
+               hashedApiKey, err := apiKeyHelper.DigestToken(apiKeyStr)
+               if err != nil {
+                       logger.Error(err, "DigestToken")
+                       c.Abort()
+                       c.JSON(http.StatusInternalServerError, &ApiBody{
+                               Success: false,
+                               Message: err.Error(),
+                       })
+                       return
+               }
+
+               apiKey, err := apiKeyHelper.GetApiKey(nil, dal.Where("api_key = 
?", hashedApiKey))
+               if err != nil {
+                       c.Abort()
+                       if db.IsErrorNotFound(err) {
+                               c.JSON(http.StatusForbidden, &ApiBody{
+                                       Success: false,
+                                       Message: "api key is invalid",
+                               })
+                       } else {
+                               logger.Error(err, "query api key from db")
+                               c.JSON(http.StatusInternalServerError, &ApiBody{
+                                       Success: false,
+                                       Message: err.Error(),
+                               })
+                       }
+                       return
+               }
+
+               if apiKey.ExpiredAt != nil && time.Until(*apiKey.ExpiredAt) < 0 
{
+                       c.Abort()
+                       c.JSON(http.StatusForbidden, &ApiBody{
+                               Success: false,
+                               Message: "api key has expired",
+                       })
+                       return
+               }
+               matched, matchErr := regexp.MatchString(apiKey.AllowedPath, 
path)
+               if matchErr != nil {
+                       logger.Error(err, "regexp match path error")
+                       c.Abort()
+                       c.JSON(http.StatusInternalServerError, &ApiBody{
+                               Success: false,
+                               Message: matchErr.Error(),
+                       })
+                       return
+               }
+               if !matched {
+                       c.JSON(http.StatusForbidden, &ApiBody{
+                               Success: false,
+                               Message: "path doesn't match api key's scope",
+                       })
+                       return
+               }
+
+               if strings.HasPrefix(path, "/rest") {
+                       logger.Info("redirect path: %s to: %s", path, 
strings.TrimPrefix(path, "/rest"))
+                       c.Request.URL.Path = strings.TrimPrefix(path, "/rest")
+               }
+               c.Set(common.USER, &common.User{
+                       Name:  apiKey.Creator.Creator,
+                       Email: apiKey.Creator.CreatorEmail,
+               })
+               router.HandleContext(c)
+               c.Abort()
+       }
+}
diff --git a/backend/server/api/router.go b/backend/server/api/router.go
index b4005b575..34adf384c 100644
--- a/backend/server/api/router.go
+++ b/backend/server/api/router.go
@@ -19,8 +19,10 @@ package api
 
 import (
        "fmt"
+       "github.com/apache/incubator-devlake/core/context"
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/impls/logruslog"
+       "github.com/apache/incubator-devlake/server/api/apikeys"
        "net/http"
        "strings"
 
@@ -38,7 +40,7 @@ import (
        "github.com/gin-gonic/gin"
 )
 
-func RegisterRouter(r *gin.Engine) {
+func RegisterRouter(r *gin.Engine, basicRes context.BasicRes) {
        r.GET("/pipelines", pipelines.Index)
        r.POST("/pipelines", pipelines.Post)
        r.GET("/pipelines/:pipelineId", pipelines.Get)
@@ -73,6 +75,12 @@ func RegisterRouter(r *gin.Engine) {
        r.POST("/projects", project.PostProject)
        r.GET("/projects", project.GetProjects)
 
+       // api keys api
+       r.GET("/api-keys", apikeys.GetApiKeys)
+       r.POST("/api-keys", apikeys.PostApiKey)
+       r.PUT("/api-keys/:apiKeyId/", apikeys.PutApiKey)
+       r.DELETE("/api-keys/:apiKeyId", apikeys.DeleteApiKey)
+
        // mount all api resources for all plugins
        resources, err := services.GetPluginsApiResources()
        if err != nil {
@@ -80,23 +88,23 @@ func RegisterRouter(r *gin.Engine) {
        }
        // mount all api resources for all plugins
        for pluginName, apiResources := range resources {
-               registerPluginEndpoints(r, pluginName, apiResources)
+               registerPluginEndpoints(r, basicRes, pluginName, apiResources)
        }
 }
 
-func registerPluginEndpoints(r *gin.Engine, pluginName string, apiResources 
map[string]map[string]plugin.ApiResourceHandler) {
+func registerPluginEndpoints(r *gin.Engine, basicRes context.BasicRes, 
pluginName string, apiResources 
map[string]map[string]plugin.ApiResourceHandler) {
        for resourcePath, resourceHandlers := range apiResources {
                for method, h := range resourceHandlers {
                        r.Handle(
                                method,
                                fmt.Sprintf("/plugins/%s/%s", pluginName, 
resourcePath),
-                               handlePluginCall(pluginName, h),
+                               handlePluginCall(basicRes, pluginName, h),
                        )
                }
        }
 }
 
-func handlePluginCall(pluginName string, handler plugin.ApiResourceHandler) 
func(c *gin.Context) {
+func handlePluginCall(basicRes context.BasicRes, pluginName string, handler 
plugin.ApiResourceHandler) func(c *gin.Context) {
        return func(c *gin.Context) {
                var err errors.Error
                input := &plugin.ApiResourceInput{}
@@ -108,13 +116,19 @@ func handlePluginCall(pluginName string, handler 
plugin.ApiResourceHandler) func
                }
                input.Params["plugin"] = pluginName
                input.Query = c.Request.URL.Query()
+               user, exist := shared.GetUser(c)
+               if !exist {
+                       basicRes.GetLogger().Warn(nil, "user doesn't exist")
+               } else {
+                       input.User = user
+               }
                if c.Request.Body != nil {
                        if 
strings.HasPrefix(c.Request.Header.Get("Content-Type"), "multipart/form-data;") 
{
                                input.Request = c.Request
                        } else {
-                               err2 := c.ShouldBindJSON(&input.Body)
-                               if err2 != nil && err2.Error() != "EOF" {
-                                       shared.ApiOutputError(c, err2)
+                               shouldBindJSONErr := 
c.ShouldBindJSON(&input.Body)
+                               if shouldBindJSONErr != nil && 
shouldBindJSONErr.Error() != "EOF" {
+                                       shared.ApiOutputError(c, 
shouldBindJSONErr)
                                        return
                                }
                        }
diff --git a/backend/plugins/webhook/models/connection.go 
b/backend/server/api/shared/gin_utils.go
similarity index 73%
copy from backend/plugins/webhook/models/connection.go
copy to backend/server/api/shared/gin_utils.go
index c9d56b9f6..892dc489a 100644
--- a/backend/plugins/webhook/models/connection.go
+++ b/backend/server/api/shared/gin_utils.go
@@ -15,16 +15,18 @@ See the License for the specific language governing 
permissions and
 limitations under the License.
 */
 
-package models
+package shared
 
 import (
-       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/core/models/common"
+       "github.com/gin-gonic/gin"
 )
 
-type WebhookConnection struct {
-       helper.BaseConnection `mapstructure:",squash"`
-}
-
-func (WebhookConnection) TableName() string {
-       return "_tool_webhook_connections"
+func GetUser(c *gin.Context) (*common.User, bool) {
+       userObj, exist := c.Get(common.USER)
+       if !exist {
+               return nil, false
+       }
+       user := userObj.(*common.User)
+       return user, true
 }
diff --git a/backend/server/services/apikeys.go 
b/backend/server/services/apikeys.go
new file mode 100644
index 000000000..e45003f32
--- /dev/null
+++ b/backend/server/services/apikeys.go
@@ -0,0 +1,115 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package services
+
+import (
+       "github.com/apache/incubator-devlake/core/dal"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/models"
+       "github.com/apache/incubator-devlake/core/models/common"
+       "github.com/apache/incubator-devlake/helpers/apikeyhelper"
+)
+
+// ApiKeysQuery used to query api keys as the api key input
+type ApiKeysQuery struct {
+       Pagination
+}
+
+// GetApiKeys returns a paginated list of api keys based on `query`
+func GetApiKeys(query *ApiKeysQuery) ([]*models.ApiKey, int64, errors.Error) {
+       // verify input
+       if err := VerifyStruct(query); err != nil {
+               return nil, 0, err
+       }
+       clauses := []dal.Clause{
+               dal.From(&models.ApiKey{}),
+               dal.Where("type = ?", "devlake"),
+       }
+
+       logger.Info("query: %+v", query)
+       count, err := db.Count(clauses...)
+       if err != nil {
+               return nil, 0, errors.Default.Wrap(err, "error getting DB count 
of api key")
+       }
+
+       clauses = append(clauses,
+               dal.Orderby("created_at DESC"),
+               dal.Offset(query.GetSkip()),
+               dal.Limit(query.GetPageSize()),
+       )
+       apiKeys := make([]*models.ApiKey, 0)
+       err = db.All(&apiKeys, clauses...)
+       if err != nil {
+               return nil, 0, errors.Default.Wrap(err, "error finding DB api 
key")
+       }
+
+       return apiKeys, count, nil
+}
+
+func DeleteApiKey(id uint64) errors.Error {
+       // verify input
+       if id == 0 {
+               return errors.BadInput.New("api key's id is missing")
+       }
+
+       apiKeyHelper := apikeyhelper.NewApiKeyHelper(basicRes, logger)
+       err := apiKeyHelper.Delete(id)
+       if err != nil {
+               logger.Error(err, "api key helper delete: %d", id)
+               return err
+       }
+       return nil
+}
+
+func PutApiKey(user *common.User, id uint64) (*models.ApiOutputApiKey, 
errors.Error) {
+       // verify input
+       if id == 0 {
+               return nil, errors.BadInput.New("api key's id is missing")
+       }
+       apiKeyHelper := apikeyhelper.NewApiKeyHelper(basicRes, logger)
+       apiKey, err := apiKeyHelper.Put(user, id)
+       if err != nil {
+               logger.Error(err, "api key helper put: %d", id)
+               return nil, err
+       }
+       return apiKey, nil
+}
+
+// CreateApiKey accepts an api key instance and insert it to database
+func CreateApiKey(user *common.User, apiKeyInput *models.ApiInputApiKey) 
(*models.ApiOutputApiKey, errors.Error) {
+       // verify input
+       if err := VerifyStruct(apiKeyInput); err != nil {
+               logger.Error(err, "verify: %+v", apiKeyInput)
+               return nil, err
+       }
+
+       apiKeyHelper := apikeyhelper.NewApiKeyHelper(basicRes, logger)
+       tx := basicRes.GetDal().Begin()
+       apiKey, err := apiKeyHelper.Create(tx, user, apiKeyInput.Name, 
apiKeyInput.ExpiredAt, apiKeyInput.AllowedPath, apiKeyInput.Type, "")
+       if err != nil {
+               if err := tx.Rollback(); err != nil {
+                       logger.Error(err, "transaction Rollback")
+               }
+               logger.Error(err, "api key helper create")
+               return nil, errors.Default.Wrap(err, "random letters")
+       }
+       if err := tx.Commit(); err != nil {
+               logger.Info("transaction commit: %s", err)
+       }
+       return apiKey, nil
+}
diff --git a/backend/server/services/base.go b/backend/server/services/base.go
index c84639015..98727748b 100644
--- a/backend/server/services/base.go
+++ b/backend/server/services/base.go
@@ -17,7 +17,9 @@ limitations under the License.
 
 package services
 
-import "github.com/apache/incubator-devlake/core/errors"
+import (
+       "github.com/apache/incubator-devlake/core/errors"
+)
 
 // Pagination holds the paginate information
 type Pagination struct {


Reply via email to