Chore: Move ReqContext to contexthandler service (#62102)
* Chore: Move ReqContext to contexthandler service * Rename package to contextmodel * Generate ngalert files * Remove unused imports
This commit is contained in:
+17
-17
@@ -10,9 +10,9 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/middleware/cookies"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
@@ -27,7 +27,7 @@ type AuthOptions struct {
|
||||
ReqNoAnonynmous bool
|
||||
}
|
||||
|
||||
func accessForbidden(c *models.ReqContext) {
|
||||
func accessForbidden(c *contextmodel.ReqContext) {
|
||||
if c.IsApiRequest() {
|
||||
c.JsonApiErr(403, "Permission denied", nil)
|
||||
return
|
||||
@@ -36,7 +36,7 @@ func accessForbidden(c *models.ReqContext) {
|
||||
c.Redirect(setting.AppSubUrl + "/")
|
||||
}
|
||||
|
||||
func notAuthorized(c *models.ReqContext) {
|
||||
func notAuthorized(c *contextmodel.ReqContext) {
|
||||
if c.IsApiRequest() {
|
||||
c.WriteErrOrFallback(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized), c.LookupTokenErr)
|
||||
return
|
||||
@@ -46,7 +46,7 @@ func notAuthorized(c *models.ReqContext) {
|
||||
c.Redirect(setting.AppSubUrl + "/login")
|
||||
}
|
||||
|
||||
func tokenRevoked(c *models.ReqContext, err *auth.TokenRevokedError) {
|
||||
func tokenRevoked(c *contextmodel.ReqContext, err *auth.TokenRevokedError) {
|
||||
if c.IsApiRequest() {
|
||||
c.JSON(401, map[string]interface{}{
|
||||
"message": "Token revoked",
|
||||
@@ -62,7 +62,7 @@ func tokenRevoked(c *models.ReqContext, err *auth.TokenRevokedError) {
|
||||
c.Redirect(setting.AppSubUrl + "/login")
|
||||
}
|
||||
|
||||
func writeRedirectCookie(c *models.ReqContext) {
|
||||
func writeRedirectCookie(c *contextmodel.ReqContext) {
|
||||
redirectTo := c.Req.RequestURI
|
||||
if setting.AppSubUrl != "" && !strings.HasPrefix(redirectTo, setting.AppSubUrl) {
|
||||
redirectTo = setting.AppSubUrl + c.Req.RequestURI
|
||||
@@ -83,14 +83,14 @@ func removeForceLoginParams(str string) string {
|
||||
return forceLoginParamsRegexp.ReplaceAllString(str, "")
|
||||
}
|
||||
|
||||
func EnsureEditorOrViewerCanEdit(c *models.ReqContext) {
|
||||
func EnsureEditorOrViewerCanEdit(c *contextmodel.ReqContext) {
|
||||
if !c.SignedInUser.HasRole(org.RoleEditor) && !setting.ViewersCanEdit {
|
||||
accessForbidden(c)
|
||||
}
|
||||
}
|
||||
|
||||
func CanAdminPlugins(cfg *setting.Cfg) func(c *models.ReqContext) {
|
||||
return func(c *models.ReqContext) {
|
||||
func CanAdminPlugins(cfg *setting.Cfg) func(c *contextmodel.ReqContext) {
|
||||
return func(c *contextmodel.ReqContext) {
|
||||
if !plugins.ReqCanAdminPlugins(cfg)(c) {
|
||||
accessForbidden(c)
|
||||
return
|
||||
@@ -99,7 +99,7 @@ func CanAdminPlugins(cfg *setting.Cfg) func(c *models.ReqContext) {
|
||||
}
|
||||
|
||||
func RoleAuth(roles ...org.RoleType) web.Handler {
|
||||
return func(c *models.ReqContext) {
|
||||
return func(c *contextmodel.ReqContext) {
|
||||
ok := false
|
||||
for _, role := range roles {
|
||||
if role == c.OrgRole {
|
||||
@@ -114,7 +114,7 @@ func RoleAuth(roles ...org.RoleType) web.Handler {
|
||||
}
|
||||
|
||||
func Auth(options *AuthOptions) web.Handler {
|
||||
return func(c *models.ReqContext) {
|
||||
return func(c *contextmodel.ReqContext) {
|
||||
forceLogin := false
|
||||
if c.AllowAnonymous {
|
||||
forceLogin = shouldForceLogin(c)
|
||||
@@ -153,7 +153,7 @@ func Auth(options *AuthOptions) web.Handler {
|
||||
// Intended for when feature flags open up access to APIs that
|
||||
// are otherwise only available to admins.
|
||||
func AdminOrEditorAndFeatureEnabled(enabled bool) web.Handler {
|
||||
return func(c *models.ReqContext) {
|
||||
return func(c *contextmodel.ReqContext) {
|
||||
if c.OrgRole == org.RoleAdmin {
|
||||
return
|
||||
}
|
||||
@@ -169,7 +169,7 @@ func AdminOrEditorAndFeatureEnabled(enabled bool) web.Handler {
|
||||
// SnapshotPublicModeOrSignedIn creates a middleware that allows access
|
||||
// if snapshot public mode is enabled or if user is signed in.
|
||||
func SnapshotPublicModeOrSignedIn(cfg *setting.Cfg) web.Handler {
|
||||
return func(c *models.ReqContext) {
|
||||
return func(c *contextmodel.ReqContext) {
|
||||
if cfg.SnapshotPublicMode {
|
||||
return
|
||||
}
|
||||
@@ -181,7 +181,7 @@ func SnapshotPublicModeOrSignedIn(cfg *setting.Cfg) web.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
func ReqNotSignedIn(c *models.ReqContext) {
|
||||
func ReqNotSignedIn(c *contextmodel.ReqContext) {
|
||||
if c.IsSignedIn {
|
||||
c.Redirect(setting.AppSubUrl + "/")
|
||||
}
|
||||
@@ -190,7 +190,7 @@ func ReqNotSignedIn(c *models.ReqContext) {
|
||||
// NoAuth creates a middleware that doesn't require any authentication.
|
||||
// If forceLogin param is set it will redirect the user to the login page.
|
||||
func NoAuth() web.Handler {
|
||||
return func(c *models.ReqContext) {
|
||||
return func(c *contextmodel.ReqContext) {
|
||||
if shouldForceLogin(c) {
|
||||
notAuthorized(c)
|
||||
return
|
||||
@@ -200,7 +200,7 @@ func NoAuth() web.Handler {
|
||||
|
||||
// shouldForceLogin checks if user should be enforced to login.
|
||||
// Returns true if forceLogin parameter is set.
|
||||
func shouldForceLogin(c *models.ReqContext) bool {
|
||||
func shouldForceLogin(c *contextmodel.ReqContext) bool {
|
||||
forceLogin := false
|
||||
forceLoginParam, err := strconv.ParseBool(c.Req.URL.Query().Get("forceLogin"))
|
||||
if err == nil {
|
||||
@@ -210,8 +210,8 @@ func shouldForceLogin(c *models.ReqContext) bool {
|
||||
return forceLogin
|
||||
}
|
||||
|
||||
func OrgAdminDashOrFolderAdminOrTeamAdmin(ss db.DB, ds dashboards.DashboardService, ts team.Service) func(c *models.ReqContext) {
|
||||
return func(c *models.ReqContext) {
|
||||
func OrgAdminDashOrFolderAdminOrTeamAdmin(ss db.DB, ds dashboards.DashboardService, ts team.Service) func(c *contextmodel.ReqContext) {
|
||||
return func(c *contextmodel.ReqContext) {
|
||||
if c.OrgRole == org.RoleAdmin {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
@@ -87,7 +87,7 @@ func TestMiddlewareAuth(t *testing.T) {
|
||||
|
||||
middlewareScenario(t, "Snapshot public mode disabled and unauthenticated request should return 401", func(
|
||||
t *testing.T, sc *scenarioContext) {
|
||||
sc.m.Get("/api/snapshot", func(c *models.ReqContext) {
|
||||
sc.m.Get("/api/snapshot", func(c *contextmodel.ReqContext) {
|
||||
c.IsSignedIn = false
|
||||
}, SnapshotPublicModeOrSignedIn(sc.cfg), sc.defaultHandler)
|
||||
sc.fakeReq("GET", "/api/snapshot").exec()
|
||||
@@ -96,7 +96,7 @@ func TestMiddlewareAuth(t *testing.T) {
|
||||
|
||||
middlewareScenario(t, "Snapshot public mode disabled and authenticated request should return 200", func(
|
||||
t *testing.T, sc *scenarioContext) {
|
||||
sc.m.Get("/api/snapshot", func(c *models.ReqContext) {
|
||||
sc.m.Get("/api/snapshot", func(c *contextmodel.ReqContext) {
|
||||
c.IsSignedIn = true
|
||||
}, SnapshotPublicModeOrSignedIn(sc.cfg), sc.defaultHandler)
|
||||
sc.fakeReq("GET", "/api/snapshot").exec()
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@@ -55,7 +55,7 @@ func WriteCookie(w http.ResponseWriter, name string, value string, maxAge int, g
|
||||
http.SetCookie(w, &cookie)
|
||||
}
|
||||
|
||||
func WriteSessionCookie(ctx *models.ReqContext, cfg *setting.Cfg, value string, maxLifetime time.Duration) {
|
||||
func WriteSessionCookie(ctx *contextmodel.ReqContext, cfg *setting.Cfg, value string, maxLifetime time.Duration) {
|
||||
if cfg.Env == setting.Dev {
|
||||
ctx.Logger.Info("New token", "unhashed token", value)
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
// In Grafana v7.0 we changed panel edit & view query parameters.
|
||||
// This middleware tries to detect those old url parameters and direct to the new url query params
|
||||
func RedirectFromLegacyPanelEditURL(cfg *setting.Cfg) func(c *models.ReqContext) {
|
||||
return func(c *models.ReqContext) {
|
||||
func RedirectFromLegacyPanelEditURL(cfg *setting.Cfg) func(c *contextmodel.ReqContext) {
|
||||
return func(c *contextmodel.ReqContext) {
|
||||
queryParams := c.Req.URL.Query()
|
||||
|
||||
panelID, hasPanelID := queryParams["panelId"]
|
||||
|
||||
@@ -21,8 +21,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
@@ -89,7 +89,7 @@ var sensitiveQueryStrings = [...]string{
|
||||
"auth_token",
|
||||
}
|
||||
|
||||
func SanitizeURL(ctx *models.ReqContext, s string) string {
|
||||
func SanitizeURL(ctx *contextmodel.ReqContext, s string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_sanitizeURL(t *testing.T) {
|
||||
type args struct {
|
||||
ctx *models.ReqContext
|
||||
ctx *contextmodel.ReqContext
|
||||
s string
|
||||
}
|
||||
tests := []struct {
|
||||
@@ -21,7 +21,7 @@ func Test_sanitizeURL(t *testing.T) {
|
||||
{
|
||||
name: "Receiving empty string should return it",
|
||||
args: args{
|
||||
ctx: &models.ReqContext{
|
||||
ctx: &contextmodel.ReqContext{
|
||||
Logger: log.New("test.logger"),
|
||||
},
|
||||
s: "",
|
||||
@@ -31,7 +31,7 @@ func Test_sanitizeURL(t *testing.T) {
|
||||
{
|
||||
name: "Receiving valid URL string should return it parsed",
|
||||
args: args{
|
||||
ctx: &models.ReqContext{
|
||||
ctx: &contextmodel.ReqContext{
|
||||
Logger: log.New("test.logger"),
|
||||
},
|
||||
s: "https://grafana.com/",
|
||||
@@ -41,7 +41,7 @@ func Test_sanitizeURL(t *testing.T) {
|
||||
{
|
||||
name: "Receiving invalid URL string should return empty string",
|
||||
args: args{
|
||||
ctx: &models.ReqContext{
|
||||
ctx: &contextmodel.ReqContext{
|
||||
Logger: log.New("test.logger"),
|
||||
},
|
||||
s: "this is not a valid URL",
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
@@ -21,7 +21,7 @@ var (
|
||||
ReqOrgAdmin = RoleAuth(org.RoleAdmin)
|
||||
)
|
||||
|
||||
func HandleNoCacheHeader(ctx *models.ReqContext) {
|
||||
func HandleNoCacheHeader(ctx *contextmodel.ReqContext) {
|
||||
ctx.SkipCache = ctx.Req.Header.Get("X-Grafana-NoCache") == "true"
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/authn/authntest"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler/authproxy"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/login/loginservice"
|
||||
"github.com/grafana/grafana/pkg/services/login/logintest"
|
||||
@@ -162,7 +163,7 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
|
||||
middlewareScenario(t, "middleware should add Cache-Control header for requests with HTML response", func(
|
||||
t *testing.T, sc *scenarioContext) {
|
||||
sc.handlerFunc = func(c *models.ReqContext) {
|
||||
sc.handlerFunc = func(c *contextmodel.ReqContext) {
|
||||
t.Log("Handler called")
|
||||
data := &dtos.IndexViewData{
|
||||
User: &dtos.CurrentUser{},
|
||||
@@ -721,7 +722,7 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
body := "key=value"
|
||||
sc.req.Body = io.NopCloser(strings.NewReader(body))
|
||||
|
||||
sc.handlerFunc = func(c *models.ReqContext) {
|
||||
sc.handlerFunc = func(c *contextmodel.ReqContext) {
|
||||
t.Log("Handler called")
|
||||
defer func() {
|
||||
err := c.Req.Body.Close()
|
||||
@@ -745,7 +746,7 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
body := "key=value"
|
||||
sc.req.Body = io.NopCloser(strings.NewReader(body))
|
||||
|
||||
sc.handlerFunc = func(c *models.ReqContext) {
|
||||
sc.handlerFunc = func(c *contextmodel.ReqContext) {
|
||||
t.Log("Handler called")
|
||||
defer func() {
|
||||
err := c.Req.Body.Close()
|
||||
@@ -889,7 +890,7 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func(
|
||||
sc.jwtAuthService = ctxHdlr.JWTAuthService.(*jwt.FakeJWTService)
|
||||
sc.remoteCacheService = ctxHdlr.RemoteCache
|
||||
|
||||
sc.defaultHandler = func(c *models.ReqContext) {
|
||||
sc.defaultHandler = func(c *contextmodel.ReqContext) {
|
||||
require.NotNil(t, c)
|
||||
t.Log("Default HTTP handler called")
|
||||
sc.context = c
|
||||
|
||||
@@ -3,7 +3,7 @@ package middleware
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
@@ -15,7 +15,7 @@ func Quota(quotaService quota.Service) func(string) web.Handler {
|
||||
}
|
||||
//https://open.spotify.com/track/7bZSoBEAEEUsGEuLOf94Jm?si=T1Tdju5qRSmmR0zph_6RBw fuuuuunky
|
||||
return func(targetSrv string) web.Handler {
|
||||
return func(c *models.ReqContext) {
|
||||
return func(c *contextmodel.ReqContext) {
|
||||
limitReached, err := quotaService.QuotaReached(c, quota.TargetSrv(targetSrv))
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Failed to get quota", err)
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/auth/authtest"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
@@ -42,7 +42,7 @@ func TestRecoveryMiddleware(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func panicHandler(c *models.ReqContext) {
|
||||
func panicHandler(c *contextmodel.ReqContext) {
|
||||
panic("Handler has panicked")
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ func recoveryScenario(t *testing.T, desc string, url string, fn scenarioFunc) {
|
||||
// mock out gc goroutine
|
||||
sc.m.Use(OrgRedirect(cfg, sc.userService))
|
||||
|
||||
sc.defaultHandler = func(c *models.ReqContext) {
|
||||
sc.defaultHandler = func(c *contextmodel.ReqContext) {
|
||||
sc.context = c
|
||||
if sc.handlerFunc != nil {
|
||||
sc.handlerFunc(sc.context)
|
||||
|
||||
@@ -11,12 +11,12 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/db/dbtest"
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/apikey/apikeytest"
|
||||
"github.com/grafana/grafana/pkg/services/auth/authtest"
|
||||
"github.com/grafana/grafana/pkg/services/auth/jwt"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/login/loginservice"
|
||||
"github.com/grafana/grafana/pkg/services/org/orgtest"
|
||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
type scenarioContext struct {
|
||||
t *testing.T
|
||||
m *web.Mux
|
||||
context *models.ReqContext
|
||||
context *contextmodel.ReqContext
|
||||
resp *httptest.ResponseRecorder
|
||||
apiKey string
|
||||
authHeader string
|
||||
@@ -80,7 +80,7 @@ func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext {
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
require.NoError(sc.t, err)
|
||||
|
||||
reqCtx := &models.ReqContext{
|
||||
reqCtx := &contextmodel.ReqContext{
|
||||
Context: web.FromContext(req.Context()),
|
||||
}
|
||||
sc.req = req.WithContext(ctxkey.Set(req.Context(), reqCtx))
|
||||
@@ -102,7 +102,7 @@ func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map
|
||||
req.URL.RawQuery = q.Encode()
|
||||
require.NoError(sc.t, err)
|
||||
|
||||
reqCtx := &models.ReqContext{
|
||||
reqCtx := &contextmodel.ReqContext{
|
||||
Context: web.FromContext(req.Context()),
|
||||
}
|
||||
sc.req = req.WithContext(ctxkey.Set(req.Context(), reqCtx))
|
||||
@@ -147,4 +147,4 @@ func (sc *scenarioContext) exec() {
|
||||
}
|
||||
|
||||
type scenarioFunc func(t *testing.T, c *scenarioContext)
|
||||
type handlerFunc func(c *models.ReqContext)
|
||||
type handlerFunc func(c *contextmodel.ReqContext)
|
||||
|
||||
@@ -3,13 +3,13 @@ package middleware
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
func ValidateHostHeader(cfg *setting.Cfg) web.Handler {
|
||||
return func(c *models.ReqContext) {
|
||||
return func(c *contextmodel.ReqContext) {
|
||||
// ignore local render calls
|
||||
if c.IsRenderCall {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user