From f18ebc6c4b803849897796a16d5096aed4577479 Mon Sep 17 00:00:00 2001 From: Wenxuan Date: Thu, 5 Mar 2020 13:48:45 +0800 Subject: [PATCH] logs: add authentication support for log searching API (#165) --- go.mod | 1 + pkg/apiserver/logsearch/service.go | 106 ++++++++++++------ pkg/apiserver/utils/jwt.go | 66 +++++++++++ .../components/SearchProgress.tsx | 10 +- 4 files changed, 148 insertions(+), 35 deletions(-) create mode 100644 pkg/apiserver/utils/jwt.go diff --git a/go.mod b/go.mod index ec2f1d56c4..875e133a77 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.13 require ( github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 github.com/appleboy/gin-jwt/v2 v2.6.3 + github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/elazarl/go-bindata-assetfs v1.0.0 github.com/gin-contrib/gzip v0.0.1 github.com/gin-gonic/gin v1.5.0 diff --git a/pkg/apiserver/logsearch/service.go b/pkg/apiserver/logsearch/service.go index 0fad59a431..0ea53fb29b 100644 --- a/pkg/apiserver/logsearch/service.go +++ b/pkg/apiserver/logsearch/service.go @@ -18,6 +18,7 @@ import ( "io/ioutil" "net/http" "strconv" + "strings" "github.com/gin-gonic/gin" "github.com/pingcap/log" @@ -61,15 +62,15 @@ func NewService(config *config.Config, db *dbstore.DB) *Service { func (s *Service) Register(r *gin.RouterGroup, auth *user.AuthService) { endpoint := r.Group("/logs") - //endpoint.Use(auth.MWAuthRequired()) endpoint.GET("/download", s.DownloadLogs) - endpoint.PUT("/taskgroup", s.CreateTaskGroup) - endpoint.GET("/taskgroups/:id", s.GetTaskGroup) - endpoint.GET("/taskgroups/:id/preview", s.GetTaskGroupPreview) - endpoint.POST("/taskgroups/:id/retry", s.RetryTask) - endpoint.POST("/taskgroups/:id/cancel", s.CancelTask) - endpoint.DELETE("/taskgroups/:id", s.DeleteTaskGroup) + endpoint.GET("/download/acquire_token", auth.MWAuthRequired(), s.GetDownloadToken) + endpoint.PUT("/taskgroup", auth.MWAuthRequired(), s.CreateTaskGroup) + endpoint.GET("/taskgroups/:id", auth.MWAuthRequired(), s.GetTaskGroup) + endpoint.GET("/taskgroups/:id/preview", auth.MWAuthRequired(), s.GetTaskGroupPreview) + endpoint.POST("/taskgroups/:id/retry", auth.MWAuthRequired(), s.RetryTask) + endpoint.POST("/taskgroups/:id/cancel", auth.MWAuthRequired(), s.CancelTask) + endpoint.DELETE("/taskgroups/:id", auth.MWAuthRequired(), s.DeleteTaskGroup) } type CreateTaskGroupRequest struct { @@ -86,8 +87,10 @@ type TaskGroupResponse struct { // @Description Create and run task group // @Produce json // @Param request body CreateTaskGroupRequest true "Request body" +// @Security JwtAuth // @Success 200 {object} TaskGroupResponse // @Failure 400 {object} utils.APIError +// @Failure 401 {object} utils.APIError "Unauthorized failure" // @Failure 500 {object} utils.APIError // @Router /logs/taskgroup [put] func (s *Service) CreateTaskGroup(c *gin.Context) { @@ -133,43 +136,34 @@ func (s *Service) CreateTaskGroup(c *gin.Context) { c.JSON(http.StatusOK, resp) } -// @Summary Download logs -// @Description download logs by multiple task IDs -// @Produce application/x-tar,application/zip +// @Summary Get Download token +// @Description get download token with multiple task IDs +// @Produce plain // @Param id query []string false "task id" +// @Security JwtAuth +// @Success 200 {string} string "xxx" // @Failure 400 {object} utils.APIError -// @Failure 500 {object} utils.APIError -// @Router /logs/download [get] -func (s *Service) DownloadLogs(c *gin.Context) { +// @Failure 401 {object} utils.APIError "Unauthorized failure" +// @Router /logs/download/acquire_token [get] +func (s *Service) GetDownloadToken(c *gin.Context) { ids := c.QueryArray("id") - tasks := make([]*TaskModel, 0, len(ids)) - for _, id := range ids { - var task TaskModel - if s.db. - Where("id = ? AND state = ?", id, TaskStateFinished). - First(&task). - Error == nil { - tasks = append(tasks, &task) - // Ignore errors silently - } - } - - switch len(tasks) { - case 0: + str := strings.Join(ids, ",") + token, err := utils.NewJWTString(str) + if err != nil { c.Status(http.StatusBadRequest) - _ = c.Error(utils.ErrInvalidRequest.New("At least one target should be provided")) - case 1: - serveTaskForDownload(tasks[0], c) - default: - serveMultipleTaskForDownload(tasks, c) + _ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err)) + return } + c.String(http.StatusOK, token) } // @Summary List tasks in a task group // @Description list all log search tasks in a task group by providing task group ID // @Produce json // @Param id path string true "Task Group ID" +// @Security JwtAuth // @Success 200 {object} TaskGroupResponse +// @Failure 401 {object} utils.APIError "Unauthorized failure" // @Failure 500 {object} utils.APIError // @Router /logs/taskgroups/{id} [get] func (s *Service) GetTaskGroup(c *gin.Context) { @@ -197,7 +191,9 @@ func (s *Service) GetTaskGroup(c *gin.Context) { // @Description preview fetched logs in a task group by providing task group ID // @Produce json // @Param id path string true "task group id" +// @Security JwtAuth // @Success 200 {array} PreviewModel +// @Failure 401 {object} utils.APIError "Unauthorized failure" // @Failure 500 {object} utils.APIError // @Router /logs/taskgroups/{id}/preview [get] func (s *Service) GetTaskGroupPreview(c *gin.Context) { @@ -219,8 +215,10 @@ func (s *Service) GetTaskGroupPreview(c *gin.Context) { // @Description retry tasks that has been failed in a task group // @Produce json // @Param id path string true "task group id" +// @Security JwtAuth // @Success 200 {object} utils.APIEmptyResponse // @Failure 400 {object} utils.APIError +// @Failure 401 {object} utils.APIError "Unauthorized failure" // @Failure 500 {object} utils.APIError // @Router /logs/taskgroups/{id}/retry [post] func (s *Service) RetryTask(c *gin.Context) { @@ -269,7 +267,9 @@ func (s *Service) RetryTask(c *gin.Context) { // @Description cancel all running tasks in a task group // @Produce json // @Param id path string true "task group id" +// @Security JwtAuth // @Success 200 {object} utils.APIEmptyResponse +// @Failure 401 {object} utils.APIError "Unauthorized failure" // @Failure 400 {object} utils.APIError // @Router /logs/taskgroups/{id}/cancel [post] func (s *Service) CancelTask(c *gin.Context) { @@ -298,7 +298,9 @@ func (s *Service) CancelTask(c *gin.Context) { // @Description delete a task group by providing task group ID // @Produce json // @Param id path string true "task group id" +// @Security JwtAuth // @Success 200 {object} utils.APIEmptyResponse +// @Failure 401 {object} utils.APIError "Unauthorized failure" // @Failure 500 {object} utils.APIError // @Router /logs/taskgroups/{id} [delete] func (s *Service) DeleteTaskGroup(c *gin.Context) { @@ -312,3 +314,43 @@ func (s *Service) DeleteTaskGroup(c *gin.Context) { taskGroup.Delete(s.db) c.JSON(http.StatusOK, utils.APIEmptyResponse{}) } + +// @Summary Download +// @Description download logs by multiple task IDs +// @Produce application/x-tar,application/zip +// @Param token query string true "download token" +// @Failure 400 {object} utils.APIError +// @Failure 401 {object} utils.APIError "Unauthorized failure" +// @Failure 500 {object} utils.APIError +// @Router /logs/download [get] +func (s *Service) DownloadLogs(c *gin.Context) { + token := c.Query("token") + str, err := utils.ParseJWTString(token) + if err != nil { + c.Status(http.StatusUnauthorized) + _ = c.Error(utils.ErrInvalidRequest.New(err.Error())) + return + } + ids := strings.Split(str, ",") + tasks := make([]*TaskModel, 0, len(ids)) + for _, id := range ids { + var task TaskModel + if s.db. + Where("id = ? AND state = ?", id, TaskStateFinished). + First(&task). + Error == nil { + tasks = append(tasks, &task) + // Ignore errors silently + } + } + + switch len(tasks) { + case 0: + c.Status(http.StatusBadRequest) + _ = c.Error(utils.ErrInvalidRequest.New("At least one target should be provided")) + case 1: + serveTaskForDownload(tasks[0], c) + default: + serveMultipleTaskForDownload(tasks, c) + } +} diff --git a/pkg/apiserver/utils/jwt.go b/pkg/apiserver/utils/jwt.go new file mode 100644 index 0000000000..38ce0db13a --- /dev/null +++ b/pkg/apiserver/utils/jwt.go @@ -0,0 +1,66 @@ +// Copyright 2020 PingCAP, Inc. +// +// 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package utils + +import ( + "errors" + "time" + + "github.com/dgrijalva/jwt-go" + "github.com/gtank/cryptopasta" +) + +var hmacSampleSecret = cryptopasta.NewEncryptionKey() + +// Claims is a struct that will be encoded to a JWT. +type Claims struct { + Data string `json:"data"` + jwt.StandardClaims +} + +func newClaims(str string) *Claims { + expirationTime := time.Now().Add(24 * time.Hour) + return &Claims{ + Data: str, + StandardClaims: jwt.StandardClaims{ + ExpiresAt: expirationTime.Unix(), + }, + } +} + +// NewJWTString create a JWT string by given data +func NewJWTString(str string) (string, error) { + claims := newClaims(str) + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString(hmacSampleSecret[:]) + if err != nil { + return "", err + } + return tokenString, nil +} + +// ParseJWTString parse the JWT string and return the raw data +func ParseJWTString(str string) (string, error) { + claims := &Claims{} + token, err := jwt.ParseWithClaims(str, claims, func(token *jwt.Token) (interface{}, error) { + return hmacSampleSecret[:], nil + }) + if err != nil { + return "", err + } + if !token.Valid { + return "", errors.New("token is invalid") + } + return claims.Data, nil +} diff --git a/ui/src/apps/logSearching/components/SearchProgress.tsx b/ui/src/apps/logSearching/components/SearchProgress.tsx index 2d7b42b60f..e53a788657 100644 --- a/ui/src/apps/logSearching/components/SearchProgress.tsx +++ b/ui/src/apps/logSearching/components/SearchProgress.tsx @@ -207,9 +207,13 @@ export default function SearchProgress({ name === key ) ) - - const params = keys.map(id => `id=${id}`).join('&') - const url = `${DASHBOARD_API_URL}/logs/download?${params}` + + const res = await client.dashboard.logsDownloadAcquireTokenGet(keys) + const token = res.data + if (!token) { + return + } + const url = `${DASHBOARD_API_URL}/logs/download?token=${token}` downloadFile(url) }