mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
PublicDashboards: moved tokens service and new repository method (#61806)
This commit is contained in:
parent
89ef62f163
commit
a128944471
@ -13,8 +13,8 @@ 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/services/publicdashboards/validation"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
@ -102,7 +102,7 @@ func (api *Api) ListPublicDashboards(c *contextmodel.ReqContext) response.Respon
|
||||
func (api *Api) GetPublicDashboard(c *contextmodel.ReqContext) response.Response {
|
||||
// exit if we don't have a valid dashboardUid
|
||||
dashboardUid := web.Params(c.Req)[":dashboardUid"]
|
||||
if !tokens.IsValidShortUID(dashboardUid) {
|
||||
if !validation.IsValidShortUID(dashboardUid) {
|
||||
return response.Err(ErrPublicDashboardIdentifierNotSet.Errorf("GetPublicDashboard: no dashboard Uid for public dashboard specified"))
|
||||
}
|
||||
|
||||
@ -123,7 +123,7 @@ func (api *Api) GetPublicDashboard(c *contextmodel.ReqContext) response.Response
|
||||
func (api *Api) CreatePublicDashboard(c *contextmodel.ReqContext) response.Response {
|
||||
// exit if we don't have a valid dashboardUid
|
||||
dashboardUid := web.Params(c.Req)[":dashboardUid"]
|
||||
if !tokens.IsValidShortUID(dashboardUid) {
|
||||
if !validation.IsValidShortUID(dashboardUid) {
|
||||
return response.Err(ErrInvalidUid.Errorf("CreatePublicDashboard: invalid Uid %s", dashboardUid))
|
||||
}
|
||||
|
||||
@ -155,12 +155,12 @@ func (api *Api) CreatePublicDashboard(c *contextmodel.ReqContext) response.Respo
|
||||
func (api *Api) UpdatePublicDashboard(c *contextmodel.ReqContext) response.Response {
|
||||
// exit if we don't have a valid dashboardUid
|
||||
dashboardUid := web.Params(c.Req)[":dashboardUid"]
|
||||
if !tokens.IsValidShortUID(dashboardUid) {
|
||||
if !validation.IsValidShortUID(dashboardUid) {
|
||||
return response.Err(ErrInvalidUid.Errorf("UpdatePublicDashboard: invalid dashboard Uid %s", dashboardUid))
|
||||
}
|
||||
|
||||
uid := web.Params(c.Req)[":uid"]
|
||||
if !tokens.IsValidShortUID(uid) {
|
||||
if !validation.IsValidShortUID(uid) {
|
||||
return response.Err(ErrInvalidUid.Errorf("UpdatePublicDashboard: invalid Uid %s", uid))
|
||||
}
|
||||
|
||||
@ -192,7 +192,7 @@ func (api *Api) UpdatePublicDashboard(c *contextmodel.ReqContext) response.Respo
|
||||
// DELETE /api/dashboards/uid/:dashboardUid/public-dashboards/:uid
|
||||
func (api *Api) DeletePublicDashboard(c *contextmodel.ReqContext) response.Response {
|
||||
uid := web.Params(c.Req)[":uid"]
|
||||
if !tokens.IsValidShortUID(uid) {
|
||||
if !validation.IsValidShortUID(uid) {
|
||||
return response.Err(ErrInvalidUid.Errorf("UpdatePublicDashboard: invalid Uid %s", uid))
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards/validation"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
@ -14,7 +14,7 @@ import (
|
||||
func SetPublicDashboardOrgIdOnContext(publicDashboardService publicdashboards.Service) func(c *contextmodel.ReqContext) {
|
||||
return func(c *contextmodel.ReqContext) {
|
||||
accessToken, ok := web.Params(c.Req)[":accessToken"]
|
||||
if !ok || !tokens.IsValidAccessToken(accessToken) {
|
||||
if !ok || !validation.IsValidAccessToken(accessToken) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@ func RequiresExistingAccessToken(publicDashboardService publicdashboards.Service
|
||||
return
|
||||
}
|
||||
|
||||
if !tokens.IsValidAccessToken(accessToken) {
|
||||
if !validation.IsValidAccessToken(accessToken) {
|
||||
c.JsonApiErr(http.StatusBadRequest, "Invalid access token", nil)
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards/service"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -18,7 +18,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var validAccessToken, _ = tokens.GenerateAccessToken()
|
||||
var validAccessToken, _ = service.GenerateAccessToken()
|
||||
|
||||
func TestRequiresExistingAccessToken(t *testing.T) {
|
||||
tests := []struct {
|
||||
|
@ -8,8 +8,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens"
|
||||
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards/validation"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
@ -17,7 +17,7 @@ import (
|
||||
// GET /api/public/dashboards/:accessToken
|
||||
func (api *Api) ViewPublicDashboard(c *contextmodel.ReqContext) response.Response {
|
||||
accessToken := web.Params(c.Req)[":accessToken"]
|
||||
if !tokens.IsValidAccessToken(accessToken) {
|
||||
if !validation.IsValidAccessToken(accessToken) {
|
||||
return response.Err(ErrInvalidAccessToken.Errorf("ViewPublicDashboard: invalid access token"))
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ func (api *Api) ViewPublicDashboard(c *contextmodel.ReqContext) response.Respons
|
||||
// POST /api/public/dashboard/:accessToken/panels/:panelId/query
|
||||
func (api *Api) QueryPublicDashboard(c *contextmodel.ReqContext) response.Response {
|
||||
accessToken := web.Params(c.Req)[":accessToken"]
|
||||
if !tokens.IsValidAccessToken(accessToken) {
|
||||
if !validation.IsValidAccessToken(accessToken) {
|
||||
return response.Err(ErrInvalidAccessToken.Errorf("QueryPublicDashboard: invalid access token"))
|
||||
}
|
||||
|
||||
@ -81,7 +81,7 @@ func (api *Api) QueryPublicDashboard(c *contextmodel.ReqContext) response.Respon
|
||||
// GET /api/public/dashboards/:accessToken/annotations
|
||||
func (api *Api) GetAnnotations(c *contextmodel.ReqContext) response.Response {
|
||||
accessToken := web.Params(c.Req)[":accessToken"]
|
||||
if !tokens.IsValidAccessToken(accessToken) {
|
||||
if !validation.IsValidAccessToken(accessToken) {
|
||||
return response.Err(ErrInvalidAccessToken.Errorf("GetAnnotations: invalid access token"))
|
||||
}
|
||||
|
||||
|
@ -10,8 +10,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens"
|
||||
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards/service"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -693,7 +693,7 @@ func insertPublicDashboard(t *testing.T, publicdashboardStore *PublicDashboardSt
|
||||
|
||||
uid := util.GenerateShortUID()
|
||||
|
||||
accessToken, err := tokens.GenerateAccessToken()
|
||||
accessToken, err := service.GenerateAccessToken()
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd := SavePublicDashboardCommand{
|
||||
|
@ -1,29 +0,0 @@
|
||||
package tokens
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// IsValidAccessToken asserts that an accessToken is a valid uuid
|
||||
func IsValidAccessToken(token string) bool {
|
||||
_, err := uuid.Parse(token)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsValidShortUID checks that the uid is not blank and contains valid
|
||||
// characters. Wraps utils.IsValidShortUID
|
||||
func IsValidShortUID(uid string) bool {
|
||||
return uid != "" && util.IsValidShortUID(uid)
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package tokens
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGenerateAccessToken(t *testing.T) {
|
||||
accessToken, err := GenerateAccessToken()
|
||||
|
||||
t.Run("length", func(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 32, len(accessToken))
|
||||
})
|
||||
|
||||
t.Run("no - ", func(t *testing.T) {
|
||||
assert.False(t, strings.Contains("-", accessToken))
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidAccessToken(t *testing.T) {
|
||||
t.Run("true", func(t *testing.T) {
|
||||
uuid, _ := GenerateAccessToken()
|
||||
assert.True(t, IsValidAccessToken(uuid))
|
||||
})
|
||||
|
||||
t.Run("false when blank", func(t *testing.T) {
|
||||
assert.False(t, IsValidAccessToken(""))
|
||||
})
|
||||
|
||||
t.Run("false when can't be parsed by uuid lib", func(t *testing.T) {
|
||||
// too long
|
||||
assert.False(t, IsValidAccessToken("0123456789012345678901234567890123456789"))
|
||||
})
|
||||
}
|
||||
|
||||
// we just check base cases since this wraps utils.IsValidShortUID which has
|
||||
// test coverage
|
||||
func TestValidUid(t *testing.T) {
|
||||
t.Run("true", func(t *testing.T) {
|
||||
assert.True(t, IsValidShortUID("afqrz7jZZ"))
|
||||
})
|
||||
|
||||
t.Run("false when blank", func(t *testing.T) {
|
||||
assert.False(t, IsValidShortUID(""))
|
||||
})
|
||||
|
||||
t.Run("false when invalid chars", func(t *testing.T) {
|
||||
assert.False(t, IsValidShortUID("afqrz7j%%"))
|
||||
})
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.16.0. DO NOT EDIT.
|
||||
// Code generated by mockery v2.14.0. DO NOT EDIT.
|
||||
|
||||
package publicdashboards
|
||||
|
||||
@ -102,6 +102,29 @@ func (_m *FakePublicDashboardService) ExistsEnabledByDashboardUid(ctx context.Co
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Find provides a mock function with given fields: ctx, uid
|
||||
func (_m *FakePublicDashboardService) Find(ctx context.Context, uid string) (*models.PublicDashboard, error) {
|
||||
ret := _m.Called(ctx, uid)
|
||||
|
||||
var r0 *models.PublicDashboard
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) *models.PublicDashboard); ok {
|
||||
r0 = rf(ctx, uid)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*models.PublicDashboard)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, uid)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// FindAll provides a mock function with given fields: ctx, u, orgId
|
||||
func (_m *FakePublicDashboardService) FindAll(ctx context.Context, u *user.SignedInUser, orgId int64) ([]models.PublicDashboardListResponse, error) {
|
||||
ret := _m.Called(ctx, u, orgId)
|
||||
|
@ -21,6 +21,7 @@ type Service interface {
|
||||
FindAnnotations(ctx context.Context, reqDTO AnnotationsQueryDTO, accessToken string) ([]AnnotationEvent, error)
|
||||
FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error)
|
||||
FindAll(ctx context.Context, u *user.SignedInUser, orgId int64) ([]PublicDashboardListResponse, error)
|
||||
Find(ctx context.Context, uid string) (*PublicDashboard, error)
|
||||
Create(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error)
|
||||
Update(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error)
|
||||
Delete(ctx context.Context, orgId int64, uid string) error
|
||||
|
@ -3,15 +3,16 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"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/services/publicdashboards/validation"
|
||||
"github.com/grafana/grafana/pkg/services/query"
|
||||
@ -60,6 +61,14 @@ func ProvideService(
|
||||
}
|
||||
}
|
||||
|
||||
func (pd *PublicDashboardServiceImpl) Find(ctx context.Context, uid string) (*PublicDashboard, error) {
|
||||
pubdash, err := pd.store.Find(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, ErrInternalServerError.Errorf("Find: failed to find public dashboard%w", err)
|
||||
}
|
||||
return pubdash, nil
|
||||
}
|
||||
|
||||
// FindDashboard Gets a dashboard by Uid
|
||||
func (pd *PublicDashboardServiceImpl) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error) {
|
||||
dash, err := pd.store.FindDashboard(ctx, orgId, dashboardUid)
|
||||
@ -281,7 +290,7 @@ func (pd *PublicDashboardServiceImpl) NewPublicDashboardAccessToken(ctx context.
|
||||
var accessToken string
|
||||
for i := 0; i < 3; i++ {
|
||||
var err error
|
||||
accessToken, err = tokens.GenerateAccessToken()
|
||||
accessToken, err = GenerateAccessToken()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@ -396,3 +405,12 @@ func publicDashboardIsEnabledChanged(existingPubdash *PublicDashboard, newPubdas
|
||||
isEnabledChanged := existingPubdash != nil && newPubdash.IsEnabled != existingPubdash.IsEnabled
|
||||
return newDashCreated || isEnabledChanged
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -18,8 +19,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
. "github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards/database"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens"
|
||||
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards/validation"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
@ -914,7 +915,7 @@ func TestPublicDashboardServiceImpl_NewPublicDashboardAccessToken(t *testing.T)
|
||||
|
||||
if err == nil {
|
||||
assert.NotEqual(t, got, tt.want, "NewPublicDashboardAccessToken(%v)", tt.args.ctx)
|
||||
assert.True(t, tokens.IsValidAccessToken(got), "NewPublicDashboardAccessToken(%v)", tt.args.ctx)
|
||||
assert.True(t, validation.IsValidAccessToken(got), "NewPublicDashboardAccessToken(%v)", tt.args.ctx)
|
||||
store.AssertNumberOfCalls(t, "FindByAccessToken", 1)
|
||||
} else {
|
||||
store.AssertNumberOfCalls(t, "FindByAccessToken", 3)
|
||||
@ -1028,3 +1029,16 @@ func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardSto
|
||||
dash.Data.Set("uid", dash.UID)
|
||||
return dash
|
||||
}
|
||||
|
||||
func TestGenerateAccessToken(t *testing.T) {
|
||||
accessToken, err := GenerateAccessToken()
|
||||
|
||||
t.Run("length", func(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 32, len(accessToken))
|
||||
})
|
||||
|
||||
t.Run("no - ", func(t *testing.T) {
|
||||
assert.False(t, strings.Contains("-", accessToken))
|
||||
})
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||
"github.com/grafana/grafana/pkg/tsdb/legacydata"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func ValidatePublicDashboard(dto *SavePublicDashboardDTO, dashboard *dashboards.Dashboard) error {
|
||||
@ -44,3 +46,15 @@ func ValidateQueryPublicDashboardRequest(req PublicDashboardQueryDTO, pd *Public
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsValidAccessToken asserts that an accessToken is a valid uuid
|
||||
func IsValidAccessToken(token string) bool {
|
||||
_, err := uuid.Parse(token)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsValidShortUID checks that the uid is not blank and contains valid
|
||||
// characters. Wraps utils.IsValidShortUID
|
||||
func IsValidShortUID(uid string) bool {
|
||||
return uid != "" && util.IsValidShortUID(uid)
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -157,3 +158,35 @@ func TestValidateQueryPublicDashboardRequest(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidAccessToken(t *testing.T) {
|
||||
t.Run("true", func(t *testing.T) {
|
||||
uuid := "da82510c2aa64d78a2e87fef36c58e89"
|
||||
assert.True(t, IsValidAccessToken(uuid))
|
||||
})
|
||||
|
||||
t.Run("false when blank", func(t *testing.T) {
|
||||
assert.False(t, IsValidAccessToken(""))
|
||||
})
|
||||
|
||||
t.Run("false when can't be parsed by uuid lib", func(t *testing.T) {
|
||||
// too long
|
||||
assert.False(t, IsValidAccessToken("0123456789012345678901234567890123456789"))
|
||||
})
|
||||
}
|
||||
|
||||
// we just check base cases since this wraps utils.IsValidShortUID which has
|
||||
// test coverage
|
||||
func TestValidUid(t *testing.T) {
|
||||
t.Run("true", func(t *testing.T) {
|
||||
assert.True(t, IsValidShortUID("afqrz7jZZ"))
|
||||
})
|
||||
|
||||
t.Run("false when blank", func(t *testing.T) {
|
||||
assert.False(t, IsValidShortUID(""))
|
||||
})
|
||||
|
||||
t.Run("false when invalid chars", func(t *testing.T) {
|
||||
assert.False(t, IsValidShortUID("afqrz7j%%"))
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user