[release-11.3.6] Auth: Introduce authn.SSOClientConfig to get client config from SSOSettings service (#103001)
Auth: Introduce authn.SSOClientConfig to get client config from SSOSettings service (#94618)
* wip
* possible solution
* Separate interface for SSO settings clients
* Rename interface
* Fix tests
* Rename
* Change GetClientConfig to comma ok idiom
(cherry picked from commit 50a635bc7e)
This commit is contained in:
+15
-3
@@ -359,15 +359,27 @@ func (hs *HTTPServer) samlEnabled() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (hs *HTTPServer) samlName() string {
|
func (hs *HTTPServer) samlName() string {
|
||||||
return hs.SettingsProvider.KeyValue("auth.saml", "name").MustString("SAML")
|
config, ok := hs.authnService.GetClientConfig(authn.ClientSAML)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return config.GetDisplayName()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hs *HTTPServer) samlSingleLogoutEnabled() bool {
|
func (hs *HTTPServer) samlSingleLogoutEnabled() bool {
|
||||||
return hs.samlEnabled() && hs.SettingsProvider.KeyValue("auth.saml", "single_logout").MustBool(false) && hs.samlEnabled()
|
config, ok := hs.authnService.GetClientConfig(authn.ClientSAML)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return hs.samlEnabled() && config.IsSingleLogoutEnabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hs *HTTPServer) samlAutoLoginEnabled() bool {
|
func (hs *HTTPServer) samlAutoLoginEnabled() bool {
|
||||||
return hs.samlEnabled() && hs.SettingsProvider.KeyValue("auth.saml", "auto_login").MustBool(false)
|
config, ok := hs.authnService.GetClientConfig(authn.ClientSAML)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return hs.samlEnabled() && config.IsAutoLoginEnabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLoginExternalError(err error) string {
|
func getLoginExternalError(err error) string {
|
||||||
|
|||||||
@@ -659,7 +659,11 @@ func TestLogoutSaml(t *testing.T) {
|
|||||||
license.On("FeatureEnabled", "saml").Return(true)
|
license.On("FeatureEnabled", "saml").Return(true)
|
||||||
|
|
||||||
hs := &HTTPServer{
|
hs := &HTTPServer{
|
||||||
authnService: &authntest.FakeService{},
|
authnService: &authntest.FakeService{
|
||||||
|
ExpectedClientConfig: &authntest.FakeSSOClientConfig{
|
||||||
|
ExpectedIsSingleLogoutEnabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
Cfg: sc.cfg,
|
Cfg: sc.cfg,
|
||||||
SettingsProvider: &setting.OSSImpl{Cfg: sc.cfg},
|
SettingsProvider: &setting.OSSImpl{Cfg: sc.cfg},
|
||||||
License: license,
|
License: license,
|
||||||
|
|||||||
@@ -27,9 +27,7 @@ const (
|
|||||||
LDAPProviderName = "ldap"
|
LDAPProviderName = "ldap"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var SocialBaseUrl = "/login/"
|
||||||
SocialBaseUrl = "/login/"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
GetOAuthProviders() map[string]bool
|
GetOAuthProviders() map[string]bool
|
||||||
@@ -101,6 +99,19 @@ func NewOAuthInfo() *OAuthInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *OAuthInfo) GetDisplayName() string {
|
||||||
|
return o.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OAuthInfo) IsSingleLogoutEnabled() bool {
|
||||||
|
// OIDC SLO is not supported
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OAuthInfo) IsAutoLoginEnabled() bool {
|
||||||
|
return o.AutoLogin
|
||||||
|
}
|
||||||
|
|
||||||
type BasicUserInfo struct {
|
type BasicUserInfo struct {
|
||||||
Id string
|
Id string
|
||||||
Name string
|
Name string
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ var (
|
|||||||
errDeviceLimit = errutil.Unauthorized("anonymous.device-limit-reached", errutil.WithPublicMessage("Anonymous device limit reached. Contact Administrator"))
|
errDeviceLimit = errutil.Unauthorized("anonymous.device-limit-reached", errutil.WithPublicMessage("Anonymous device limit reached. Contact Administrator"))
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ authn.ContextAwareClient = new(Anonymous)
|
var (
|
||||||
var _ authn.IdentityResolverClient = new(Anonymous)
|
_ authn.ContextAwareClient = new(Anonymous)
|
||||||
|
_ authn.IdentityResolverClient = new(Anonymous)
|
||||||
|
)
|
||||||
|
|
||||||
type Anonymous struct {
|
type Anonymous struct {
|
||||||
cfg *setting.Cfg
|
cfg *setting.Cfg
|
||||||
|
|||||||
@@ -86,6 +86,15 @@ type Authenticator interface {
|
|||||||
Authenticate(ctx context.Context, r *Request) (*Identity, error)
|
Authenticate(ctx context.Context, r *Request) (*Identity, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SSOClientConfig interface {
|
||||||
|
// GetDisplayName returns the display name of the client
|
||||||
|
GetDisplayName() string
|
||||||
|
// IsAutoLoginEnabled returns true if the client has auto login enabled
|
||||||
|
IsAutoLoginEnabled() bool
|
||||||
|
// IsSingleLogoutEnabled returns true if the client has single logout enabled
|
||||||
|
IsSingleLogoutEnabled() bool
|
||||||
|
}
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
Authenticator
|
Authenticator
|
||||||
// RegisterPostAuthHook registers a hook with a priority that is called after a successful authentication.
|
// RegisterPostAuthHook registers a hook with a priority that is called after a successful authentication.
|
||||||
@@ -120,6 +129,9 @@ type Service interface {
|
|||||||
// - "saml" = "auth.client.saml"
|
// - "saml" = "auth.client.saml"
|
||||||
// - "github" = "auth.client.github"
|
// - "github" = "auth.client.github"
|
||||||
IsClientEnabled(client string) bool
|
IsClientEnabled(client string) bool
|
||||||
|
|
||||||
|
// GetClientConfig returns the client configuration for the given client and a boolean indicating if the config was present.
|
||||||
|
GetClientConfig(client string) (SSOClientConfig, bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
type IdentitySynchronizer interface {
|
type IdentitySynchronizer interface {
|
||||||
@@ -168,6 +180,11 @@ type LogoutClient interface {
|
|||||||
Logout(ctx context.Context, user identity.Requester) (*Redirect, bool)
|
Logout(ctx context.Context, user identity.Requester) (*Redirect, bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SSOSettingsAwareClient interface {
|
||||||
|
Client
|
||||||
|
GetConfig() SSOClientConfig
|
||||||
|
}
|
||||||
|
|
||||||
type PasswordClient interface {
|
type PasswordClient interface {
|
||||||
AuthenticatePassword(ctx context.Context, r *Request, username, password string) (*Identity, error)
|
AuthenticatePassword(ctx context.Context, r *Request, username, password string) (*Identity, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -380,6 +380,20 @@ func (s *Service) IsClientEnabled(name string) bool {
|
|||||||
return client.IsEnabled()
|
return client.IsEnabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetClientConfig(name string) (authn.SSOClientConfig, bool) {
|
||||||
|
client, ok := s.clients[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
ssoSettingsAwareClient, ok := client.(authn.SSOSettingsAwareClient)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return ssoSettingsAwareClient.GetConfig(), true
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) SyncIdentity(ctx context.Context, identity *authn.Identity) error {
|
func (s *Service) SyncIdentity(ctx context.Context, identity *authn.Identity) error {
|
||||||
ctx, span := s.tracer.Start(ctx, "authn.SyncIdentity")
|
ctx, span := s.tracer.Start(ctx, "authn.SyncIdentity")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|||||||
@@ -8,16 +8,39 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/authn"
|
"github.com/grafana/grafana/pkg/services/authn"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ authn.Service = new(FakeService)
|
var _ authn.SSOClientConfig = new(FakeSSOClientConfig)
|
||||||
var _ authn.IdentitySynchronizer = new(FakeService)
|
|
||||||
|
type FakeSSOClientConfig struct {
|
||||||
|
ExpectedName string
|
||||||
|
ExpectedIsAutoLoginEnabled bool
|
||||||
|
ExpectedIsSingleLogoutEnabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeSSOClientConfig) GetDisplayName() string {
|
||||||
|
return f.ExpectedName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeSSOClientConfig) IsAutoLoginEnabled() bool {
|
||||||
|
return f.ExpectedIsAutoLoginEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeSSOClientConfig) IsSingleLogoutEnabled() bool {
|
||||||
|
return f.ExpectedIsSingleLogoutEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ authn.Service = new(FakeService)
|
||||||
|
_ authn.IdentitySynchronizer = new(FakeService)
|
||||||
|
)
|
||||||
|
|
||||||
type FakeService struct {
|
type FakeService struct {
|
||||||
ExpectedErr error
|
ExpectedClientConfig authn.SSOClientConfig
|
||||||
ExpectedRedirect *authn.Redirect
|
ExpectedErr error
|
||||||
ExpectedIdentity *authn.Identity
|
ExpectedRedirect *authn.Redirect
|
||||||
ExpectedErrs []error
|
ExpectedIdentity *authn.Identity
|
||||||
ExpectedIdentities []*authn.Identity
|
ExpectedErrs []error
|
||||||
CurrentIndex int
|
ExpectedIdentities []*authn.Identity
|
||||||
|
CurrentIndex int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FakeService) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
func (f *FakeService) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
||||||
@@ -44,6 +67,13 @@ func (f *FakeService) IsClientEnabled(name string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FakeService) GetClientConfig(name string) (authn.SSOClientConfig, bool) {
|
||||||
|
if f.ExpectedClientConfig == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return f.ExpectedClientConfig, true
|
||||||
|
}
|
||||||
|
|
||||||
func (f *FakeService) RegisterPostAuthHook(hook authn.PostAuthHookFn, priority uint) {}
|
func (f *FakeService) RegisterPostAuthHook(hook authn.PostAuthHookFn, priority uint) {}
|
||||||
|
|
||||||
func (f *FakeService) RegisterPreLogoutHook(hook authn.PreLogoutHookFn, priority uint) {}
|
func (f *FakeService) RegisterPreLogoutHook(hook authn.PreLogoutHookFn, priority uint) {}
|
||||||
@@ -127,6 +157,10 @@ func (f *FakeClient) Authenticate(ctx context.Context, r *authn.Request) (*authn
|
|||||||
|
|
||||||
func (f FakeClient) IsEnabled() bool { return true }
|
func (f FakeClient) IsEnabled() bool { return true }
|
||||||
|
|
||||||
|
func (f *FakeClient) GetConfig() authn.SSOClientConfig {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (f *FakeClient) Test(ctx context.Context, r *authn.Request) bool {
|
func (f *FakeClient) Test(ctx context.Context, r *authn.Request) bool {
|
||||||
return f.ExpectedTest
|
return f.ExpectedTest
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/authn"
|
"github.com/grafana/grafana/pkg/services/authn"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ authn.Service = new(MockService)
|
var (
|
||||||
var _ authn.IdentitySynchronizer = new(MockService)
|
_ authn.Service = new(MockService)
|
||||||
|
_ authn.IdentitySynchronizer = new(MockService)
|
||||||
|
)
|
||||||
|
|
||||||
type MockService struct {
|
type MockService struct {
|
||||||
SyncIdentityFunc func(ctx context.Context, identity *authn.Identity) error
|
SyncIdentityFunc func(ctx context.Context, identity *authn.Identity) error
|
||||||
@@ -25,6 +27,10 @@ func (m *MockService) IsClientEnabled(name string) bool {
|
|||||||
panic("unimplemented")
|
panic("unimplemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockService) GetClientConfig(name string) (authn.SSOClientConfig, bool) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MockService) Login(ctx context.Context, client string, r *authn.Request) (*authn.Identity, error) {
|
func (m *MockService) Login(ctx context.Context, client string, r *authn.Request) (*authn.Identity, error) {
|
||||||
panic("unimplemented")
|
panic("unimplemented")
|
||||||
}
|
}
|
||||||
@@ -66,10 +72,12 @@ func (m *MockService) SyncIdentity(ctx context.Context, identity *authn.Identity
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ authn.HookClient = new(MockClient)
|
var (
|
||||||
var _ authn.LogoutClient = new(MockClient)
|
_ authn.HookClient = new(MockClient)
|
||||||
var _ authn.ContextAwareClient = new(MockClient)
|
_ authn.LogoutClient = new(MockClient)
|
||||||
var _ authn.IdentityResolverClient = new(MockClient)
|
_ authn.ContextAwareClient = new(MockClient)
|
||||||
|
_ authn.IdentityResolverClient = new(MockClient)
|
||||||
|
)
|
||||||
|
|
||||||
type MockClient struct {
|
type MockClient struct {
|
||||||
NameFunc func() string
|
NameFunc func() string
|
||||||
@@ -100,6 +108,10 @@ func (m MockClient) IsEnabled() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m MockClient) GetConfig() authn.SSOClientConfig {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m MockClient) Test(ctx context.Context, r *authn.Request) bool {
|
func (m MockClient) Test(ctx context.Context, r *authn.Request) bool {
|
||||||
if m.TestFunc != nil {
|
if m.TestFunc != nil {
|
||||||
return m.TestFunc(ctx, r)
|
return m.TestFunc(ctx, r)
|
||||||
|
|||||||
@@ -27,9 +27,11 @@ var (
|
|||||||
errAPIKeyOrgMismatch = errutil.Unauthorized("api-key.organization-mismatch", errutil.WithPublicMessage("API key does not belong to the requested organization"))
|
errAPIKeyOrgMismatch = errutil.Unauthorized("api-key.organization-mismatch", errutil.WithPublicMessage("API key does not belong to the requested organization"))
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ authn.HookClient = new(APIKey)
|
var (
|
||||||
var _ authn.ContextAwareClient = new(APIKey)
|
_ authn.HookClient = new(APIKey)
|
||||||
var _ authn.IdentityResolverClient = new(APIKey)
|
_ authn.ContextAwareClient = new(APIKey)
|
||||||
|
_ authn.IdentityResolverClient = new(APIKey)
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
metaKeyID = "keyID"
|
metaKeyID = "keyID"
|
||||||
|
|||||||
@@ -150,7 +150,8 @@ func (s *ExtendedJWT) authenticateAsUser(
|
|||||||
RestrictedActions: accessTokenClaims.Rest.DelegatedPermissions,
|
RestrictedActions: accessTokenClaims.Rest.DelegatedPermissions,
|
||||||
},
|
},
|
||||||
FetchSyncedUser: true,
|
FetchSyncedUser: true,
|
||||||
}}, nil
|
},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ExtendedJWT) authenticateAsService(accessTokenClaims authlib.Claims[authlib.AccessTokenClaims]) (*authn.Identity, error) {
|
func (s *ExtendedJWT) authenticateAsService(accessTokenClaims authlib.Claims[authlib.AccessTokenClaims]) (*authn.Identity, error) {
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var errBadForm = errutil.BadRequest("form-auth.invalid", errutil.WithPublicMessage("bad login data"))
|
||||||
errBadForm = errutil.BadRequest("form-auth.invalid", errutil.WithPublicMessage("bad login data"))
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ authn.Client = new(Form)
|
var _ authn.Client = new(Form)
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi
|
|||||||
SyncOrgRoles: !s.cfg.JWTAuth.SkipOrgRoleSync,
|
SyncOrgRoles: !s.cfg.JWTAuth.SkipOrgRoleSync,
|
||||||
AllowSignUp: s.cfg.JWTAuth.AutoSignUp,
|
AllowSignUp: s.cfg.JWTAuth.AutoSignUp,
|
||||||
SyncTeams: s.cfg.JWTAuth.GroupsAttributePath != "",
|
SyncTeams: s.cfg.JWTAuth.GroupsAttributePath != "",
|
||||||
}}
|
},
|
||||||
|
}
|
||||||
|
|
||||||
if key := s.cfg.JWTAuth.UsernameClaim; key != "" {
|
if key := s.cfg.JWTAuth.UsernameClaim; key != "" {
|
||||||
id.Login, _ = claims[key].(string)
|
id.Login, _ = claims[key].(string)
|
||||||
@@ -117,7 +118,6 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi
|
|||||||
|
|
||||||
return role, &grafanaAdmin, nil
|
return role, &grafanaAdmin, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,8 +60,11 @@ func fromSocialErr(err *connectors.SocialError) error {
|
|||||||
return errutil.Unauthorized("auth.oauth.userinfo.failed", errutil.WithPublicMessage(err.Error())).Errorf("%w", err)
|
return errutil.Unauthorized("auth.oauth.userinfo.failed", errutil.WithPublicMessage(err.Error())).Errorf("%w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ authn.LogoutClient = new(OAuth)
|
var (
|
||||||
var _ authn.RedirectClient = new(OAuth)
|
_ authn.LogoutClient = new(OAuth)
|
||||||
|
_ authn.RedirectClient = new(OAuth)
|
||||||
|
_ authn.SSOSettingsAwareClient = new(OAuth)
|
||||||
|
)
|
||||||
|
|
||||||
func ProvideOAuth(
|
func ProvideOAuth(
|
||||||
name string, cfg *setting.Cfg, oauthService oauthtoken.OAuthTokenService,
|
name string, cfg *setting.Cfg, oauthService oauthtoken.OAuthTokenService,
|
||||||
@@ -203,6 +206,15 @@ func (c *OAuth) IsEnabled() bool {
|
|||||||
return provider.Enabled
|
return provider.Enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *OAuth) GetConfig() authn.SSOClientConfig {
|
||||||
|
provider := c.socialService.GetOAuthInfoProvider(c.providerName)
|
||||||
|
if provider == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider
|
||||||
|
}
|
||||||
|
|
||||||
func (c *OAuth) RedirectURL(ctx context.Context, r *authn.Request) (*authn.Redirect, error) {
|
func (c *OAuth) RedirectURL(ctx context.Context, r *authn.Request) (*authn.Redirect, error) {
|
||||||
var opts []oauth2.AuthCodeOption
|
var opts []oauth2.AuthCodeOption
|
||||||
|
|
||||||
@@ -274,7 +286,7 @@ func (c *OAuth) Logout(ctx context.Context, user identity.Requester) (*authn.Red
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
if isOICDLogout(redirectURL) && token != nil && token.Valid() {
|
if isOIDCLogout(redirectURL) && token != nil && token.Valid() {
|
||||||
if idToken, ok := token.Extra("id_token").(string); ok {
|
if idToken, ok := token.Extra("id_token").(string); ok {
|
||||||
redirectURL = withIDTokenHint(redirectURL, idToken)
|
redirectURL = withIDTokenHint(redirectURL, idToken)
|
||||||
}
|
}
|
||||||
@@ -346,7 +358,7 @@ func withIDTokenHint(redirectURL string, idToken string) string {
|
|||||||
return u.String()
|
return u.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func isOICDLogout(redirectUrl string) bool {
|
func isOIDCLogout(redirectUrl string) bool {
|
||||||
if redirectUrl == "" {
|
if redirectUrl == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/rendering"
|
"github.com/grafana/grafana/pkg/services/rendering"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var errInvalidRenderKey = errutil.Unauthorized("render-auth.invalid-key", errutil.WithPublicMessage("Invalid Render Key"))
|
||||||
errInvalidRenderKey = errutil.Unauthorized("render-auth.invalid-key", errutil.WithPublicMessage("Invalid Render Key"))
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
renderCookieName = "renderKey"
|
renderCookieName = "renderKey"
|
||||||
|
|||||||
Reference in New Issue
Block a user