mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
Merge commit from fork
* Add DenyLists parsing * Remove unneeded returned error * Plug global denylist into Command * app creation: apply denylist to backchannel logout URI * Inject denylist to backchannel logout worker * webhook config: validate against blocked URLs * Add notificationsWebhook denylist target * command: Add SMTP endpoint validation against blocklist * command: Add SMS endpoint validation against blocklist * Validate webhook endpoint against denylist on channel notification * Remove unused tests * handle deprecated denylists * remove unintended denylist entry in deprecated list * use single http client * fix tests * update comments * fixes * cleanup * address comments * fix merge --------- Co-authored-by: Livio Spring <[email protected]>
This commit is contained in:
+35
-10
@@ -598,12 +598,9 @@ Executions:
|
||||
MaxTtl: 5m # ZITADEL_EXECUTIONS_MAXTTL
|
||||
# List of domains and IPs that are not valid execution target's endpoints
|
||||
# Wildcard sub domains are currently unsupported
|
||||
|
||||
# Deprecated: use HTTPClient.DenyList instead. If both are set, this list will be merged into the HTTPClient.DenyList.
|
||||
DenyList: # ZITADEL_EXECUTIONS_DENYLIST (comma separated list)
|
||||
- localhost
|
||||
- "127.0.0.0/8"
|
||||
- "::1"
|
||||
- "0.0.0.0"
|
||||
- "::"
|
||||
|
||||
Auth:
|
||||
# See Projections.BulkLimit
|
||||
@@ -1033,12 +1030,8 @@ SystemDefaults:
|
||||
Actions:
|
||||
HTTP:
|
||||
# Wildcard sub domains are currently unsupported
|
||||
# Deprecated: use HTTPClient.DenyList instead. If both are set, this list will be merged into the HTTPClient.DenyList.
|
||||
DenyList: # ZITADEL_ACTIONS_HTTP_DENYLIST (comma separated list)
|
||||
- localhost
|
||||
- "127.0.0.0/8"
|
||||
- "::1"
|
||||
- "0.0.0.0"
|
||||
- "::"
|
||||
|
||||
LogStore:
|
||||
Access:
|
||||
@@ -1072,6 +1065,38 @@ Eventstore:
|
||||
# Maximum amount of push retries in case of primary key violation on the sequence
|
||||
MaxRetries: 5 #ZITADEL_EVENTSTORE_MAXRETRIES
|
||||
|
||||
HTTPClient:
|
||||
# Sets the limit of the response body size in bytes, which will be processed by the HTTP client.
|
||||
# If the response body exceeds this limit, the client will return an error.
|
||||
# The default value is 20MB.
|
||||
MaxBodySize: 20971520 # ZITADEL_HTTPCLIENT_MAXBODYSIZE
|
||||
# Sets the timeout for the HTTP client.
|
||||
Timeout: 30s # ZITADEL_HTTPCLIENT_TIMEOUT
|
||||
# Sets the number of allowed redirects for the HTTP client.
|
||||
MaxRedirects: 5 # ZITADEL_HTTPCLIENT_MAXREDIRECTS
|
||||
# Specifies if the HTTP client should follow redirects from HTTPS to HTTP.
|
||||
AllowHTTPSDowngrade: false # ZITADEL_HTTPCLIENT_ALLOWHTTPSDOWNGRADE
|
||||
# Specifies the list of IP addresses or CIDR ranges that are not allowed to be called by the HTTP client.
|
||||
# Note that hosts / domains are matched best-case and wildcards are not supported.
|
||||
# CRITICAL SECURITY NOTE: Domain rules (like "localhost") protect application-layer string matches.
|
||||
# Underneath, the network dialer relies exclusively on the CIDR ranges below to block resolved IPs and prevent DNS Rebinding.
|
||||
# Do not remove the CIDR subnets even if a domain mapping exists.
|
||||
DenyList: # ZITADEL_HTTPCLIENT_DENYLIST (comma separated list)
|
||||
- localhost
|
||||
- "0.0.0.0/8" # Unspecified IPv4 address range / Local network routing trick protection
|
||||
- "10.0.0.0/8" # Class A private networks
|
||||
- "100.64.0.0/10" # Carrier-Grade NAT space
|
||||
- "127.0.0.0/8" # Loopback addresses (IPv4)
|
||||
- "169.254.0.0/16" # Link-local subnets (Covers Cloud Metadata Endpoints like AWS/GCP 169.254.169.254)
|
||||
- "172.16.0.0/12" # Class B private networks
|
||||
- "192.168.0.0/16" # Class C private networks
|
||||
- "198.18.0.0/15" # Benchmark/inter-network communication testing
|
||||
- "::/128" # Unspecified IPv6 address
|
||||
- "::1/128" # IPv6 loopback address
|
||||
- "::ffff:0:0/96" # IPv4-mapped IPv6 addresses (Prevents IPv6 encapsulation bypasses)
|
||||
- "fc00::/7" # Unique local addresses / IPv6 equivalent to private IPv4 subnets
|
||||
- "fe80::/10" # Link-local addresses / IPv6 equivalent to cloud metadata subnets
|
||||
|
||||
# The DefaultInstance section defines the default values for each new virtual instance that is created.
|
||||
# Check out https://zitadel.com/docs/concepts/structure/instance#multiple-virtual-instances for more information about virtual instances.
|
||||
# For the initial setup, the default values are used to create the first instance.
|
||||
|
||||
@@ -211,3 +211,35 @@ FirstInstance:
|
||||
|
||||
Log:
|
||||
Level: info
|
||||
|
||||
HTTPClient:
|
||||
# Sets the limit of the response body size in bytes, which will be processed by the HTTP client.
|
||||
# If the response body exceeds this limit, the client will return an error.
|
||||
# The default value is 20MB.
|
||||
MaxBodySize: 20971520 # ZITADEL_HTTPCLIENT_MAXBODYSIZE
|
||||
# Sets the timeout for the HTTP client.
|
||||
Timeout: 30s # ZITADEL_HTTPCLIENT_TIMEOUT
|
||||
# Sets the number of allowed redirects for the HTTP client.
|
||||
MaxRedirects: 5 # ZITADEL_HTTPCLIENT_MAXREDIRECTS
|
||||
# Specifies if the HTTP client should follow redirects from HTTPS to HTTP.
|
||||
AllowHTTPSDowngrade: false # ZITADEL_HTTPCLIENT_ALLOWHTTPSDOWNGRADE
|
||||
# Specifies the list of IP addresses or CIDR ranges that are not allowed to be called by the HTTP client.
|
||||
# Note that hosts / domains are matched best-case and wildcards are not supported.
|
||||
# CRITICAL SECURITY NOTE: Domain rules (like "localhost") protect application-layer string matches.
|
||||
# Underneath, the network dialer relies exclusively on the CIDR ranges below to block resolved IPs and prevent DNS Rebinding.
|
||||
# Do not remove the CIDR subnets even if a domain mapping exists.
|
||||
DenyList: # ZITADEL_HTTPCLIENT_DENYLIST (comma separated list)
|
||||
- localhost
|
||||
- "0.0.0.0/8" # Unspecified IPv4 address range / Local network routing trick protection
|
||||
- "10.0.0.0/8" # Class A private networks
|
||||
- "100.64.0.0/10" # Carrier-Grade NAT space
|
||||
- "127.0.0.0/8" # Loopback addresses (IPv4)
|
||||
- "169.254.0.0/16" # Link-local subnets (Covers Cloud Metadata Endpoints like AWS/GCP 169.254.169.254)
|
||||
- "172.16.0.0/12" # Class B private networks
|
||||
- "192.168.0.0/16" # Class C private networks
|
||||
- "198.18.0.0/15" # Benchmark/inter-network communication testing
|
||||
- "::/128" # Unspecified IPv6 address
|
||||
- "::1/128" # IPv6 loopback address
|
||||
- "::ffff:0:0/96" # IPv4-mapped IPv6 addresses (Prevents IPv6 encapsulation bypasses)
|
||||
- "fc00::/7" # Unique local addresses / IPv6 equivalent to private IPv4 subnets
|
||||
- "fe80::/10" # Link-local addresses / IPv6 equivalent to cloud metadata subnets
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -22,6 +21,7 @@ import (
|
||||
admin_handler "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing/handler"
|
||||
admin_view "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing/view"
|
||||
internal_authz "github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/oidc"
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
auth_es "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing"
|
||||
@@ -34,7 +34,6 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/config/systemdefaults"
|
||||
crypto_db "github.com/zitadel/zitadel/internal/crypto/database"
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/denylist"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql"
|
||||
@@ -109,6 +108,7 @@ type ProjectionsConfig struct {
|
||||
WebAuthNName string
|
||||
DefaultInstance command.InstanceSetup
|
||||
AssetStorage static_config.AssetStorageConfig
|
||||
HTTPClient http.ClientConfig
|
||||
}
|
||||
|
||||
func migrateProjectionsFlags(cmd *cobra.Command) {
|
||||
@@ -187,6 +187,9 @@ func projections(
|
||||
DisplayName: config.WebAuthNName,
|
||||
ExternalSecure: config.ExternalSecure,
|
||||
}
|
||||
|
||||
httpClient := config.HTTPClient.NewClient()
|
||||
|
||||
commands, err := command.StartCommands(ctx,
|
||||
es,
|
||||
cacheConnectors,
|
||||
@@ -206,7 +209,7 @@ func projections(
|
||||
keys.SAML,
|
||||
keys.Target,
|
||||
keys.OIDC,
|
||||
&http.Client{},
|
||||
httpClient,
|
||||
func(ctx context.Context, permission, orgID, resourceID string) (err error) {
|
||||
return internal_authz.CheckPermission(ctx, authZRepo, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID)
|
||||
},
|
||||
@@ -216,7 +219,7 @@ func projections(
|
||||
config.OIDC.DefaultRefreshTokenIdleExpiration,
|
||||
config.DefaultInstance.SecretGenerators,
|
||||
config.Login.DefaultPaths,
|
||||
[]denylist.AddressChecker{},
|
||||
config.HTTPClient.DenyList,
|
||||
)
|
||||
logging.OnError(ctx, err).Fatal("unable to start commands")
|
||||
|
||||
@@ -246,6 +249,7 @@ func projections(
|
||||
keys.SMTP,
|
||||
keys.SMS,
|
||||
nil,
|
||||
httpClient,
|
||||
)
|
||||
|
||||
config.Auth.Spooler.Client = client
|
||||
|
||||
+5
-2
@@ -3,6 +3,7 @@ package setup
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -47,6 +48,8 @@ type FirstInstance struct {
|
||||
externalPort uint16
|
||||
domain string
|
||||
defaultPaths *login.DefaultPaths
|
||||
httpClient *http.Client
|
||||
denylist []denylist.AddressChecker
|
||||
}
|
||||
|
||||
func (mig *FirstInstance) Execute(ctx context.Context, _ eventstore.Event) error {
|
||||
@@ -89,7 +92,7 @@ func (mig *FirstInstance) Execute(ctx context.Context, _ eventstore.Event) error
|
||||
nil,
|
||||
nil,
|
||||
oidcEncryption,
|
||||
nil,
|
||||
mig.httpClient,
|
||||
nil,
|
||||
nil,
|
||||
0,
|
||||
@@ -97,7 +100,7 @@ func (mig *FirstInstance) Execute(ctx context.Context, _ eventstore.Event) error
|
||||
0,
|
||||
nil,
|
||||
mig.defaultPaths,
|
||||
[]denylist.AddressChecker{},
|
||||
mig.denylist,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/zitadel/zitadel/cmd/hooks"
|
||||
"github.com/zitadel/zitadel/internal/actions"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/oidc"
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/cache/connector"
|
||||
@@ -64,6 +65,7 @@ type Config struct {
|
||||
WebAuthNName string
|
||||
Telemetry *handlers.TelemetryPusherConfig
|
||||
SystemAPIUsers map[string]*authz.SystemAPIUser
|
||||
HTTPClient http.ClientConfig
|
||||
}
|
||||
|
||||
type InitProjections struct {
|
||||
@@ -114,6 +116,8 @@ func NewConfig(cmd *cobra.Command, v *viper.Viper) (*Config, instrumentation.Shu
|
||||
|
||||
id.Configure(config.Machine)
|
||||
|
||||
config.HTTPClient.MergeDeprecatedDenylists(nil, config.Executions.DenyList)
|
||||
|
||||
// Copy the global role permissions mappings to the instance until we allow instance-level configuration over the API.
|
||||
config.DefaultInstance.RolePermissionMappings = config.InternalAuthZ.RolePermissionMappings
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/cache/connector"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/config/systemdefaults"
|
||||
"github.com/zitadel/zitadel/internal/denylist"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
@@ -61,7 +60,7 @@ func (mig *externalConfigChange) Execute(ctx context.Context, _ eventstore.Event
|
||||
0,
|
||||
nil,
|
||||
&login.DefaultPaths{},
|
||||
[]denylist.AddressChecker{},
|
||||
nil,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
||||
+9
-3
@@ -170,6 +170,8 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
||||
MaxRetries: config.Eventstore.MaxRetries,
|
||||
}))
|
||||
|
||||
httpClient := config.HTTPClient.NewClient()
|
||||
|
||||
steps.s1ProjectionTable = &ProjectionTable{dbClient: dbClient.DB}
|
||||
steps.s2AssetsTable = &AssetTable{dbClient: dbClient.DB}
|
||||
|
||||
@@ -187,6 +189,8 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
||||
steps.FirstInstance.externalSecure = config.ExternalSecure
|
||||
steps.FirstInstance.externalPort = config.ExternalPort
|
||||
steps.FirstInstance.defaultPaths = config.Login.DefaultPaths
|
||||
steps.FirstInstance.httpClient = httpClient
|
||||
steps.FirstInstance.denylist = config.HTTPClient.DenyList
|
||||
|
||||
steps.s5LastFailed = &LastFailed{dbClient: dbClient.DB}
|
||||
steps.s6OwnerRemoveColumns = &OwnerRemoveColumns{dbClient: dbClient.DB}
|
||||
@@ -315,7 +319,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
||||
}
|
||||
}
|
||||
|
||||
commands, _, _, _ := startCommandsQueries(ctx, eventstoreClient, eventstoreV4, dbClient, masterKey, config)
|
||||
commands, _, _, _ := startCommandsQueries(ctx, eventstoreClient, eventstoreV4, dbClient, masterKey, config, httpClient)
|
||||
steps.s59SetupWebkeys = &SetupWebkeys{eventstore: eventstoreClient, commands: commands}
|
||||
|
||||
repeatableSteps := []migration.RepeatableMigration{
|
||||
@@ -454,6 +458,7 @@ func startCommandsQueries(
|
||||
dbClient *database.DB,
|
||||
masterKey string,
|
||||
config *Config,
|
||||
httpClient *http.Client,
|
||||
) (
|
||||
*command.Commands,
|
||||
*query.Queries,
|
||||
@@ -572,7 +577,7 @@ func startCommandsQueries(
|
||||
keys.SAML,
|
||||
keys.Target,
|
||||
keys.OIDC,
|
||||
&http.Client{},
|
||||
httpClient,
|
||||
permissionCheck,
|
||||
sessionTokenVerifier,
|
||||
config.OIDC.DefaultAccessTokenLifetime,
|
||||
@@ -580,7 +585,7 @@ func startCommandsQueries(
|
||||
config.OIDC.DefaultRefreshTokenIdleExpiration,
|
||||
config.DefaultInstance.SecretGenerators,
|
||||
config.Login.DefaultPaths,
|
||||
config.Executions.DenyList,
|
||||
config.HTTPClient.DenyList,
|
||||
)
|
||||
logging.OnError(ctx, err).Fatal("unable to start commands")
|
||||
|
||||
@@ -610,6 +615,7 @@ func startCommandsQueries(
|
||||
keys.SMTP,
|
||||
keys.SMS,
|
||||
q,
|
||||
httpClient,
|
||||
)
|
||||
|
||||
return commands, queries, adminView, authView
|
||||
|
||||
+6
-1
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/actions"
|
||||
admin_es "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
"github.com/zitadel/zitadel/internal/api/oidc"
|
||||
"github.com/zitadel/zitadel/internal/api/saml"
|
||||
@@ -87,6 +88,7 @@ type Config struct {
|
||||
Quotas *QuotasConfig
|
||||
Telemetry *handlers.TelemetryPusherConfig
|
||||
ServicePing *serviceping.Config
|
||||
HTTPClient *http.ClientConfig
|
||||
}
|
||||
|
||||
type QuotasConfig struct {
|
||||
@@ -121,9 +123,12 @@ func NewConfig(cmd *cobra.Command, v *viper.Viper) (*Config, instrumentation.Shu
|
||||
}
|
||||
|
||||
id.Configure(config.Machine)
|
||||
|
||||
var actionsDenylist []denylist.AddressChecker
|
||||
if config.Actions != nil {
|
||||
actions.SetHTTPConfig(&config.Actions.HTTP)
|
||||
actionsDenylist = config.Actions.HTTP.DenyList
|
||||
}
|
||||
config.HTTPClient.MergeDeprecatedDenylists(actionsDenylist, config.Executions.DenyList)
|
||||
|
||||
err = config.SystemDefaults.Validate()
|
||||
if err != nil {
|
||||
|
||||
+27
-4
@@ -272,6 +272,8 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
|
||||
|
||||
new_domain.SetWebAuthNConfig(webAuthNConfig)
|
||||
|
||||
httpClient := config.HTTPClient.NewClient()
|
||||
|
||||
commands, err := command.StartCommands(ctx,
|
||||
eventstoreClient,
|
||||
cacheConnectors,
|
||||
@@ -291,7 +293,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
|
||||
keys.SAML,
|
||||
keys.Target,
|
||||
keys.OIDC,
|
||||
&http.Client{},
|
||||
httpClient,
|
||||
permissionCheck,
|
||||
sessionTokenVerifier,
|
||||
config.OIDC.DefaultAccessTokenLifetime,
|
||||
@@ -299,7 +301,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
|
||||
config.OIDC.DefaultRefreshTokenIdleExpiration,
|
||||
config.DefaultInstance.SecretGenerators,
|
||||
config.Login.DefaultPaths,
|
||||
config.Executions.DenyList,
|
||||
config.HTTPClient.DenyList,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot start commands: %w", err)
|
||||
@@ -345,12 +347,14 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
|
||||
keys.SMTP,
|
||||
keys.SMS,
|
||||
q,
|
||||
httpClient,
|
||||
)
|
||||
notification.Start(ctx)
|
||||
|
||||
execution.Register(
|
||||
ctx,
|
||||
config.Executions,
|
||||
httpClient,
|
||||
q,
|
||||
keys.Target,
|
||||
queries.GetActiveSigningWebKey,
|
||||
@@ -390,6 +394,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
|
||||
keys,
|
||||
permissionCheck,
|
||||
cacheConnectors,
|
||||
httpClient,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -435,6 +440,7 @@ func startAPIs(
|
||||
keys *encryption.EncryptionKeys,
|
||||
permissionCheck domain.PermissionCheck,
|
||||
cacheConnectors connector.Connectors,
|
||||
httpClient *http.Client,
|
||||
) (*api.API, error) {
|
||||
repo := struct {
|
||||
authz_repo.Repository
|
||||
@@ -492,7 +498,7 @@ func startAPIs(
|
||||
keys.Target,
|
||||
translator,
|
||||
config.Instrumentation.Trace.TrustRemoteSpans,
|
||||
config.Executions.DenyList,
|
||||
httpClient,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating api %w", err)
|
||||
@@ -675,13 +681,29 @@ func startAPIs(
|
||||
config.Log.Slog(),
|
||||
config.SystemDefaults.SecretHasher,
|
||||
federatedLogoutsCache,
|
||||
httpClient,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to start oidc provider: %w", err)
|
||||
}
|
||||
apis.RegisterHandlerPrefixes(oidcServer, oidcPrefixes...)
|
||||
|
||||
samlProvider, err := saml.NewProvider(config.SAML, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.SAML, keys.Target, eventstore, dbClient, instanceInterceptor.Handler, userAgentInterceptor, limitingAccessInterceptor)
|
||||
samlProvider, err := saml.NewProvider(
|
||||
config.SAML,
|
||||
config.ExternalSecure,
|
||||
commands,
|
||||
queries,
|
||||
authRepo,
|
||||
keys.OIDC,
|
||||
keys.SAML,
|
||||
keys.Target,
|
||||
eventstore,
|
||||
dbClient,
|
||||
instanceInterceptor.Handler,
|
||||
userAgentInterceptor,
|
||||
limitingAccessInterceptor,
|
||||
httpClient,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to start saml provider: %w", err)
|
||||
}
|
||||
@@ -726,6 +748,7 @@ func startAPIs(
|
||||
keys.CSRFCookieKey,
|
||||
cacheConnectors,
|
||||
federatedLogoutsCache,
|
||||
httpClient,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to start login: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user