From b7231818e9526ad4a8807f817c17830ecadf7ed2 Mon Sep 17 00:00:00 2001 From: Gaius Date: Tue, 1 Aug 2023 15:18:06 +0800 Subject: [PATCH] feat: add personal access token middleware to open api (#2590) Signed-off-by: Gaius --- manager/manager.go | 2 +- manager/middlewares/personal_access_token.go | 55 ++++++++++++++++++++ manager/models/personal_access_token.go | 2 +- manager/router/router.go | 40 ++++++++++---- 4 files changed, 88 insertions(+), 11 deletions(-) create mode 100644 manager/middlewares/personal_access_token.go diff --git a/manager/manager.go b/manager/manager.go index f56121e49b6..0f211284296 100644 --- a/manager/manager.go +++ b/manager/manager.go @@ -148,7 +148,7 @@ func New(cfg *config.Config, d dfpath.Dfpath) (*Server, error) { // Initialize REST server restService := service.New(cfg, db, cache, job, enforcer, objectStorage) - router, err := router.Init(cfg, d.LogDir(), restService, enforcer, EmbedFolder(assets, assetsTargetPath)) + router, err := router.Init(cfg, d.LogDir(), restService, db, enforcer, EmbedFolder(assets, assetsTargetPath)) if err != nil { return nil, err } diff --git a/manager/middlewares/personal_access_token.go b/manager/middlewares/personal_access_token.go new file mode 100644 index 00000000000..71bdcee6ed4 --- /dev/null +++ b/manager/middlewares/personal_access_token.go @@ -0,0 +1,55 @@ +/* + * Copyright 2023 The Dragonfly Authors + * + * Licensed 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 middlewares + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/go-http-utils/headers" + "gorm.io/gorm" + + "d7y.io/dragonfly/v2/manager/models" +) + +func PersonalAccessToken(gdb *gorm.DB) gin.HandlerFunc { + return func(c *gin.Context) { + // Get bearer token from Authorization header. + authorization := c.GetHeader(headers.Authorization) + tokenFields := strings.Fields(authorization) + if len(tokenFields) != 2 || tokenFields[0] != "Bearer" { + c.JSON(http.StatusUnauthorized, ErrorResponse{ + Message: http.StatusText(http.StatusUnauthorized), + }) + c.Abort() + return + } + + // Check if the personal access token is valid. + personalAccessToken := tokenFields[1] + if err := gdb.WithContext(c).Where("token = ?", personalAccessToken).First(&models.PersonalAccessToken{}).Error; err != nil { + c.JSON(http.StatusUnauthorized, ErrorResponse{ + Message: http.StatusText(http.StatusUnauthorized), + }) + c.Abort() + return + } + + c.Next() + } +} diff --git a/manager/models/personal_access_token.go b/manager/models/personal_access_token.go index f1140492d10..0ee5ec35ffc 100644 --- a/manager/models/personal_access_token.go +++ b/manager/models/personal_access_token.go @@ -30,7 +30,7 @@ type PersonalAccessToken struct { BaseModel Name string `gorm:"column:name;type:varchar(256);index:uk_personal_access_token_name,unique;not null;comment:name" json:"name"` BIO string `gorm:"column:bio;type:varchar(1024);comment:biography" json:"bio"` - Token string `gorm:"column:token;type:varchar(256);not null;comment:access token" json:"token"` + Token string `gorm:"column:token;type:varchar(256);index:uk_personal_access_token,unique;not null;comment:access token" json:"token"` Scopes Array `gorm:"column:scopes;not null;comment:scopes flags" json:"scopes"` State string `gorm:"column:state;type:varchar(256);default:'inactive';comment:service state" json:"state"` ExpiredAt time.Time `gorm:"column:expired_at;type:timestamp;default:current_timestamp;not null;comment:expired at" json:"expired_at"` diff --git a/manager/router/router.go b/manager/router/router.go index bfd9d2db8c6..f77f5038bd7 100644 --- a/manager/router/router.go +++ b/manager/router/router.go @@ -31,6 +31,7 @@ import ( logger "d7y.io/dragonfly/v2/internal/dflog" "d7y.io/dragonfly/v2/manager/config" + "d7y.io/dragonfly/v2/manager/database" "d7y.io/dragonfly/v2/manager/handlers" "d7y.io/dragonfly/v2/manager/middlewares" "d7y.io/dragonfly/v2/manager/service" @@ -41,7 +42,7 @@ const ( OtelServiceName = "dragonfly-manager" ) -func Init(cfg *config.Config, logDir string, service service.Service, enforcer *casbin.Enforcer, assets static.ServeFileSystem) (*gin.Engine, error) { +func Init(cfg *config.Config, logDir string, service service.Service, database *database.Database, enforcer *casbin.Enforcer, assets static.ServeFileSystem) (*gin.Engine, error) { // Set mode. if !cfg.Verbose { gin.SetMode(gin.ReleaseMode) @@ -65,23 +66,31 @@ func Init(cfg *config.Config, logDir string, service service.Service, enforcer * r.Use(otelgin.Middleware(OtelServiceName)) } - // Middleware. + // Gin middleware. r.Use(gin.Recovery()) r.Use(ginzap.Ginzap(logger.GinLogger.Desugar(), time.RFC3339, true)) r.Use(ginzap.RecoveryWithZap(logger.GinLogger.Desugar(), true)) + + // Error middleware. r.Use(middlewares.Error()) + + // CORS middleware. r.Use(middlewares.CORS()) + // RBAC middleware. rbac := middlewares.RBAC(enforcer) jwt, err := middlewares.Jwt(cfg.Auth.JWT, service) if err != nil { return nil, err } + // Personal access token middleware. + personalAccessToken := middlewares.PersonalAccessToken(database.DB) + // Manager view. r.Use(static.Serve("/", assets)) - // Router. + // API router. apiv1 := r.Group("/api/v1") // User. @@ -179,6 +188,7 @@ func Init(cfg *config.Config, logDir string, service service.Service, enforcer * config.GET(":id", jwt.MiddlewareFunc(), rbac, h.GetConfig) config.GET("", h.GetConfigs) + // TODO Add auth to the following routes and fix the tests. // Job. job := apiv1.Group("/jobs") job.POST("", h.CreateJob) @@ -187,12 +197,6 @@ func Init(cfg *config.Config, logDir string, service service.Service, enforcer * job.GET(":id", h.GetJob) job.GET("", h.GetJobs) - // Compatible with the V1 preheat. - pv1 := r.Group("/preheats") - r.GET("_ping", h.GetHealth) - pv1.POST("", h.CreateV1Preheat) - pv1.GET(":id", h.GetV1Preheat) - // Application. cs := apiv1.Group("/applications", jwt.MiddlewareFunc(), rbac) cs.POST("", h.CreateApplication) @@ -216,6 +220,24 @@ func Init(cfg *config.Config, logDir string, service service.Service, enforcer * pat.GET(":id", h.GetPersonalAccessToken) pat.GET("", h.GetPersonalAccessTokens) + // Open API router. + oapiv1 := r.Group("/oapi/v1") + + // Job. + ojob := oapiv1.Group("/jobs", personalAccessToken) + ojob.POST("", h.CreateJob) + ojob.DELETE(":id", h.DestroyJob) + ojob.PATCH(":id", h.UpdateJob) + ojob.GET(":id", h.GetJob) + ojob.GET("", h.GetJobs) + + // TODO Remove this api. + // Compatible with the V1 preheat. + pv1 := r.Group("/preheats") + r.GET("_ping", h.GetHealth) + pv1.POST("", h.CreateV1Preheat) + pv1.GET(":id", h.GetV1Preheat) + // Health Check. r.GET("/healthy", h.GetHealth)