mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
PublicDashboards: Validate access token (#57298)
Adding validation for access token
This commit is contained in:
parent
5ee4744d62
commit
552d9d70eb
@ -17,6 +17,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens"
|
||||
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
@ -52,7 +53,7 @@ func ProvideApi(
|
||||
return api
|
||||
}
|
||||
|
||||
// Registers Endpoints on Grafana Router
|
||||
// RegisterAPIEndpoints Registers Endpoints on Grafana Router
|
||||
func (api *Api) RegisterAPIEndpoints() {
|
||||
auth := accesscontrol.Middleware(api.AccessControl)
|
||||
|
||||
@ -79,11 +80,15 @@ func (api *Api) RegisterAPIEndpoints() {
|
||||
routing.Wrap(api.SavePublicDashboardConfig))
|
||||
}
|
||||
|
||||
// Gets public dashboard
|
||||
// GetPublicDashboard Gets public dashboard
|
||||
// GET /api/public/dashboards/:accessToken
|
||||
func (api *Api) GetPublicDashboard(c *models.ReqContext) response.Response {
|
||||
accessToken := web.Params(c.Req)[":accessToken"]
|
||||
|
||||
if !tokens.IsValidAccessToken(accessToken) {
|
||||
return response.Error(http.StatusBadRequest, "Invalid Access Token", nil)
|
||||
}
|
||||
|
||||
pubdash, dash, err := api.PublicDashboardService.GetPublicDashboard(
|
||||
c.Req.Context(),
|
||||
accessToken,
|
||||
@ -115,7 +120,7 @@ func (api *Api) GetPublicDashboard(c *models.ReqContext) response.Response {
|
||||
return response.JSON(http.StatusOK, dto)
|
||||
}
|
||||
|
||||
// Gets list of public dashboards for an org
|
||||
// ListPublicDashboards Gets list of public dashboards for an org
|
||||
// GET /api/dashboards/public
|
||||
func (api *Api) ListPublicDashboards(c *models.ReqContext) response.Response {
|
||||
resp, err := api.PublicDashboardService.ListPublicDashboards(c.Req.Context(), c.SignedInUser, c.OrgID)
|
||||
@ -125,7 +130,7 @@ func (api *Api) ListPublicDashboards(c *models.ReqContext) response.Response {
|
||||
return response.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// Gets public dashboard configuration for dashboard
|
||||
// GetPublicDashboardConfig Gets public dashboard configuration for dashboard
|
||||
// GET /api/dashboards/uid/:uid/public-config
|
||||
func (api *Api) GetPublicDashboardConfig(c *models.ReqContext) response.Response {
|
||||
pdc, err := api.PublicDashboardService.GetPublicDashboardConfig(c.Req.Context(), c.OrgID, web.Params(c.Req)[":uid"])
|
||||
@ -135,7 +140,7 @@ func (api *Api) GetPublicDashboardConfig(c *models.ReqContext) response.Response
|
||||
return response.JSON(http.StatusOK, pdc)
|
||||
}
|
||||
|
||||
// Sets public dashboard configuration for dashboard
|
||||
// SavePublicDashboardConfig Sets public dashboard configuration for dashboard
|
||||
// POST /api/dashboards/uid/:uid/public-config
|
||||
func (api *Api) SavePublicDashboardConfig(c *models.ReqContext) response.Response {
|
||||
// exit if we don't have a valid dashboardUid
|
||||
@ -170,6 +175,11 @@ func (api *Api) SavePublicDashboardConfig(c *models.ReqContext) response.Respons
|
||||
// QueryPublicDashboard returns all results for a given panel on a public dashboard
|
||||
// POST /api/public/dashboard/:accessToken/panels/:panelId/query
|
||||
func (api *Api) QueryPublicDashboard(c *models.ReqContext) response.Response {
|
||||
accessToken := web.Params(c.Req)[":accessToken"]
|
||||
if !tokens.IsValidAccessToken(accessToken) {
|
||||
return response.Error(http.StatusBadRequest, "Invalid Access Token", nil)
|
||||
}
|
||||
|
||||
panelId, err := strconv.ParseInt(web.Params(c.Req)[":panelId"], 10, 64)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusBadRequest, "QueryPublicDashboard: invalid panel ID", err)
|
||||
@ -180,7 +190,7 @@ func (api *Api) QueryPublicDashboard(c *models.ReqContext) response.Response {
|
||||
return response.Error(http.StatusBadRequest, "QueryPublicDashboard: bad request data", err)
|
||||
}
|
||||
|
||||
resp, err := api.PublicDashboardService.GetQueryDataResponse(c.Req.Context(), c.SkipCache, reqDTO, panelId, web.Params(c.Req)[":accessToken"])
|
||||
resp, err := api.PublicDashboardService.GetQueryDataResponse(c.Req.Context(), c.SkipCache, reqDTO, panelId, accessToken)
|
||||
if err != nil {
|
||||
return api.handleError(c.Req.Context(), http.StatusInternalServerError, "QueryPublicDashboard: error running public dashboard panel queries", err)
|
||||
}
|
||||
@ -188,13 +198,20 @@ func (api *Api) QueryPublicDashboard(c *models.ReqContext) response.Response {
|
||||
return toJsonStreamingResponse(api.Features, resp)
|
||||
}
|
||||
|
||||
// GetAnnotations returns annotations for a public dashboard
|
||||
// GET /api/public/dashboards/:accessToken/annotations
|
||||
func (api *Api) GetAnnotations(c *models.ReqContext) response.Response {
|
||||
accessToken := web.Params(c.Req)[":accessToken"]
|
||||
if !tokens.IsValidAccessToken(accessToken) {
|
||||
return response.Error(http.StatusBadRequest, "Invalid Access Token", nil)
|
||||
}
|
||||
|
||||
reqDTO := AnnotationsQueryDTO{
|
||||
From: c.QueryInt64("from"),
|
||||
To: c.QueryInt64("to"),
|
||||
}
|
||||
|
||||
annotations, err := api.PublicDashboardService.GetAnnotations(c.Req.Context(), reqDTO, web.Params(c.Req)[":accessToken"])
|
||||
annotations, err := api.PublicDashboardService.GetAnnotations(c.Req.Context(), reqDTO, accessToken)
|
||||
|
||||
if err != nil {
|
||||
return api.handleError(c.Req.Context(), http.StatusInternalServerError, "error getting public dashboard annotations", err)
|
||||
|
@ -10,7 +10,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/google/uuid"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -57,24 +56,40 @@ func TestAPIGetAnnotations(t *testing.T) {
|
||||
ExpectedHttpResponse int
|
||||
Annotations []AnnotationEvent
|
||||
ServiceError error
|
||||
AccessToken string
|
||||
From string
|
||||
To string
|
||||
ExpectedServiceCalled bool
|
||||
}{
|
||||
{
|
||||
Name: "will return success when there is no error and to and from are provided",
|
||||
ExpectedHttpResponse: http.StatusOK,
|
||||
Annotations: []AnnotationEvent{{Id: 1}},
|
||||
ServiceError: nil,
|
||||
AccessToken: validAccessToken,
|
||||
From: "123",
|
||||
To: "123",
|
||||
ExpectedServiceCalled: true,
|
||||
},
|
||||
{
|
||||
Name: "will return 500 when service returns an error",
|
||||
ExpectedHttpResponse: http.StatusInternalServerError,
|
||||
Annotations: nil,
|
||||
ServiceError: errors.New("an error happened"),
|
||||
AccessToken: validAccessToken,
|
||||
From: "123",
|
||||
To: "123",
|
||||
ExpectedServiceCalled: true,
|
||||
},
|
||||
{
|
||||
Name: "will return 400 when has an incorrect Access Token",
|
||||
ExpectedHttpResponse: http.StatusBadRequest,
|
||||
Annotations: nil,
|
||||
ServiceError: errors.New("an error happened"),
|
||||
AccessToken: "TooShortAccessToken",
|
||||
From: "123",
|
||||
To: "123",
|
||||
ExpectedServiceCalled: false,
|
||||
},
|
||||
}
|
||||
for _, test := range testCases {
|
||||
@ -82,11 +97,15 @@ func TestAPIGetAnnotations(t *testing.T) {
|
||||
cfg := setting.NewCfg()
|
||||
cfg.RBACEnabled = false
|
||||
service := publicdashboards.NewFakePublicDashboardService(t)
|
||||
|
||||
if test.ExpectedServiceCalled {
|
||||
service.On("GetAnnotations", mock.Anything, mock.Anything, mock.AnythingOfType("string")).
|
||||
Return(test.Annotations, test.ServiceError).Once()
|
||||
}
|
||||
|
||||
testServer := setupTestServer(t, cfg, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), service, nil, anonymousUser)
|
||||
|
||||
path := fmt.Sprintf("/api/public/dashboards/abc123/annotations?from=%s&to=%s", test.From, test.To)
|
||||
path := fmt.Sprintf("/api/public/dashboards/%s/annotations?from=%s&to=%s", test.AccessToken, test.From, test.To)
|
||||
response := callAPI(testServer, http.MethodGet, path, nil, t)
|
||||
|
||||
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
|
||||
@ -221,9 +240,6 @@ func TestAPIListPublicDashboard(t *testing.T) {
|
||||
|
||||
func TestAPIGetPublicDashboard(t *testing.T) {
|
||||
DashboardUid := "dashboard-abcd1234"
|
||||
token, err := uuid.NewRandom()
|
||||
require.NoError(t, err)
|
||||
accessToken := fmt.Sprintf("%x", token)
|
||||
|
||||
testCases := []struct {
|
||||
Name string
|
||||
@ -231,10 +247,11 @@ func TestAPIGetPublicDashboard(t *testing.T) {
|
||||
ExpectedHttpResponse int
|
||||
DashboardResult *models.Dashboard
|
||||
Err error
|
||||
FixedErrorResponse string
|
||||
}{
|
||||
{
|
||||
Name: "It gets a public dashboard",
|
||||
AccessToken: accessToken,
|
||||
AccessToken: validAccessToken,
|
||||
ExpectedHttpResponse: http.StatusOK,
|
||||
DashboardResult: &models.Dashboard{
|
||||
Data: simplejson.NewFromAny(map[string]interface{}{
|
||||
@ -242,13 +259,23 @@ func TestAPIGetPublicDashboard(t *testing.T) {
|
||||
}),
|
||||
},
|
||||
Err: nil,
|
||||
FixedErrorResponse: "",
|
||||
},
|
||||
{
|
||||
Name: "It should return 404 if no public dashboard",
|
||||
AccessToken: accessToken,
|
||||
AccessToken: validAccessToken,
|
||||
ExpectedHttpResponse: http.StatusNotFound,
|
||||
DashboardResult: nil,
|
||||
Err: ErrPublicDashboardNotFound,
|
||||
FixedErrorResponse: "",
|
||||
},
|
||||
{
|
||||
Name: "It should return 400 if it is an invalid access token",
|
||||
AccessToken: "SomeInvalidAccessToken",
|
||||
ExpectedHttpResponse: http.StatusBadRequest,
|
||||
DashboardResult: nil,
|
||||
Err: nil,
|
||||
FixedErrorResponse: "{\"message\":\"Invalid Access Token\"}",
|
||||
},
|
||||
}
|
||||
|
||||
@ -278,7 +305,7 @@ func TestAPIGetPublicDashboard(t *testing.T) {
|
||||
|
||||
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
|
||||
|
||||
if test.Err == nil {
|
||||
if test.Err == nil && test.FixedErrorResponse == "" {
|
||||
var dashResp dtos.DashboardFullWithMeta
|
||||
err := json.Unmarshal(response.Body.Bytes(), &dashResp)
|
||||
require.NoError(t, err)
|
||||
@ -287,6 +314,9 @@ func TestAPIGetPublicDashboard(t *testing.T) {
|
||||
assert.Equal(t, false, dashResp.Meta.CanEdit)
|
||||
assert.Equal(t, false, dashResp.Meta.CanDelete)
|
||||
assert.Equal(t, false, dashResp.Meta.CanSave)
|
||||
} else if test.FixedErrorResponse != "" {
|
||||
require.Equal(t, test.ExpectedHttpResponse, response.Code)
|
||||
require.JSONEq(t, "{\"message\":\"Invalid Access Token\"}", response.Body.String())
|
||||
} else {
|
||||
var errResp JsonErrResponse
|
||||
err := json.Unmarshal(response.Body.Bytes(), &errResp)
|
||||
@ -585,29 +615,37 @@ func TestAPIQueryPublicDashboard(t *testing.T) {
|
||||
|
||||
t.Run("Status code is 400 when the panel ID is invalid", func(t *testing.T) {
|
||||
server, _ := setup(true)
|
||||
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/notanumber/query", strings.NewReader("{}"), t)
|
||||
path := fmt.Sprintf("/api/public/dashboards/%s/panels/notanumber/query", validAccessToken)
|
||||
resp := callAPI(server, http.MethodPost, path, strings.NewReader("{}"), t)
|
||||
require.Equal(t, http.StatusBadRequest, resp.Code)
|
||||
})
|
||||
|
||||
t.Run("Status code is 400 when the access token is invalid", func(t *testing.T) {
|
||||
server, _ := setup(true)
|
||||
resp := callAPI(server, http.MethodPost, getValidQueryPath("SomeInvalidAccessToken"), strings.NewReader("{}"), t)
|
||||
require.Equal(t, http.StatusBadRequest, resp.Code)
|
||||
require.JSONEq(t, "{\"message\":\"Invalid Access Token\"}", resp.Body.String())
|
||||
})
|
||||
|
||||
t.Run("Status code is 400 when the intervalMS is lesser than 0", func(t *testing.T) {
|
||||
server, fakeDashboardService := setup(true)
|
||||
fakeDashboardService.On("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), "abc123").Return(&backend.QueryDataResponse{}, ErrPublicDashboardBadRequest)
|
||||
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader(`{"intervalMs":-100,"maxDataPoints":1000}`), t)
|
||||
fakeDashboardService.On("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), validAccessToken).Return(&backend.QueryDataResponse{}, ErrPublicDashboardBadRequest)
|
||||
resp := callAPI(server, http.MethodPost, getValidQueryPath(validAccessToken), strings.NewReader(`{"intervalMs":-100,"maxDataPoints":1000}`), t)
|
||||
require.Equal(t, http.StatusBadRequest, resp.Code)
|
||||
})
|
||||
|
||||
t.Run("Status code is 400 when the maxDataPoints is lesser than 0", func(t *testing.T) {
|
||||
server, fakeDashboardService := setup(true)
|
||||
fakeDashboardService.On("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), "abc123").Return(&backend.QueryDataResponse{}, ErrPublicDashboardBadRequest)
|
||||
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader(`{"intervalMs":100,"maxDataPoints":-1000}`), t)
|
||||
fakeDashboardService.On("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), validAccessToken).Return(&backend.QueryDataResponse{}, ErrPublicDashboardBadRequest)
|
||||
resp := callAPI(server, http.MethodPost, getValidQueryPath(validAccessToken), strings.NewReader(`{"intervalMs":100,"maxDataPoints":-1000}`), t)
|
||||
require.Equal(t, http.StatusBadRequest, resp.Code)
|
||||
})
|
||||
|
||||
t.Run("Returns query data when feature toggle is enabled", func(t *testing.T) {
|
||||
server, fakeDashboardService := setup(true)
|
||||
fakeDashboardService.On("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), "abc123").Return(mockedResponse, nil)
|
||||
fakeDashboardService.On("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), validAccessToken).Return(mockedResponse, nil)
|
||||
|
||||
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t)
|
||||
resp := callAPI(server, http.MethodPost, getValidQueryPath(validAccessToken), strings.NewReader("{}"), t)
|
||||
|
||||
require.JSONEq(
|
||||
t,
|
||||
@ -619,13 +657,17 @@ func TestAPIQueryPublicDashboard(t *testing.T) {
|
||||
|
||||
t.Run("Status code is 500 when the query fails", func(t *testing.T) {
|
||||
server, fakeDashboardService := setup(true)
|
||||
fakeDashboardService.On("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), "abc123").Return(&backend.QueryDataResponse{}, fmt.Errorf("error"))
|
||||
fakeDashboardService.On("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), validAccessToken).Return(&backend.QueryDataResponse{}, fmt.Errorf("error"))
|
||||
|
||||
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t)
|
||||
resp := callAPI(server, http.MethodPost, getValidQueryPath(validAccessToken), strings.NewReader("{}"), t)
|
||||
require.Equal(t, http.StatusInternalServerError, resp.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func getValidQueryPath(accessToken string) string {
|
||||
return fmt.Sprintf("/api/public/dashboards/%s/panels/2/query", accessToken)
|
||||
}
|
||||
|
||||
func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) {
|
||||
db := db.InitTestDB(t)
|
||||
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
// Adds orgId to context based on org of public dashboard
|
||||
// SetPublicDashboardOrgIdOnContext Adds orgId to context based on org of public dashboard
|
||||
func SetPublicDashboardOrgIdOnContext(publicDashboardService publicdashboards.Service) func(c *models.ReqContext) {
|
||||
return func(c *models.ReqContext) {
|
||||
accessToken, ok := web.Params(c.Req)[":accessToken"]
|
||||
@ -28,14 +28,15 @@ func SetPublicDashboardOrgIdOnContext(publicDashboardService publicdashboards.Se
|
||||
}
|
||||
}
|
||||
|
||||
// Adds public dashboard flag on context
|
||||
// SetPublicDashboardFlag Adds public dashboard flag on context
|
||||
func SetPublicDashboardFlag(c *models.ReqContext) {
|
||||
c.IsPublicDashboardView = true
|
||||
}
|
||||
|
||||
// Middleware to enforce that a public dashboards exists before continuing to
|
||||
// handler
|
||||
func RequiresValidAccessToken(publicDashboardService publicdashboards.Service) func(c *models.ReqContext) {
|
||||
// RequiresExistingAccessToken Middleware to enforce that a public dashboards exists before continuing to handler. This
|
||||
// method will query the database to ensure that it exists.
|
||||
// Use when we want to enforce a public dashboard is valid on an endpoint we do not maintain
|
||||
func RequiresExistingAccessToken(publicDashboardService publicdashboards.Service) func(c *models.ReqContext) {
|
||||
return func(c *models.ReqContext) {
|
||||
accessToken, ok := web.Params(c.Req)[":accessToken"]
|
||||
|
||||
|
@ -20,7 +20,7 @@ import (
|
||||
|
||||
var validAccessToken, _ = tokens.GenerateAccessToken()
|
||||
|
||||
func TestRequiresValidAccessToken(t *testing.T) {
|
||||
func TestRequiresExistingAccessToken(t *testing.T) {
|
||||
tests := []struct {
|
||||
Name string
|
||||
Path string
|
||||
@ -76,7 +76,7 @@ func TestRequiresValidAccessToken(t *testing.T) {
|
||||
publicdashboardService := &publicdashboards.FakePublicDashboardService{}
|
||||
publicdashboardService.On("AccessTokenExists", mock.Anything, mock.Anything).Return(tt.AccessTokenExists, tt.AccessTokenExistsErr)
|
||||
params := map[string]string{":accessToken": tt.AccessToken}
|
||||
mw := RequiresValidAccessToken(publicdashboardService)
|
||||
mw := RequiresExistingAccessToken(publicdashboardService)
|
||||
_, resp := runMw(t, nil, "GET", tt.Path, params, mw)
|
||||
require.Equal(t, tt.ExpectedResponseCode, resp.Code)
|
||||
})
|
||||
|
@ -6,17 +6,16 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// generates a uuid formatted without dashes to use as access token
|
||||
// GenerateAccessToken generates an uuid formatted without dashes to use as access token
|
||||
func GenerateAccessToken() (string, error) {
|
||||
token, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", token[:]), nil
|
||||
}
|
||||
|
||||
// asserts that an accessToken is a valid uuid
|
||||
// IsValidAccessToken asserts that an accessToken is a valid uuid
|
||||
func IsValidAccessToken(token string) bool {
|
||||
_, err := uuid.Parse(token)
|
||||
return err == nil
|
||||
|
@ -25,7 +25,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/tsdb/legacydata"
|
||||
)
|
||||
|
||||
// Define the Service Implementation. We're generating mock implementation
|
||||
// PublicDashboardServiceImpl Define the Service Implementation. We're generating mock implementation
|
||||
// automatically
|
||||
type PublicDashboardServiceImpl struct {
|
||||
log log.Logger
|
||||
@ -43,7 +43,7 @@ var LogPrefix = "publicdashboards.service"
|
||||
// the interface
|
||||
var _ publicdashboards.Service = (*PublicDashboardServiceImpl)(nil)
|
||||
|
||||
// Factory for method used by wire to inject dependencies.
|
||||
// ProvideService Factory for method used by wire to inject dependencies.
|
||||
// builds the service, and api, and configures routes
|
||||
func ProvideService(
|
||||
cfg *setting.Cfg,
|
||||
@ -63,7 +63,7 @@ func ProvideService(
|
||||
}
|
||||
}
|
||||
|
||||
// Gets a dashboard by Uid
|
||||
// GetDashboard Gets a dashboard by Uid
|
||||
func (pd *PublicDashboardServiceImpl) GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) {
|
||||
dashboard, err := pd.store.GetDashboard(ctx, dashboardUid)
|
||||
|
||||
@ -74,7 +74,7 @@ func (pd *PublicDashboardServiceImpl) GetDashboard(ctx context.Context, dashboar
|
||||
return dashboard, err
|
||||
}
|
||||
|
||||
// Gets public dashboard via access token
|
||||
// GetPublicDashboard Gets public dashboard via access token
|
||||
func (pd *PublicDashboardServiceImpl) GetPublicDashboard(ctx context.Context, accessToken string) (*PublicDashboard, *models.Dashboard, error) {
|
||||
pubdash, dash, err := pd.store.GetPublicDashboard(ctx, accessToken)
|
||||
ctxLogger := pd.log.FromContext(ctx)
|
||||
|
Loading…
Reference in New Issue
Block a user