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 {