Skip to content

Commit

Permalink
logs: add authentication support for log searching API (#165)
Browse files Browse the repository at this point in the history
  • Loading branch information
breezewish authored Mar 5, 2020
1 parent ef914dc commit f18ebc6
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 35 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
106 changes: 74 additions & 32 deletions pkg/apiserver/logsearch/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"io/ioutil"
"net/http"
"strconv"
"strings"

"github.com/gin-gonic/gin"
"github.com/pingcap/log"
Expand Down Expand Up @@ -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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
}
}
66 changes: 66 additions & 0 deletions pkg/apiserver/utils/jwt.go
Original file line number Diff line number Diff line change
@@ -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
}
10 changes: 7 additions & 3 deletions ui/src/apps/logSearching/components/SearchProgress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down

0 comments on commit f18ebc6

Please sign in to comment.